@morehao/dsh-github-trending 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -76,7 +76,6 @@ var DICT = {
76
76
  "core.installed": "已安装",
77
77
  "core.missing": "未安装",
78
78
  "core.override": "覆盖路径:{path}",
79
- "core.path.advancedHint": "仅开发/排障使用;留空走自动 Release 缓存/下载。",
80
79
  "core.order": "解析顺序:TRENDING_DIGEST_CORE(仅开发/排障)→ Release 资产(缓存命中直接用,否则下载并校验);都不可用时报 CORE_UNAVAILABLE。",
81
80
  "coexist.title": "检测到本机另有 CLI",
82
81
  "coexist.hint": "插件仍使用自带核心,不切换、不接管它的定时。",
@@ -92,10 +91,10 @@ var DICT = {
92
91
  "model.mode": "密钥来源",
93
92
  "model.mode.direct": "DSH 凭据 / 手动密钥",
94
93
  "model.mode.proxy": "回环代理(凭据不可导出)",
95
- "model.keyRef": "凭据引用",
96
94
  "model.credential": "凭据状态",
97
- "model.credential.hint": "沿用 DSH 配置时自动带入,无需填写。",
98
- "model.keyRef.advancedHint": "手动指定时才会用到;一般不填。",
95
+ "model.credential.proxyHint": "回环代理用宿主自己的凭据转发,密钥不经过插件、也不落盘,本页无需填写。",
96
+ "model.credential.directHint": "密钥在运行时从宿主凭据 / 环境变量解析,直传给核心。",
97
+ "model.credential.missingHint": "当前是直传模式,但凭据引用为空或解析不到;点右上角「沿用 DSH 配置」重新填充。",
99
98
  "model.params": "参数",
100
99
  "model.preflight": "连通性预检",
101
100
  "model.preflighting": "预检中…",
@@ -133,6 +132,7 @@ var DICT = {
133
132
  "auth.status": "状态",
134
133
  "auth.configured": "已配置",
135
134
  "auth.missing": "未配置",
135
+ "auth.hostProxy": "沿用 DSH 凭据",
136
136
  "card.scope.title": "榜单范围",
137
137
  "scope.period": "周期",
138
138
  "scope.daily": "每日",
@@ -157,8 +157,6 @@ var DICT = {
157
157
  "settings.troubleshoot": "排障",
158
158
  "settings.copyConfig": "复制脱敏配置",
159
159
  "settings.copied": "已复制",
160
- "settings.advanced": "高级设置",
161
- "settings.advanced.hide": "收起高级",
162
160
  "settings.lastRun": "上次运行",
163
161
  "run.now": "本次",
164
162
  "run.history": "历史",
@@ -186,7 +184,7 @@ var DICT = {
186
184
  "run.usage.branch": "分支",
187
185
  "run.usage.pr": "PR",
188
186
  "run.pendingPr": "存在未合并 PR",
189
- "run.pendingPr.hint": "同日重跑会更新该 PR(核心以 --force-with-lease 推送);若已合并/关闭,记录会自动清理。",
187
+ "run.pendingPr.hint": "同日重跑会在分支上追加提交并更新该 PR;若已合并/关闭,记录会自动清理。",
190
188
  "run.error.title": "最近错误",
191
189
  "run.history.title": "历史记录",
192
190
  "run.history.hint": "最近 30 条,超出即裁剪",
@@ -216,7 +214,132 @@ var DICT = {
216
214
  "quickfill.close": "关闭",
217
215
  "quickfill.loading": "读取中…",
218
216
  "error.prefix": "错误",
219
- "label.optional": "可选"
217
+ "err.reason.stale-lease": "推送被拒(stale info):本地对远端分支的记录过期(分支被删或被改),并非有人抢先推送。在仓库里执行 `git fetch --prune origin` 后重试。",
218
+ "err.reason.non-fast-forward": "推送被拒(non-fast-forward):远端分支已前进。先 `git fetch` 并 rebase 到远端分支后重试。",
219
+ "err.reason.auth": "推送认证失败:更新 GitHub token 或重新 `gh auth login` 后重试;本地提交已保留。",
220
+ "err.reason.protected": "目标分支受保护:改为通过 PR 提交,或调整分支保护规则。",
221
+ "err.reason.hook": "仓库 hook 拒绝了操作:按 hook 输出修正后重试;本地提交已保留。",
222
+ "err.reason.pathspec": "暂存路径不存在:检查设置里的输出目录是否与仓库实际目录一致。",
223
+ "err.reason.unknown": "推送失败:本地提交已保留,查看运行日志后重试。",
224
+ "skip.merged": "该日期的 PR 已合并,本次跳过。",
225
+ "skip.pr-closed": "该日期的 PR 已被关闭(未合并);定时任务不重开,如需重开请手动运行。",
226
+ "skip.unchanged": "内容与分支上已有产物一致,未新增提交。",
227
+ "label.optional": "可选",
228
+ "nav.overview": "概览",
229
+ "nav.digest": "日报",
230
+ "nav.runs": "运行记录",
231
+ "nav.settings": "设置",
232
+ "shell.nav": "面板",
233
+ "shell.overview.dek": "每天定时生成日报;先看下次运行、就绪检查与上次交付。",
234
+ "shell.digest.dek": "按日期回看最近 30 天;只有保留过预览副本的期数能读。",
235
+ "shell.runs.dek": "最近 30 次运行;展开看交付物、错误与日志。",
236
+ "shell.settings.dek": "沿用现有设置页;每个分区独立保存。",
237
+ "shell.nextRun": "下次运行",
238
+ "shell.scheduleOff": "定时未开启",
239
+ "shell.ready": "就绪 {ready} / {total}",
240
+ "shell.github": "GitHub",
241
+ "shell.package.copy": "复制完整包名",
242
+ "time.in.minutes": "{minutes} 分钟后",
243
+ "time.in.hours": "{hours} 小时 {minutes} 分后",
244
+ "time.in.days": "{days} 天后",
245
+ "overview.last.none": "还没有运行记录",
246
+ "overview.last.files": "{count} 个文件",
247
+ "overview.last.digest": "{date} · GitHub 趋势日报",
248
+ "overview.last.read": "阅读本期",
249
+ "overview.last.noPreview": "这次运行没有保留预览副本",
250
+ "overview.last.prPending": "待合并",
251
+ "overview.last.prCreated": "PR 已创建",
252
+ "overview.last.prUnknown": "PR 状态未知",
253
+ "overview.last.direct": "直接提交",
254
+ "overview.model": "模型",
255
+ "overview.ready.title": "就绪检查",
256
+ "overview.ready.core": "核心",
257
+ "overview.ready.model": "模型",
258
+ "overview.ready.repo": "仓库",
259
+ "overview.ready.github": "GitHub",
260
+ "overview.ready.view": "查看",
261
+ "overview.week.title": "最近 7 天",
262
+ "overview.week.all": "全部运行记录",
263
+ "overview.week.none": "无记录",
264
+ "overview.week.wait": "等待 {time}",
265
+ "overview.live.title": "正在运行",
266
+ "overview.live.core": "核心执行中",
267
+ "overview.live.note": "核心一次完成抓取、总结、渲染与提交,结束前没有分段进度;这里只显示已用时间和粗阶段。",
268
+ "overview.live.started": "开始于",
269
+ "run.dryRunShort": "试跑",
270
+ "run.usage.commit": "提交",
271
+ "runs.count": "最近 {count} 次,超出即裁剪",
272
+ "runs.files": "{count} 个文件",
273
+ "runs.noChange": "内容与上一期一致,核心没有产生新文件",
274
+ "runs.dryHint": "试跑不写文件、不提交",
275
+ "runs.empty": "没有符合条件的记录",
276
+ "runs.filter.all": "全部",
277
+ "runs.filter.failed": "失败",
278
+ "runs.filter.dry": "试跑",
279
+ "runs.pendingPr": "待合并",
280
+ "runs.prUnknown": "状态未知",
281
+ "runs.prCreated": "PR 已创建",
282
+ "runs.legacy": "旧记录",
283
+ "runs.legacyHint": "旧记录没有预览副本,内容哈希使用旧算法,不能与 v2 直接比较。",
284
+ "runs.detail.coreModel": "核心 / 模型",
285
+ "runs.detail.date": "运行日期",
286
+ "runs.detail.warnings": "警告",
287
+ "runs.detail.none": "无",
288
+ "runs.detail.files": "文件 / 耗时",
289
+ "runs.detail.readDigest": "阅读本期日报",
290
+ "runs.detail.noPreview": "没有预览副本",
291
+ "runs.detail.log": "查看日志",
292
+ "digest.allRuns": "全部运行记录",
293
+ "digest.window": "最近 30 天 · {days} 天有记录 · {previews} 期有预览",
294
+ "digest.window.empty": "最近 30 天没有运行记录",
295
+ "digest.prev": "上一期(更早)",
296
+ "digest.next": "下一期(更新)",
297
+ "digest.select": "选择日报日期",
298
+ "digest.render": "渲染",
299
+ "digest.raw": "原文",
300
+ "digest.mode": "预览方式",
301
+ "digest.copyPath": "复制路径",
302
+ "digest.copied": "已复制",
303
+ "digest.openPr": "打开 PR",
304
+ "digest.truncated": "已截断到 512 KB",
305
+ "digest.loading": "读取预览…",
306
+ "digest.tag.preview": "有预览",
307
+ "digest.tag.failed": "运行失败",
308
+ "digest.tag.skipped": "无变更",
309
+ "digest.tag.dry": "试跑",
310
+ "digest.tag.legacy": "旧记录",
311
+ "digest.tag.missing": "无预览",
312
+ "digest.empty.none.title": "还没有可回看的日报",
313
+ "digest.empty.none.reason": "运行成功并保留预览副本后,这里会按日期列出最近 30 天。",
314
+ "digest.empty.failed.title": "这一期没有可预览的日报",
315
+ "digest.empty.failed.reason": "这次运行失败了,没有产出可读的日报文件。",
316
+ "digest.empty.skipped.title": "这一期没有新的日报",
317
+ "digest.empty.skipped.reason": "内容与上一期一致,核心没有产生新文件,也就没有新的预览副本。",
318
+ "digest.empty.dry.title": "试跑没有产物",
319
+ "digest.empty.dry.reason": "试跑只抓取与渲染,不写文件、不提交,所以没有可预览的产物。",
320
+ "digest.empty.legacy.title": "旧记录没有预览副本",
321
+ "digest.empty.legacy.reason": "预览副本从接入后的成功运行开始保留;更早的记录只能看运行结果。",
322
+ "digest.empty.missing.title": "预览副本没有保留",
323
+ "digest.empty.missing.reason": "这次运行成功,但当时没有保留预览副本,或副本已被清理。",
324
+ "digest.empty.viewRuns": "查看运行记录",
325
+ "digest.empty.latest": "阅读最近一期 · {date}",
326
+ "log.title": "运行日志",
327
+ "log.loading": "读取日志…",
328
+ "log.empty": "这次运行没有日志",
329
+ "log.truncated": "最多显示最后 64 KB",
330
+ "log.copy": "复制日志",
331
+ "log.copied": "已复制",
332
+ "log.close": "关闭",
333
+ "about.title": "关于",
334
+ "about.copy": "复制完整包名",
335
+ "about.copied": "已复制",
336
+ "about.close": "关闭",
337
+ "about.repo": "GitHub 仓库",
338
+ "about.repo.missing": "package.json 没有可解析的仓库地址",
339
+ "about.license": "许可",
340
+ "about.core": "核心",
341
+ "about.env": "运行环境",
342
+ "about.env.value": "DSH Web · Node 插件"
220
343
  },
221
344
  en: {
222
345
  "section.title": "GitHub Trending",
@@ -243,7 +366,6 @@ var DICT = {
243
366
  "core.installed": "installed",
244
367
  "core.missing": "missing",
245
368
  "core.override": "Override path: {path}",
246
- "core.path.advancedHint": "Dev/troubleshooting only; leave empty to use the Release cache/download.",
247
369
  "core.cached": "cached",
248
370
  "core.notDownloaded": "not downloaded",
249
371
  "core.download": "Download core",
@@ -265,10 +387,10 @@ var DICT = {
265
387
  "model.mode": "Key source",
266
388
  "model.mode.direct": "DSH credential / manual key",
267
389
  "model.mode.proxy": "Loopback proxy (credential not exportable)",
268
- "model.keyRef": "Credential ref",
269
390
  "model.credential": "Credential",
270
- "model.credential.hint": "Filled automatically when reusing DSH config; no need to enter it.",
271
- "model.keyRef.advancedHint": "Only used when setting a ref manually; usually left empty.",
391
+ "model.credential.proxyHint": "The loopback proxy forwards with the host credential; the key never passes through the plugin. Nothing to fill in here.",
392
+ "model.credential.directHint": "Resolved at run time from host credentials / env and passed straight to the core.",
393
+ "model.credential.missingHint": "Direct mode, but the ref is empty or unresolvable; use “Reuse DSH configuration” to refill it.",
272
394
  "model.params": "Parameters",
273
395
  "model.preflight": "Test connection",
274
396
  "model.preflighting": "Testing…",
@@ -306,6 +428,7 @@ var DICT = {
306
428
  "auth.status": "Status",
307
429
  "auth.configured": "configured",
308
430
  "auth.missing": "missing",
431
+ "auth.hostProxy": "uses DSH credential",
309
432
  "card.scope.title": "Trending scope",
310
433
  "scope.period": "Period",
311
434
  "scope.daily": "Daily",
@@ -330,8 +453,6 @@ var DICT = {
330
453
  "settings.troubleshoot": "Troubleshooting",
331
454
  "settings.copyConfig": "Copy redacted config",
332
455
  "settings.copied": "Copied",
333
- "settings.advanced": "Advanced settings",
334
- "settings.advanced.hide": "Hide advanced",
335
456
  "settings.lastRun": "Last run",
336
457
  "run.now": "This run",
337
458
  "run.history": "History",
@@ -359,7 +480,7 @@ var DICT = {
359
480
  "run.usage.branch": "Branch",
360
481
  "run.usage.pr": "PR",
361
482
  "run.pendingPr": "Unmerged PR exists",
362
- "run.pendingPr.hint": "Re-running the same day updates this PR (core pushes with --force-with-lease); merged/closed records are cleaned up automatically.",
483
+ "run.pendingPr.hint": "Re-running the same day appends a commit and updates this PR; merged/closed records are cleaned up automatically.",
363
484
  "run.error.title": "Last error",
364
485
  "run.history.title": "History",
365
486
  "run.history.hint": "last 30 records",
@@ -389,7 +510,132 @@ var DICT = {
389
510
  "quickfill.close": "Close",
390
511
  "quickfill.loading": "Loading…",
391
512
  "error.prefix": "Error",
392
- "label.optional": "optional"
513
+ "err.reason.stale-lease": "Push rejected (stale info): the local record of the remote branch is out of date (deleted or moved), not a race. Run `git fetch --prune origin` in the repo and retry.",
514
+ "err.reason.non-fast-forward": "Push rejected (non-fast-forward): the remote branch moved ahead. Fetch and rebase onto it, then retry.",
515
+ "err.reason.auth": "Push authentication failed: refresh the GitHub token or run `gh auth login`, then retry. Local commits are kept.",
516
+ "err.reason.protected": "The target branch is protected: open a PR instead, or adjust the branch protection rules.",
517
+ "err.reason.hook": "A repository hook rejected the operation: fix it per the hook output and retry. Local commits are kept.",
518
+ "err.reason.pathspec": "Nothing to stage at that path: check that the output directory matches the repository layout.",
519
+ "err.reason.unknown": "Push failed. Local commits are kept; check the run log and retry.",
520
+ "skip.merged": "The PR for this date was merged; skipped.",
521
+ "skip.pr-closed": "The PR for this date was closed unmerged. Scheduled runs do not reopen it; run manually to reopen.",
522
+ "skip.unchanged": "The branch already holds the same artifacts; no new commit.",
523
+ "label.optional": "optional",
524
+ "nav.overview": "Overview",
525
+ "nav.digest": "Digest",
526
+ "nav.runs": "Runs",
527
+ "nav.settings": "Settings",
528
+ "shell.nav": "Panel",
529
+ "shell.overview.dek": "A daily digest on a schedule; start with the next run, readiness and the last delivery.",
530
+ "shell.digest.dek": "Look back over the last 30 days; only runs with a saved preview copy can be read.",
531
+ "shell.runs.dek": "The last 30 runs; expand one for artifacts, errors and logs.",
532
+ "shell.settings.dek": "The existing settings page; each section saves on its own.",
533
+ "shell.nextRun": "Next run",
534
+ "shell.scheduleOff": "Schedule off",
535
+ "shell.ready": "Ready {ready} / {total}",
536
+ "shell.github": "GitHub",
537
+ "shell.package.copy": "Copy full package name",
538
+ "time.in.minutes": "in {minutes} min",
539
+ "time.in.hours": "in {hours} h {minutes} min",
540
+ "time.in.days": "in {days} d",
541
+ "overview.last.none": "No runs yet",
542
+ "overview.last.files": "{count} files",
543
+ "overview.last.digest": "{date} · GitHub Trending digest",
544
+ "overview.last.read": "Read this issue",
545
+ "overview.last.noPreview": "This run has no saved preview copy",
546
+ "overview.last.prPending": "Pending merge",
547
+ "overview.last.prCreated": "PR created",
548
+ "overview.last.prUnknown": "PR state unknown",
549
+ "overview.last.direct": "Direct commit",
550
+ "overview.model": "Model",
551
+ "overview.ready.title": "Readiness",
552
+ "overview.ready.core": "Core",
553
+ "overview.ready.model": "Model",
554
+ "overview.ready.repo": "Repository",
555
+ "overview.ready.github": "GitHub",
556
+ "overview.ready.view": "View",
557
+ "overview.week.title": "Last 7 days",
558
+ "overview.week.all": "All runs",
559
+ "overview.week.none": "No record",
560
+ "overview.week.wait": "Waiting for {time}",
561
+ "overview.live.title": "Running",
562
+ "overview.live.core": "Core is working",
563
+ "overview.live.note": "The core fetches, summarizes, renders and commits in one run; there is no per-step progress before it finishes. Only elapsed time and a coarse phase are shown here.",
564
+ "overview.live.started": "Started",
565
+ "run.dryRunShort": "Dry run",
566
+ "run.usage.commit": "Commit",
567
+ "runs.count": "Last {count} runs; older ones are trimmed",
568
+ "runs.files": "{count} files",
569
+ "runs.noChange": "Content matches the previous issue; the core produced no new file",
570
+ "runs.dryHint": "dry run writes nothing and commits nothing",
571
+ "runs.empty": "No matching records",
572
+ "runs.filter.all": "All",
573
+ "runs.filter.failed": "Failed",
574
+ "runs.filter.dry": "Dry run",
575
+ "runs.pendingPr": "Pending merge",
576
+ "runs.prUnknown": "State unknown",
577
+ "runs.prCreated": "PR created",
578
+ "runs.legacy": "Legacy",
579
+ "runs.legacyHint": "Legacy records have no preview copy and use the old hash algorithm, so they cannot be compared with v2 directly.",
580
+ "runs.detail.coreModel": "Core / model",
581
+ "runs.detail.date": "Run date",
582
+ "runs.detail.warnings": "Warnings",
583
+ "runs.detail.none": "None",
584
+ "runs.detail.files": "Files / duration",
585
+ "runs.detail.readDigest": "Read this digest",
586
+ "runs.detail.noPreview": "No preview copy",
587
+ "runs.detail.log": "View log",
588
+ "digest.allRuns": "All runs",
589
+ "digest.window": "Last 30 days · {days} days with runs · {previews} with previews",
590
+ "digest.window.empty": "No runs in the last 30 days",
591
+ "digest.prev": "Previous (older)",
592
+ "digest.next": "Next (newer)",
593
+ "digest.select": "Choose a digest date",
594
+ "digest.render": "Rendered",
595
+ "digest.raw": "Raw",
596
+ "digest.mode": "Preview mode",
597
+ "digest.copyPath": "Copy path",
598
+ "digest.copied": "Copied",
599
+ "digest.openPr": "Open PR",
600
+ "digest.truncated": "Truncated at 512 KB",
601
+ "digest.loading": "Loading preview…",
602
+ "digest.tag.preview": "Preview",
603
+ "digest.tag.failed": "Failed",
604
+ "digest.tag.skipped": "No change",
605
+ "digest.tag.dry": "Dry run",
606
+ "digest.tag.legacy": "Legacy",
607
+ "digest.tag.missing": "No preview",
608
+ "digest.empty.none.title": "No digest to look back on yet",
609
+ "digest.empty.none.reason": "Once a run succeeds and keeps a preview copy, the last 30 days will be listed here by date.",
610
+ "digest.empty.failed.title": "No digest preview for this issue",
611
+ "digest.empty.failed.reason": "This run failed, so there is no readable digest file.",
612
+ "digest.empty.skipped.title": "No new digest for this issue",
613
+ "digest.empty.skipped.reason": "Content matched the previous issue; the core produced no new file, so there is no new preview copy.",
614
+ "digest.empty.dry.title": "A dry run produces no artifact",
615
+ "digest.empty.dry.reason": "A dry run only fetches and renders; it writes no file and commits nothing, so there is nothing to preview.",
616
+ "digest.empty.legacy.title": "Legacy records have no preview copy",
617
+ "digest.empty.legacy.reason": "Preview copies are kept from successful runs after this feature shipped; older records only have run results.",
618
+ "digest.empty.missing.title": "The preview copy was not kept",
619
+ "digest.empty.missing.reason": "This run succeeded, but no preview copy was saved at the time, or it has been cleaned up.",
620
+ "digest.empty.viewRuns": "View runs",
621
+ "digest.empty.latest": "Read the latest issue · {date}",
622
+ "log.title": "Run log",
623
+ "log.loading": "Loading log…",
624
+ "log.empty": "This run has no log",
625
+ "log.truncated": "Shows at most the last 64 KB",
626
+ "log.copy": "Copy log",
627
+ "log.copied": "Copied",
628
+ "log.close": "Close",
629
+ "about.title": "About",
630
+ "about.copy": "Copy full package name",
631
+ "about.copied": "Copied",
632
+ "about.close": "Close",
633
+ "about.repo": "GitHub repository",
634
+ "about.repo.missing": "package.json has no resolvable repository URL",
635
+ "about.license": "License",
636
+ "about.core": "Core",
637
+ "about.env": "Runtime",
638
+ "about.env.value": "DSH Web · Node plugin"
393
639
  }
394
640
  };
395
641
  function activeLang(locale) {
@@ -412,7 +658,7 @@ function buildTranslate(locale) {
412
658
  // client/styles.ts
413
659
  var STYLE_ID = "dsh-github-trending-style";
414
660
  var CSS = `
415
- .dshgt-root{--gt-fg:var(--dsw-alias-label-primary,#1f2328);--gt-muted:var(--dsw-alias-label-secondary,#6b7280);--gt-bg:var(--dsw-alias-bg-base,#fff);--gt-card:var(--dsw-alias-bg-layer-1,#f8fafc);--gt-border:var(--dsw-alias-border-l2,#e5e7eb);--gt-accent:var(--dsw-alias-state-business-primary,#2563eb);--gt-ok:var(--dsw-alias-state-success-primary,#16a34a);--gt-warn:var(--dsw-alias-state-warn-primary,#d97706);--gt-err:var(--dsw-alias-state-error-primary,#dc2626);color:var(--gt-fg);font-size:13px;line-height:1.5;display:flex;flex-direction:column;gap:12px;}
661
+ .dshgt-root{--gt-fg:var(--dsw-alias-label-primary,#1f2328);--gt-muted:var(--dsw-alias-label-secondary,#6b7280);--gt-bg:var(--dsw-alias-bg-base,#fff);--gt-card:var(--dsw-alias-bg-layer-1,#f8fafc);--gt-border:var(--dsw-alias-border-l2,#e5e7eb);--gt-accent:var(--dsw-alias-state-business-primary,#2563eb);--gt-ok:var(--dsw-alias-state-success-primary,#16a34a);--gt-warn:var(--dsw-alias-state-warn-primary,#d97706);--gt-err:var(--dsw-alias-state-error-primary,#dc2626);color:var(--gt-fg);font-size:14px;line-height:1.55;display:flex;flex-direction:column;gap:12px;}
416
662
  .dshgt-root *{box-sizing:border-box;}
417
663
  .dshgt-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;}
418
664
  .dshgt-title{font-size:15px;font-weight:600;}
@@ -477,6 +723,202 @@ var CSS = `
477
723
  .dshgt-monobox{margin:0;padding:8px;border-radius:7px;background:color-mix(in srgb,var(--gt-fg) 6%,var(--gt-bg));max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;}
478
724
  .dshgt-modal-mask{position:fixed;inset:0;background:var(--dsw-alias-bg-mask-1,rgba(15,23,42,.38));display:flex;align-items:center;justify-content:center;z-index:2147483000;}
479
725
  .dshgt-modal{background:var(--gt-bg);border-radius:12px;border:1px solid var(--gt-border);max-width:640px;width:calc(100% - 32px);max-height:80vh;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:10px;box-shadow:0 20px 50px rgba(15,23,42,.25);}
726
+
727
+ /* ——— 01 工作台外壳 ——— */
728
+ .dshgt-shell{gap:0;height:100%;min-height:0;overflow:hidden;}
729
+ .dshgt-app{flex:1;min-height:0;display:grid;grid-template-columns:216px minmax(0,1fr);background:var(--gt-bg);}
730
+ .dshgt-rail{background:color-mix(in srgb,var(--gt-fg) 3%,var(--gt-bg));border-right:1px solid var(--gt-border);display:flex;flex-direction:column;padding:14px 10px 12px;min-height:0;overflow:hidden;}
731
+ .dshgt-brand{display:flex;align-items:center;gap:9px;padding:2px 6px 16px;}
732
+ .dshgt-brand svg{flex:none;border-radius:8px;}
733
+ .dshgt-brand-name{font-size:13.5px;font-weight:600;line-height:1.3;}
734
+ .dshgt-brand-ver{font-size:11px;color:var(--gt-muted);margin-top:1px;min-height:15px;}
735
+ .dshgt-nav{display:flex;flex-direction:column;gap:2px;}
736
+ .dshgt-nav-item{display:flex;align-items:center;gap:9px;height:36px;padding:0 9px;border:1px solid transparent;border-radius:8px;background:none;text-align:left;font-family:inherit;font-size:13.5px;color:var(--gt-muted);width:100%;cursor:pointer;}
737
+ .dshgt-nav-item svg{flex:none;opacity:.82;}
738
+ .dshgt-nav-item:hover{background:color-mix(in srgb,var(--gt-fg) 6%,var(--gt-bg));color:var(--gt-fg);}
739
+ .dshgt-nav-item.on{background:var(--gt-bg);border-color:var(--gt-border);color:var(--gt-fg);font-weight:600;}
740
+ .dshgt-nav-item.on svg{opacity:1;}
741
+ .dshgt-nav-tail{margin-left:auto;display:flex;align-items:center;gap:5px;font-size:11.5px;color:var(--gt-muted);font-weight:400;}
742
+ .dshgt-rail-spacer{flex:1;min-height:16px;}
743
+ .dshgt-rail-status{border-top:1px solid var(--gt-border);padding:12px 8px 10px;}
744
+ .dshgt-rail-cap{font-size:11.5px;color:var(--gt-muted);margin-bottom:3px;}
745
+ .dshgt-rail-time{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:14px;line-height:1.35;}
746
+ .dshgt-rail-count{font-size:12px;color:var(--gt-muted);margin-top:1px;min-height:18px;}
747
+ .dshgt-rail-ready{display:flex;align-items:center;gap:7px;margin-top:10px;font-size:12px;color:var(--gt-muted);}
748
+ .dshgt-rail-pkg{border-top:1px solid var(--gt-border);padding:10px 8px 8px;}
749
+ .dshgt-pkg-name{border:0;background:none;padding:0;text-align:left;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;line-height:1.45;color:var(--gt-muted);word-break:break-all;cursor:pointer;max-width:100%;}
750
+ .dshgt-pkg-name:hover{color:var(--gt-fg);}
751
+ .dshgt-pkg-sub{display:flex;align-items:center;gap:8px;margin-top:3px;font-size:11px;color:var(--gt-muted);}
752
+ .dshgt-pkg-sub .dshgt-mono{font-size:10.5px;color:var(--gt-fg);}
753
+ .dshgt-pkg-sub a{display:inline-flex;align-items:center;gap:3px;font-size:11px;color:var(--gt-accent);text-decoration:none;}
754
+ .dshgt-pkg-sub a:hover{text-decoration:underline;}
755
+ .dshgt-pkg-copy{width:22px;height:22px;margin-left:auto;display:grid;place-items:center;border:0;background:none;border-radius:4px;color:var(--gt-muted);cursor:pointer;}
756
+ .dshgt-pkg-copy:hover{background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-bg));color:var(--gt-fg);}
757
+ .dshgt-pkg-copy:disabled{opacity:.4;cursor:not-allowed;}
758
+ .dshgt-main{display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--gt-bg);}
759
+ .dshgt-page-head{display:flex;align-items:flex-start;gap:16px;padding:20px 30px 14px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);flex:none;}
760
+ .dshgt-page-heading{min-width:0;}
761
+ .dshgt-page-head h1{margin:0;font-size:21px;line-height:1.25;font-weight:650;letter-spacing:-.012em;}
762
+ .dshgt-dek{margin:4px 0 0;font-size:13px;color:var(--gt-muted);max-width:580px;line-height:1.5;}
763
+ .dshgt-head-actions{margin-left:auto;display:flex;align-items:center;gap:8px;padding-top:2px;flex:none;}
764
+ .dshgt-main-body{flex:1;min-height:0;display:flex;min-width:0;}
765
+ .dshgt-scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;scrollbar-gutter:stable;}
766
+ .dshgt-main-body > .dshgt-settings{flex:1;min-height:0;padding:16px 24px 20px;}
767
+ .dshgt-view{padding:22px 30px 38px;max-width:1180px;width:100%;display:flex;flex-direction:column;gap:18px;}
768
+ .dshgt-grow{flex:1;min-width:0;}
769
+ .dshgt-dim{font-size:12px;color:var(--gt-muted);}
770
+ .dshgt-empty{padding:48px 0;text-align:center;color:var(--gt-muted);font-size:13px;}
771
+ .dshgt-tag{display:inline-flex;align-items:center;height:20px;padding:0 7px;border:1px solid var(--gt-border);border-radius:999px;font-size:11px;color:var(--gt-muted);background:var(--gt-bg);white-space:nowrap;}
772
+ .dshgt-inline-tag{margin-left:6px;vertical-align:1px;}
773
+
774
+ /* ——— 概览 ——— */
775
+ .dshgt-delivery{border:1px solid var(--gt-border);border-radius:12px;padding:18px 20px 16px;background:var(--gt-card);display:flex;flex-direction:column;gap:6px;}
776
+ .dshgt-delivery-top{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
777
+ .dshgt-pill-mini{margin-left:auto;font-size:12px;color:var(--gt-muted);border:1px solid var(--gt-border);border-radius:999px;padding:2px 9px;background:var(--gt-bg);}
778
+ .dshgt-delivery h2{margin:4px 0 0;font-size:18px;font-weight:600;letter-spacing:-.01em;line-height:1.35;word-break:break-word;}
779
+ .dshgt-when{display:flex;gap:14px;flex-wrap:wrap;font-size:12.5px;color:var(--gt-muted);}
780
+ .dshgt-when a{color:var(--gt-accent);text-decoration:none;}
781
+ .dshgt-when a:hover{text-decoration:underline;}
782
+ .dshgt-delivery-error{margin-top:6px;}
783
+ .dshgt-artifacts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;}
784
+ .dshgt-filechip{display:inline-flex;align-items:center;gap:7px;min-height:32px;padding:0 10px;border:1px solid var(--gt-border);border-radius:8px;background:var(--gt-bg);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-fg);cursor:pointer;text-align:left;max-width:100%;overflow:hidden;}
785
+ .dshgt-filechip .dshgt-mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
786
+ .dshgt-filechip:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
787
+ .dshgt-filechip-static{cursor:default;}
788
+ .dshgt-filechip-static:hover{border-color:var(--gt-border);}
789
+ .dshgt-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;border-top:1px solid var(--gt-border);margin-top:12px;padding-top:10px;}
790
+ .dshgt-fact{padding:0 14px;border-left:1px solid var(--gt-border);min-width:0;}
791
+ .dshgt-fact:first-child{padding-left:0;border-left:0;}
792
+ .dshgt-fact-k{font-size:11.5px;color:var(--gt-muted);margin-bottom:2px;}
793
+ .dshgt-fact-v{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
794
+ .dshgt-block{display:flex;flex-direction:column;gap:8px;}
795
+ .dshgt-section-label{display:flex;align-items:center;gap:9px;font-size:12px;color:var(--gt-muted);letter-spacing:.02em;}
796
+ .dshgt-section-label:after{content:"";height:1px;flex:1;background:color-mix(in srgb,var(--gt-border) 70%,transparent);}
797
+ .dshgt-section-tail{margin-left:auto;font-size:11.5px;color:var(--gt-muted);}
798
+ .dshgt-ready-list{border-top:1px solid var(--gt-border);}
799
+ .dshgt-ready-row{display:grid;grid-template-columns:16px 108px minmax(0,1fr) auto;align-items:center;gap:12px;min-height:46px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);font-size:13.5px;}
800
+ .dshgt-ready-label{color:var(--gt-muted);font-size:13px;}
801
+ .dshgt-ready-value{color:var(--gt-fg);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
802
+ .dshgt-ready-extra{color:var(--gt-muted);font-size:12px;margin-left:7px;}
803
+ .dshgt-linkbtn{border:0;background:none;color:var(--gt-accent);font-family:inherit;font-size:12.5px;padding:6px 2px;border-radius:4px;cursor:pointer;}
804
+ .dshgt-linkbtn:hover{text-decoration:underline;}
805
+ .dshgt-week{display:flex;gap:6px;align-items:stretch;}
806
+ .dshgt-day{flex:1;min-width:0;border:1px solid var(--gt-border);border-radius:8px;padding:8px;background:var(--gt-card);display:flex;flex-direction:column;gap:5px;text-align:left;font-family:inherit;color:inherit;}
807
+ .dshgt-day-clickable{cursor:pointer;}
808
+ .dshgt-day-clickable:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
809
+ .dshgt-day-date{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--gt-muted);}
810
+ .dshgt-day-status{display:flex;align-items:center;gap:6px;font-size:11.5px;color:var(--gt-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
811
+ .dshgt-lamp{width:7px;height:7px;border-radius:50%;background:var(--gt-muted);flex:none;}
812
+ .dshgt-lamp.ok{background:var(--gt-ok);}
813
+ .dshgt-lamp.warn{background:var(--gt-warn);}
814
+ .dshgt-lamp.err{background:var(--gt-err);}
815
+ .dshgt-live{border:1px solid var(--gt-border);border-radius:12px;padding:16px 18px;background:var(--gt-card);display:flex;flex-direction:column;gap:8px;}
816
+ .dshgt-live-top{display:flex;align-items:center;gap:9px;font-size:13px;flex-wrap:wrap;}
817
+ .dshgt-live-elapsed{margin-left:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--gt-muted);}
818
+ .dshgt-live-note{display:flex;gap:10px;flex-wrap:wrap;font-size:12px;color:var(--gt-muted);line-height:1.6;}
819
+ .dshgt-live-actions{display:flex;gap:8px;}
820
+ .dshgt-spinner{width:13px;height:13px;border-radius:50%;border:1.6px solid var(--gt-border);border-top-color:var(--gt-accent);animation:dshgt-spin .8s linear infinite;display:inline-block;flex:none;}
821
+ @keyframes dshgt-spin{to{transform:rotate(360deg);}}
822
+
823
+ /* ——— 日报 ——— */
824
+ .dshgt-digest{padding-top:20px;gap:12px;}
825
+ .dshgt-digest-subbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
826
+ .dshgt-datenav{display:flex;align-items:center;gap:2px;border:1px solid var(--gt-border);border-radius:8px;background:var(--gt-card);padding:2px;}
827
+ .dshgt-icon-btn{width:28px;height:28px;display:grid;place-items:center;border:1px solid transparent;background:var(--gt-bg);border-radius:6px;color:var(--gt-fg);font-family:inherit;font-size:18px;line-height:1;cursor:pointer;flex:none;}
828
+ .dshgt-icon-btn:hover:not(:disabled){background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-bg));border-color:var(--gt-border);}
829
+ .dshgt-icon-btn:disabled{opacity:.3;cursor:not-allowed;}
830
+ .dshgt-dateselect{appearance:none;height:28px;border:0;background:transparent;font-family:inherit;font-size:13px;color:var(--gt-fg);padding:0 8px;min-width:210px;max-width:320px;cursor:pointer;}
831
+ .dshgt-digest-sheet{max-width:900px;width:100%;border:1px solid var(--gt-border);border-radius:12px;background:var(--gt-card);overflow:hidden;}
832
+ .dshgt-digest-body{display:flex;flex-direction:column;min-width:0;}
833
+ .dshgt-digest-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 12px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);background:color-mix(in srgb,var(--gt-fg) 2.5%,var(--gt-card));}
834
+ .dshgt-seg{display:inline-flex;border:1px solid var(--gt-border);border-radius:8px;padding:2px;background:var(--gt-bg);gap:2px;}
835
+ .dshgt-seg button{border:0;background:none;height:26px;padding:0 11px;border-radius:6px;font-family:inherit;font-size:12.5px;color:var(--gt-muted);cursor:pointer;}
836
+ .dshgt-seg button.on{background:var(--gt-card);color:var(--gt-fg);font-weight:600;}
837
+ .dshgt-digest-doc{padding:26px 32px 22px;min-width:0;}
838
+ .dshgt-doc-meta{display:flex;gap:12px;flex-wrap:wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--gt-muted);padding-bottom:12px;border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
839
+ .dshgt-digest-raw{margin:0;padding:22px 28px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.75;white-space:pre-wrap;word-break:break-word;color:var(--gt-muted);background:color-mix(in srgb,var(--gt-fg) 2.5%,var(--gt-card));max-height:640px;overflow:auto;}
840
+ .dshgt-digest-loading{padding:40px 28px;color:var(--gt-muted);font-size:13px;}
841
+ .dshgt-digest-empty{padding:42px 36px 46px;max-width:720px;}
842
+ .dshgt-status-line{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--gt-muted);}
843
+ .dshgt-status-line b{color:var(--gt-fg);font-weight:600;}
844
+ .dshgt-digest-empty h2{margin:14px 0 6px;font-size:20px;font-weight:650;letter-spacing:-.012em;}
845
+ .dshgt-digest-empty p{margin:0;max-width:580px;font-size:13.5px;line-height:1.7;color:var(--gt-muted);}
846
+ .dshgt-digest-error{margin-top:10px;color:var(--gt-err);}
847
+ .dshgt-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px;}
848
+ .dshgt-md{font-size:14px;line-height:1.7;color:var(--gt-fg);word-break:break-word;}
849
+ .dshgt-md-h{margin:20px 0 8px;font-weight:650;line-height:1.35;letter-spacing:-.01em;}
850
+ .dshgt-md-h1{font-size:24px;margin-top:0;}
851
+ .dshgt-md-h2{font-size:18px;}
852
+ .dshgt-md-h3{font-size:15.5px;}
853
+ .dshgt-md-h4,.dshgt-md-h5,.dshgt-md-h6{font-size:14px;}
854
+ .dshgt-md-p{margin:8px 0;color:color-mix(in srgb,var(--gt-fg) 84%,var(--gt-muted));}
855
+ .dshgt-md-list{margin:8px 0;padding-left:22px;}
856
+ .dshgt-md-list li{margin:3px 0;}
857
+ .dshgt-md-quote{margin:12px 0;padding:2px 0 2px 14px;border-left:3px solid var(--gt-border);color:var(--gt-muted);}
858
+ .dshgt-md-pre{margin:12px 0;padding:12px 14px;border-radius:8px;background:color-mix(in srgb,var(--gt-fg) 5%,var(--gt-card));overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;}
859
+ .dshgt-md-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;background:color-mix(in srgb,var(--gt-fg) 7%,var(--gt-card));padding:1px 4px;border-radius:4px;}
860
+ .dshgt-md-hr{border:0;border-top:1px solid var(--gt-border);margin:18px 0;}
861
+ .dshgt-md a{color:var(--gt-accent);text-decoration:none;}
862
+ .dshgt-md a:hover{text-decoration:underline;}
863
+
864
+ /* ——— 运行记录 ——— */
865
+ .dshgt-runs-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
866
+ .dshgt-runs-filter{display:flex;gap:4px;margin-left:auto;}
867
+ .dshgt-run-list{border-top:1px solid var(--gt-border);}
868
+ .dshgt-run-item{border-bottom:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
869
+ .dshgt-run-row{display:grid;grid-template-columns:18px 132px minmax(0,1fr) 80px 82px 20px;align-items:center;gap:12px;min-height:52px;width:100%;padding:0 6px;border:0;background:none;text-align:left;font-family:inherit;color:var(--gt-fg);cursor:pointer;border-radius:6px;}
870
+ .dshgt-run-row:hover{background:color-mix(in srgb,var(--gt-fg) 4%,var(--gt-bg));}
871
+ .dshgt-run-date{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-muted);white-space:nowrap;}
872
+ .dshgt-run-what{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;}
873
+ .dshgt-run-muted{color:var(--gt-muted);font-size:12px;margin-left:6px;}
874
+ .dshgt-run-files,.dshgt-run-duration{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--gt-muted);text-align:right;}
875
+ .dshgt-run-chev{color:var(--gt-muted);text-align:center;transition:transform .15s;}
876
+ .dshgt-run-item.open .dshgt-run-chev{transform:rotate(90deg);}
877
+ .dshgt-run-detail{padding:2px 6px 16px 30px;display:flex;flex-direction:column;gap:10px;}
878
+ .dshgt-detail-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 22px;padding:12px 0;border-top:1px solid color-mix(in srgb,var(--gt-border) 70%,transparent);}
879
+ .dshgt-detail-k{font-size:11.5px;color:var(--gt-muted);}
880
+ .dshgt-detail-v{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all;}
881
+ .dshgt-detail-v a{color:var(--gt-accent);text-decoration:none;}
882
+ .dshgt-detail-v a:hover{text-decoration:underline;}
883
+ .dshgt-run-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
884
+
885
+ /* ——— 日志抽屉 / 关于 ——— */
886
+ .dshgt-log-modal{max-width:820px;width:calc(100% - 32px);}
887
+ .dshgt-log-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
888
+ .dshgt-log-title{margin:0;font-size:15px;}
889
+ .dshgt-log-head .dshgt-btn{margin-left:auto;}
890
+ .dshgt-log-pre{margin:0;max-height:52vh;overflow:auto;padding:12px;border-radius:8px;background:color-mix(in srgb,var(--gt-fg) 5%,var(--gt-card));font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;}
891
+ .dshgt-log-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
892
+ .dshgt-about{max-width:520px;}
893
+ .dshgt-about-head{display:flex;align-items:center;gap:10px;}
894
+ .dshgt-about-title{margin:0;font-size:16px;}
895
+ .dshgt-about-head .dshgt-btn{margin-left:auto;}
896
+ .dshgt-about-pkg{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;text-align:left;border:1px solid var(--gt-border);background:var(--gt-card);border-radius:8px;padding:9px 10px;cursor:pointer;font-family:inherit;color:inherit;}
897
+ .dshgt-about-pkg:hover{border-color:color-mix(in srgb,var(--gt-accent) 40%,var(--gt-border));}
898
+ .dshgt-about-pkg .dshgt-mono{font-size:12px;word-break:break-all;}
899
+ .dshgt-about .dshgt-kv{grid-template-columns:110px 1fr;}
900
+ .dshgt-about .dshgt-kv a{color:var(--gt-accent);text-decoration:none;word-break:break-all;}
901
+ .dshgt-about .dshgt-kv a:hover{text-decoration:underline;}
902
+
903
+ @media (max-width:1100px){
904
+ .dshgt-app{grid-template-columns:190px minmax(0,1fr);}
905
+ .dshgt-page-head{padding:18px 22px 12px;}
906
+ .dshgt-view{padding:18px 22px 30px;}
907
+ .dshgt-facts{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:10px;}
908
+ .dshgt-fact:nth-child(3){padding-left:0;border-left:0;}
909
+ .dshgt-week{flex-wrap:wrap;}
910
+ .dshgt-day{flex:1 1 88px;}
911
+ .dshgt-run-row{grid-template-columns:18px 112px minmax(0,1fr) 70px 20px;}
912
+ .dshgt-run-files{display:none;}
913
+ .dshgt-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr));}
914
+ .dshgt-dateselect{min-width:180px;}
915
+ }
916
+ @media (max-width:980px){
917
+ .dshgt-app{grid-template-columns:172px minmax(0,1fr);}
918
+ .dshgt-page-head h1{font-size:19px;}
919
+ .dshgt-dek{font-size:12.5px;}
920
+ .dshgt-main-body > .dshgt-settings{padding:14px 18px 18px;}
921
+ }
480
922
  `;
481
923
  function injectStyles() {
482
924
  if (typeof document === "undefined") return () => void 0;
@@ -517,9 +959,6 @@ function Row(props) {
517
959
  props.children
518
960
  );
519
961
  }
520
- function Inline(props) {
521
- return h("div", { className: "dshgt-inline" }, props.children);
522
- }
523
962
  function Input(props) {
524
963
  const { value, onInput, placeholder, mono, width, password, ...rest } = props;
525
964
  return h("input", {
@@ -632,6 +1071,21 @@ function localTime(iso) {
632
1071
  if (Number.isNaN(date.getTime())) return "—";
633
1072
  return date.toLocaleString();
634
1073
  }
1074
+ async function copyText(value) {
1075
+ try {
1076
+ if (typeof navigator === "undefined" || navigator.clipboard === void 0) return false;
1077
+ await navigator.clipboard.writeText(value);
1078
+ return true;
1079
+ } catch {
1080
+ return false;
1081
+ }
1082
+ }
1083
+ function shortTime(iso) {
1084
+ if (iso === null || iso === void 0) return "—";
1085
+ const date = new Date(iso);
1086
+ if (Number.isNaN(date.getTime())) return "—";
1087
+ return date.toLocaleString(void 0, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1088
+ }
635
1089
 
636
1090
  // client/api.ts
637
1091
  var API = "/api/morehao/github-trending";
@@ -657,6 +1111,12 @@ async function apiCall(method, path, body, options = {}) {
657
1111
  }
658
1112
  return json;
659
1113
  }
1114
+ function getPreview(runId, signal) {
1115
+ return apiCall("GET", `/preview?id=${encodeURIComponent(runId)}`, void 0, signal === void 0 ? {} : { signal });
1116
+ }
1117
+ function getLog(runId, signal) {
1118
+ return apiCall("GET", `/log?id=${encodeURIComponent(runId)}`, void 0, signal === void 0 ? {} : { signal });
1119
+ }
660
1120
 
661
1121
  // client/store.ts
662
1122
  var POLL_MS = 5e3;
@@ -691,6 +1151,29 @@ function useStore() {
691
1151
  return { state, error, loading, refresh };
692
1152
  }
693
1153
 
1154
+ // client/credential.ts
1155
+ function credentialView(model, t) {
1156
+ if (model.mode === "proxy") {
1157
+ return { ok: true, kind: "ok", label: t("auth.hostProxy"), hint: t("model.credential.proxyHint") };
1158
+ }
1159
+ const credential = model.credential;
1160
+ if (credential.configured) {
1161
+ return {
1162
+ ok: true,
1163
+ kind: "ok",
1164
+ label: `${t("auth.configured")}${credential.source === null ? "" : ` · ${credential.source}`}`,
1165
+ hint: t("model.credential.directHint")
1166
+ };
1167
+ }
1168
+ const missing = t("auth.missing");
1169
+ return {
1170
+ ok: false,
1171
+ kind: "err",
1172
+ label: credential.ref.trim() === "" ? missing : `${missing} · ${credential.ref}`,
1173
+ hint: t("model.credential.missingHint")
1174
+ };
1175
+ }
1176
+
694
1177
  // client/settings.ts
695
1178
  var LANGUAGES = ["all", "typescript", "python", "go", "rust", "java", "c", "cpp", "javascript", "csharp"];
696
1179
  var SECTIONS = ["source", "model", "repo", "auth", "scope", "schedule"];
@@ -736,13 +1219,13 @@ function SettingsView(props) {
736
1219
  const [rechecking, setRechecking] = import_react.default.useState(false);
737
1220
  const [downloading, setDownloading] = import_react.default.useState(false);
738
1221
  const [quickOpen, setQuickOpen] = import_react.default.useState(false);
739
- const [advanced, setAdvanced] = import_react.default.useState(false);
740
1222
  const [section, setSection] = import_react.default.useState("source");
741
1223
  const [branchInfo, setBranchInfo] = import_react.default.useState(null);
742
1224
  const [branchLoading, setBranchLoading] = import_react.default.useState(false);
743
1225
  const [testing, setTesting] = import_react.default.useState(false);
744
1226
  const [githubTest, setGithubTest] = import_react.default.useState(null);
745
1227
  const anyDirty = SECTIONS.some((id) => dirtySections[id]);
1228
+ const sectionEditable = section !== "source";
746
1229
  import_react.default.useEffect(() => {
747
1230
  if (state !== null && !anyDirty) setDraft(state.settings);
748
1231
  }, [state, anyDirty]);
@@ -940,11 +1423,9 @@ function SettingsView(props) {
940
1423
  state.core.release === void 0 ? null : h("p", { className: "dshgt-hint dshgt-mono", style: { fontSize: 11 } }, `${state.core.release.url} → ${state.core.release.binaryPath}`),
941
1424
  state.core.state === "ready" ? null : h(Alert, { kind: "err", title: t("core.label") }, h("div", null, state.core.message ?? ""), h("div", { className: "dshgt-hint" }, t("core.order"))),
942
1425
  h("p", { className: "dshgt-hint" }, t("core.order")),
943
- state.coexistence.found ? h(Alert, { kind: "warn", title: t("coexist.title") }, h("div", { className: "dshgt-mono", style: { fontSize: 12 } }, `${state.coexistence.path ?? ""} ${state.coexistence.version ?? ""}`), h("div", null, state.coexistence.message ?? ""), h("div", { className: "dshgt-hint" }, t("coexist.hint"))) : null,
944
- advanced ? h(Row, { label: "TRENDING_DIGEST_CORE" }, h(Input, { mono: true, width: 420, value: draft.corePath, placeholder: "/path/to/trendradar", onInput: (value) => update((next) => {
945
- next.corePath = value;
946
- }) }), h("span", { className: "dshgt-hint" }, t("core.path.advancedHint"))) : null
1426
+ state.coexistence.found ? h(Alert, { kind: "warn", title: t("coexist.title") }, h("div", { className: "dshgt-mono", style: { fontSize: 12 } }, `${state.coexistence.path ?? ""} ${state.coexistence.version ?? ""}`), h("div", null, state.coexistence.message ?? ""), h("div", { className: "dshgt-hint" }, t("coexist.hint"))) : null
947
1427
  );
1428
+ const modelCredential = credentialView({ mode: draft.model.mode, credential: state.model.credential }, t);
948
1429
  const modelCard = h(
949
1430
  Card,
950
1431
  {
@@ -963,7 +1444,7 @@ function SettingsView(props) {
963
1444
  next.model.provider = value;
964
1445
  })
965
1446
  }),
966
- h("span", { className: "dshgt-hint" }, state.model.mode === "proxy" ? t("model.mode.proxy") : t("model.mode.direct"))
1447
+ h("span", { className: "dshgt-hint" }, draft.model.mode === "proxy" ? t("model.mode.proxy") : t("model.mode.direct"))
967
1448
  ),
968
1449
  h(Row, { label: t("model.baseURL") }, h(Input, {
969
1450
  mono: true,
@@ -988,22 +1469,11 @@ function SettingsView(props) {
988
1469
  }),
989
1470
  h(Btn, { small: true, onClick: () => void runPreflight(), disabled: preflighting }, preflighting ? t("model.preflighting") : t("model.preflight"))
990
1471
  ),
991
- advanced ? h(
992
- Row,
993
- { label: t("model.keyRef") },
994
- h(Input, { mono: true, width: 220, value: draft.model.apiKeyRef, onInput: (value) => update((next) => {
995
- next.model.apiKeyRef = value;
996
- }) }),
997
- h("span", { className: "dshgt-hint" }, t("model.keyRef.advancedHint"))
998
- ) : h(
1472
+ h(
999
1473
  Row,
1000
1474
  { label: t("model.credential") },
1001
- h(
1002
- Pill,
1003
- { kind: state.model.credential.configured ? "ok" : "err" },
1004
- state.model.credential.configured ? `${t("auth.configured")}${state.model.credential.source === null ? "" : ` · ${state.model.credential.source}`}` : t("auth.missing")
1005
- ),
1006
- h("span", { className: "dshgt-hint" }, t("model.credential.hint"))
1475
+ h(Pill, { kind: modelCredential.kind }, modelCredential.label),
1476
+ h("span", { className: "dshgt-hint" }, modelCredential.hint)
1007
1477
  ),
1008
1478
  h(
1009
1479
  Row,
@@ -1199,7 +1669,7 @@ function SettingsView(props) {
1199
1669
  h(
1200
1670
  "div",
1201
1671
  { className: "dshgt-footer" },
1202
- h(
1672
+ sectionEditable ? h(
1203
1673
  Btn,
1204
1674
  {
1205
1675
  variant: "primary",
@@ -1208,11 +1678,10 @@ function SettingsView(props) {
1208
1678
  title: t("settings.saveSection.hint")
1209
1679
  },
1210
1680
  savingSection === section ? t("settings.saving") : dirtySections[section] ? t("settings.saveSection") : savedSection === section ? t("settings.saved") : t("settings.saveSection")
1211
- ),
1212
- h("span", { className: "dshgt-hint" }, t("settings.saveSection.hint")),
1681
+ ) : null,
1682
+ sectionEditable ? h("span", { className: "dshgt-hint" }, t("settings.saveSection.hint")) : null,
1213
1683
  h(Btn, { onClick: () => void importCli() }, t("settings.import")),
1214
- h(Btn, { onClick: copyConfig }, t("settings.copyConfig")),
1215
- h(Btn, { onClick: () => setAdvanced(!advanced) }, advanced ? t("settings.advanced.hide") : t("settings.advanced"))
1684
+ h(Btn, { onClick: copyConfig }, t("settings.copyConfig"))
1216
1685
  )
1217
1686
  ),
1218
1687
  quickOpen ? h(QuickFillModal, { t, onClose: () => setQuickOpen(false), onApplied: (settings) => {
@@ -1346,166 +1815,1254 @@ function QuickFillModal(props) {
1346
1815
  );
1347
1816
  }
1348
1817
 
1349
- // client/runs.ts
1350
- var STEP_KEYS = ["resolve", "handshake", "spawn", "fetch", "summarize", "render", "commit", "pr", "state"];
1351
- var CORE_STEPS = /* @__PURE__ */ new Set(["fetch", "summarize", "render", "commit", "pr"]);
1352
- function RunsView(props) {
1353
- const { t, store } = props;
1354
- const [tab, setTab] = import_react.default.useState("now");
1355
- const [dryRun, setDryRun] = import_react.default.useState(false);
1818
+ // client/error-copy.ts
1819
+ function reasonHint(t, reason) {
1820
+ if (reason === void 0 || reason === "") return null;
1821
+ const key = `err.reason.${reason}`;
1822
+ const text = t(key);
1823
+ return text === key ? null : text;
1824
+ }
1825
+ function skipHint(t, reason, fallback) {
1826
+ if (reason === void 0 || reason === "") return fallback;
1827
+ const key = `skip.${reason}`;
1828
+ const text = t(key);
1829
+ return text === key ? fallback : text;
1830
+ }
1831
+
1832
+ // client/run-controller.ts
1833
+ function useRunController(store) {
1834
+ const { refresh } = store;
1356
1835
  const [busy, setBusy] = import_react.default.useState(false);
1357
- const [summary, setSummary] = import_react.default.useState(null);
1358
1836
  const [error, setError] = import_react.default.useState(null);
1359
- const state = store.state;
1360
- const run = async () => {
1361
- setBusy(true);
1837
+ const [summary, setSummary] = import_react.default.useState(null);
1838
+ const [dryRun, setDryRun] = import_react.default.useState(false);
1839
+ const [localStartedAt, setLocalStartedAt] = import_react.default.useState(null);
1840
+ const run = import_react.default.useCallback(
1841
+ async (overrideDry) => {
1842
+ const dry = overrideDry ?? dryRun;
1843
+ setBusy(true);
1844
+ setError(null);
1845
+ setSummary(null);
1846
+ setLocalStartedAt((/* @__PURE__ */ new Date()).toISOString());
1847
+ try {
1848
+ const result = await apiCall("POST", "/run", { dryRun: dry });
1849
+ setSummary(result.summary);
1850
+ await refresh({ quiet: true });
1851
+ } catch (failure) {
1852
+ setError(failure instanceof Error ? failure.message : String(failure));
1853
+ } finally {
1854
+ setBusy(false);
1855
+ setLocalStartedAt(null);
1856
+ }
1857
+ },
1858
+ [dryRun, refresh]
1859
+ );
1860
+ const cancel = import_react.default.useCallback(async () => {
1362
1861
  setError(null);
1363
- setSummary(null);
1364
- try {
1365
- const result = await apiCall("POST", "/run", { dryRun });
1366
- setSummary(result.summary);
1367
- await store.refresh({ quiet: true });
1368
- } catch (failure) {
1369
- setError(failure instanceof Error ? failure.message : String(failure));
1370
- } finally {
1371
- setBusy(false);
1372
- }
1373
- };
1374
- const cancel = async () => {
1375
1862
  try {
1376
1863
  await apiCall("POST", "/run/cancel", {});
1377
1864
  } catch (failure) {
1378
1865
  setError(failure instanceof Error ? failure.message : String(failure));
1866
+ } finally {
1867
+ await refresh({ quiet: true });
1379
1868
  }
1869
+ }, [refresh]);
1870
+ const serverRunning = store.state?.status.running ?? null;
1871
+ return {
1872
+ running: serverRunning !== null || busy,
1873
+ busy,
1874
+ error,
1875
+ summary,
1876
+ startedAt: serverRunning?.startedAt ?? localStartedAt,
1877
+ dryRun,
1878
+ setDryRun,
1879
+ run,
1880
+ cancel,
1881
+ clearError: () => setError(null)
1380
1882
  };
1381
- if (state === null) {
1382
- return h("div", { className: "dshgt-root" }, h("p", { className: "dshgt-hint" }, store.error ?? t("quickfill.loading")));
1383
- }
1384
- const running = state.status.running !== null;
1385
- const pendingPr = state.status.pendingPr;
1386
- const shown = summary;
1387
- const lastRecord = state.status.history[0] === void 0 ? null : state.status.history[0];
1388
- const last = shown === null ? lastRecord : { ...lastRecord, ...shown, at: (/* @__PURE__ */ new Date()).toISOString(), model: { provider: state.settings.model.provider, model: state.settings.model.model } };
1389
- const failedStep = shown !== null && !shown.ok ? stepFromError(shown.error?.area) : null;
1883
+ }
1884
+ function useElapsed(startedAt, active) {
1885
+ const [now, setNow] = import_react.default.useState(() => Date.now());
1886
+ import_react.default.useEffect(() => {
1887
+ if (!active) return () => void 0;
1888
+ setNow(Date.now());
1889
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
1890
+ return () => clearInterval(timer);
1891
+ }, [active]);
1892
+ if (startedAt === null || startedAt === void 0) return 0;
1893
+ const started = new Date(startedAt).getTime();
1894
+ if (Number.isNaN(started)) return 0;
1895
+ return Math.max(0, now - started);
1896
+ }
1897
+ function formatElapsed(ms) {
1898
+ const total = Math.floor(ms / 1e3);
1899
+ const minutes = Math.floor(total / 60);
1900
+ const seconds = total % 60;
1901
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
1902
+ }
1903
+
1904
+ // client/live-run.ts
1905
+ function LiveRun(props) {
1906
+ const { t, controller } = props;
1907
+ const elapsed = useElapsed(props.startedAt, true);
1390
1908
  return h(
1391
1909
  "div",
1392
- { className: "dshgt-root" },
1910
+ { className: "dshgt-live" },
1393
1911
  h(
1394
1912
  "div",
1395
- { className: "dshgt-head" },
1396
- h("span", { className: "dshgt-title" }, t("section.title")),
1397
- h("span", { className: "dshgt-sub" }, t("card.source.hint")),
1398
- h("span", { style: { marginLeft: "auto" } }, h(Pill, { kind: running || busy ? "warn" : "ok" }, running || busy ? t("run.running") : t("run.idle")))
1913
+ { className: "dshgt-live-top" },
1914
+ h("span", { className: "dshgt-dot warn" }),
1915
+ h("b", null, t("overview.live.title")),
1916
+ h("span", { className: "dshgt-spinner", "aria-hidden": "true" }),
1917
+ h("span", { className: "dshgt-hint" }, t("overview.live.core")),
1918
+ h("span", { className: "dshgt-live-elapsed" }, formatElapsed(elapsed))
1399
1919
  ),
1400
1920
  h(
1401
1921
  "div",
1402
- { className: "dshgt-tabs", role: "tablist" },
1403
- h("button", { type: "button", className: "dshgt-tab", role: "tab", "aria-selected": tab === "now" ? "true" : "false", onClick: () => setTab("now") }, t("run.now")),
1404
- h("button", { type: "button", className: "dshgt-tab", role: "tab", "aria-selected": tab === "history" ? "true" : "false", onClick: () => setTab("history") }, t("run.history"))
1922
+ { className: "dshgt-live-note" },
1923
+ h("span", null, t("overview.live.note")),
1924
+ props.startedAt === null ? null : h("span", { className: "dshgt-hint" }, `${t("overview.live.started")} ${localTime(props.startedAt)}`)
1405
1925
  ),
1406
- tab === "now" ? h(
1926
+ h(
1407
1927
  "div",
1408
- { style: { display: "flex", flexDirection: "column", gap: 12 } },
1928
+ { className: "dshgt-live-actions" },
1929
+ h(Btn, { small: true, variant: "danger", onClick: () => void controller.cancel() }, t("run.cancel"))
1930
+ )
1931
+ );
1932
+ }
1933
+
1934
+ // client/digest-core.ts
1935
+ var DIGEST_WINDOW_DAYS = 30;
1936
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
1937
+ function isValidDate(value) {
1938
+ if (typeof value !== "string") return false;
1939
+ const match = DATE_RE.exec(value);
1940
+ if (match === null) return false;
1941
+ const year = Number(match[1]);
1942
+ const month = Number(match[2]);
1943
+ const day = Number(match[3]);
1944
+ const date = new Date(Date.UTC(year, month - 1, day));
1945
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
1946
+ }
1947
+ function zonedDateString(date, timeZone) {
1948
+ try {
1949
+ return new Intl.DateTimeFormat("en-CA", {
1950
+ timeZone,
1951
+ year: "numeric",
1952
+ month: "2-digit",
1953
+ day: "2-digit"
1954
+ }).format(date);
1955
+ } catch {
1956
+ const year = date.getFullYear();
1957
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1958
+ const day = String(date.getDate()).padStart(2, "0");
1959
+ return `${year}-${month}-${day}`;
1960
+ }
1961
+ }
1962
+ function addDays(date, days) {
1963
+ const match = DATE_RE.exec(date);
1964
+ if (match === null) return date;
1965
+ const value = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
1966
+ value.setUTCDate(value.getUTCDate() + days);
1967
+ return value.toISOString().slice(0, 10);
1968
+ }
1969
+ function recordDate(record, timeZone) {
1970
+ if (isValidDate(record.date)) return { date: record.date, derivedFromAt: false };
1971
+ const at = new Date(record.at);
1972
+ if (Number.isNaN(at.getTime())) return void 0;
1973
+ return { date: zonedDateString(at, timeZone), derivedFromAt: true };
1974
+ }
1975
+ function digestStatus(record) {
1976
+ if (!record.ok) return "failed";
1977
+ if (record.skipped) return "skipped";
1978
+ if (record.kind === "dry-run") return "dry";
1979
+ if (record.preview !== void 0) return "preview";
1980
+ if (record.legacy === true) return "legacy";
1981
+ return "missing";
1982
+ }
1983
+ function buildDigestDates(records, options) {
1984
+ const now = options.now ?? /* @__PURE__ */ new Date();
1985
+ const today = zonedDateString(now, options.timeZone);
1986
+ const earliest = addDays(today, -(DIGEST_WINDOW_DAYS - 1));
1987
+ const groups = /* @__PURE__ */ new Map();
1988
+ for (const record of records) {
1989
+ const info = recordDate(record, options.timeZone);
1990
+ if (info === void 0) continue;
1991
+ if (info.date < earliest || info.date > today) continue;
1992
+ const group = groups.get(info.date);
1993
+ if (group === void 0) groups.set(info.date, { records: [record], derivedFromAt: info.derivedFromAt });
1994
+ else {
1995
+ group.records.push(record);
1996
+ group.derivedFromAt = group.derivedFromAt || info.derivedFromAt;
1997
+ }
1998
+ }
1999
+ const days = [];
2000
+ for (const [date, group] of groups) {
2001
+ const sorted = [...group.records].sort((a, b) => b.at.localeCompare(a.at));
2002
+ const preferred = sorted.find(
2003
+ (record2) => record2.preview !== void 0 && record2.ok && record2.kind !== "dry-run" && !record2.skipped
2004
+ );
2005
+ const record = preferred ?? sorted[0];
2006
+ if (record === void 0) continue;
2007
+ const info = recordDate(record, options.timeZone);
2008
+ const legacy = record.legacy === true || info?.derivedFromAt === true || group.derivedFromAt;
2009
+ days.push({ date, record, status: digestStatus(record), legacy });
2010
+ }
2011
+ days.sort((a, b) => b.date.localeCompare(a.date));
2012
+ return days;
2013
+ }
2014
+ function recentDates(today, count) {
2015
+ const dates = [];
2016
+ for (let offset = count - 1; offset >= 0; offset -= 1) dates.push(addDays(today, -offset));
2017
+ return dates;
2018
+ }
2019
+ function formatDayLabel(date, lang) {
2020
+ let weekday = "";
2021
+ try {
2022
+ weekday = new Intl.DateTimeFormat(lang === "en" ? "en-US" : "zh-CN", {
2023
+ weekday: "short",
2024
+ timeZone: "UTC"
2025
+ }).format(/* @__PURE__ */ new Date(`${date}T12:00:00Z`));
2026
+ } catch {
2027
+ weekday = "";
2028
+ }
2029
+ return weekday === "" ? date : `${date} ${weekday}`;
2030
+ }
2031
+ function formatBytes(bytes) {
2032
+ if (!Number.isFinite(bytes) || bytes < 0) return "—";
2033
+ if (bytes < 1024) return `${bytes} B`;
2034
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2035
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2036
+ }
2037
+
2038
+ // client/overview.ts
2039
+ function statusPill(t, record) {
2040
+ if (!record.ok) return h(Pill, { kind: "err" }, t("state.failed"));
2041
+ if (record.skipped) return h(Pill, { kind: "warn" }, t("state.skipped"));
2042
+ if (record.kind === "dry-run") return h(Pill, { kind: "warn" }, t("state.dry"));
2043
+ return h(Pill, { kind: "ok" }, t("state.ok"));
2044
+ }
2045
+ var STATUS_LAMP = {
2046
+ preview: "ok",
2047
+ failed: "err",
2048
+ skipped: "warn",
2049
+ dry: "warn",
2050
+ legacy: "warn",
2051
+ missing: "warn"
2052
+ };
2053
+ function dayLabel(t, day) {
2054
+ switch (day.status) {
2055
+ case "preview":
2056
+ return duration(day.record.durationMs);
2057
+ case "failed":
2058
+ return t("state.failed");
2059
+ case "skipped":
2060
+ return t("state.skipped");
2061
+ case "dry":
2062
+ return t("state.dry");
2063
+ case "legacy":
2064
+ return t("digest.tag.legacy");
2065
+ case "missing":
2066
+ return t("digest.tag.missing");
2067
+ }
2068
+ }
2069
+ function prLabel(t, state, record) {
2070
+ if (record === null || record.prUrl === void 0) {
2071
+ if (record !== null && record.ok && !record.skipped && record.kind !== "dry-run" && state.settings.repo.commitMode === "direct") {
2072
+ return { label: t("overview.last.direct"), kind: "idle" };
2073
+ }
2074
+ return null;
2075
+ }
2076
+ const pending = state.status.pendingPr;
2077
+ const matches = pending !== null && pending.url === record.prUrl;
2078
+ if (matches && state.status.pendingPrState === "unknown") {
2079
+ return { label: t("overview.last.prUnknown"), url: record.prUrl, kind: "warn" };
2080
+ }
2081
+ if (matches) return { label: t("overview.last.prPending"), url: record.prUrl, kind: "warn" };
2082
+ return { label: t("overview.last.prCreated"), url: record.prUrl, kind: "ok" };
2083
+ }
2084
+ function readyRows(t, state) {
2085
+ const core = state.core;
2086
+ const credential = credentialView(state.model, t);
2087
+ const repo = state.settings.repo;
2088
+ return [
2089
+ {
2090
+ ok: core.state === "ready",
2091
+ label: t("overview.ready.core"),
2092
+ value: core.state === "ready" ? `${core.version ?? "—"} · ${t("core.ready")}` : t(`core.${core.state}`),
2093
+ ...core.ipcSchema === void 0 ? {} : { extra: `ipc ${core.ipcSchema} · digest ${core.digestSchema ?? "—"}` }
2094
+ },
2095
+ {
2096
+ ok: credential.ok,
2097
+ label: t("overview.ready.model"),
2098
+ value: `${state.settings.model.provider} / ${state.settings.model.model || "—"}`,
2099
+ extra: credential.label
2100
+ },
2101
+ {
2102
+ ok: repo.path.trim() !== "",
2103
+ label: t("overview.ready.repo"),
2104
+ value: repo.path.trim() === "" ? t("repo.path.empty") : repo.path,
2105
+ extra: repo.commitMode === "direct" ? t("repo.commitMode.direct") : t("repo.commitMode.branch")
2106
+ },
2107
+ {
2108
+ ok: state.github.configured,
2109
+ label: t("overview.ready.github"),
2110
+ value: state.github.configured ? t("auth.configured") : t("auth.missing"),
2111
+ extra: `${state.github.authMode}${state.github.source === "" ? "" : ` · ${state.github.source}`}`
2112
+ }
2113
+ ];
2114
+ }
2115
+ function OverviewView(props) {
2116
+ const { t, lang, store, controller } = props;
2117
+ const state = store.state;
2118
+ if (state === null) {
2119
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2120
+ }
2121
+ const last = state.status.lastResult ?? state.status.history[0] ?? null;
2122
+ const pr = prLabel(t, state, last);
2123
+ const rows = readyRows(t, state);
2124
+ const readyCount = rows.filter((row) => row.ok).length;
2125
+ const timeZone = state.schedule.timezone;
2126
+ const today = zonedDateString(/* @__PURE__ */ new Date(), timeZone);
2127
+ const days = buildDigestDates(state.status.history, { timeZone });
2128
+ const byDate = new Map(days.map((day) => [day.date, day]));
2129
+ const week = recentDates(today, 7);
2130
+ return h(
2131
+ "div",
2132
+ { className: "dshgt-view dshgt-overview" },
2133
+ controller.running ? h(LiveRun, { t, controller, startedAt: controller.startedAt }) : null,
2134
+ controller.error === null ? null : h("div", { className: "dshgt-alert err" }, controller.error),
2135
+ last === null ? h(
2136
+ "section",
2137
+ { className: "dshgt-delivery" },
2138
+ h("div", { className: "dshgt-delivery-top" }, h("span", { className: "dshgt-hint" }, t("overview.last.none")))
2139
+ ) : h(
2140
+ "section",
2141
+ { className: "dshgt-delivery" },
1409
2142
  h(
1410
- Card,
1411
- { title: t("run.now"), hint: "spawn → 单次运行 → 退出" },
2143
+ "div",
2144
+ { className: "dshgt-delivery-top" },
2145
+ statusPill(t, last),
2146
+ h("span", { className: "dshgt-hint" }, last.date ?? localTime(last.at)),
1412
2147
  h(
1413
- Inline,
1414
- null,
1415
- h(Btn, { variant: "primary", onClick: () => void run(), disabled: busy || running }, t("run.button")),
1416
- h(Toggle, { checked: dryRun, label: t("run.dryRun"), onChange: setDryRun }),
1417
- running || busy ? h(Btn, { variant: "danger", onClick: () => void cancel() }, t("run.cancel")) : null
2148
+ "span",
2149
+ { className: "dshgt-pill-mini" },
2150
+ `${t("overview.last.files", { count: last.files })} · ${duration(last.durationMs)}`
1418
2151
  ),
1419
- pendingPr === null ? null : h(
1420
- Alert,
1421
- { kind: "warn", title: t("run.pendingPr") },
1422
- h("div", null, h("a", { href: pendingPr.url, target: "_blank", rel: "noreferrer" }, pendingPr.url)),
1423
- h("div", { className: "dshgt-hint" }, t("run.pendingPr.hint"))
2152
+ last.legacy === true ? h("span", { className: "dshgt-tag" }, t("runs.legacy")) : null
2153
+ ),
2154
+ h("h2", null, t("overview.last.digest", { date: last.date ?? localTime(last.at) })),
2155
+ h(
2156
+ "div",
2157
+ { className: "dshgt-when" },
2158
+ last.branch === void 0 ? null : h("span", null, `${t("run.usage.branch")} `, h("span", { className: "dshgt-mono" }, last.branch)),
2159
+ last.commit === void 0 ? null : h("span", null, `${t("run.usage.commit")} `, h("span", { className: "dshgt-mono" }, last.commit)),
2160
+ pr === null ? null : pr.url === void 0 ? h("span", { className: "dshgt-hint" }, pr.label) : h("a", { href: pr.url, target: "_blank", rel: "noreferrer" }, pr.label)
2161
+ ),
2162
+ last.ok ? null : h(
2163
+ "div",
2164
+ { className: "dshgt-alert err dshgt-delivery-error" },
2165
+ h("b", null, `${t("error.prefix")} ${last.errorCode ?? "INTERNAL"}`),
2166
+ h("div", null, last.errorMessage ?? t("state.failed")),
2167
+ last.errorReason === void 0 ? null : h("div", { className: "dshgt-hint" }, reasonHint(t, last.errorReason) ?? "")
2168
+ ),
2169
+ h(
2170
+ "div",
2171
+ { className: "dshgt-artifacts" },
2172
+ last.preview === void 0 ? h("span", { className: "dshgt-hint" }, t("overview.last.noPreview")) : h(
2173
+ "button",
2174
+ {
2175
+ type: "button",
2176
+ className: "dshgt-filechip",
2177
+ onClick: () => props.onOpenDigest(last.date)
2178
+ },
2179
+ h("span", { className: "dshgt-mono" }, last.preview.file)
1424
2180
  ),
1425
- error === null ? null : h(Alert, { kind: "err" }, error),
1426
- h(
1427
- "ul",
1428
- { className: "dshgt-steps" },
1429
- STEP_KEYS.map((key) => {
1430
- const stepState = running || busy ? CORE_STEPS.has(key) ? "pending" : key === "resolve" || key === "handshake" || key === "spawn" ? "active" : "pending" : shown === null ? "pending" : failedStep === key ? "failed" : "done";
1431
- const label = { done: "✓", active: "…", failed: "✗", pending: "—" }[stepState];
1432
- return h("li", { key, className: "dshgt-step", "data-state": stepState }, h("span", null, t(`step.${key}`)), h("span", { className: "st" }, label));
1433
- })
2181
+ last.preview === void 0 ? null : h(Btn, { small: true, onClick: () => props.onOpenDigest(last.date) }, t("overview.last.read")),
2182
+ pr === null || pr.url === void 0 ? null : h(
2183
+ "a",
2184
+ { className: "dshgt-btn dshgt-btn-sm", href: pr.url, target: "_blank", rel: "noreferrer" },
2185
+ t("digest.openPr")
1434
2186
  )
1435
2187
  ),
1436
- last === null ? h(Card, { title: t("run.last.title") }, h("p", { className: "dshgt-hint" }, t("run.last.none"))) : h(
1437
- Card,
1438
- { title: t("run.last.title"), extra: h(Pill, { kind: last.ok ? last.skipped ? "warn" : "ok" : "err" }, last.skipped ? t("state.skipped") : last.ok ? t("state.ok") : t("state.failed")) },
1439
- h(KV, {
1440
- items: [
1441
- [t("col.date"), localTime(last.at)],
1442
- [t("run.usage.core"), last.coreVersion || "—"],
1443
- [t("run.usage.files"), String(last.files)],
1444
- [t("run.usage.duration"), duration(last.durationMs)],
1445
- [t("run.usage.hash"), h("span", { className: "dshgt-mono" }, shortHash(last.contentHash))],
1446
- [t("run.usage.branch"), h("span", { className: "dshgt-mono" }, last.branch ?? "—")],
1447
- [t("run.usage.pr"), last.prUrl === void 0 ? "—" : h("a", { href: last.prUrl, target: "_blank", rel: "noreferrer" }, last.prUrl)],
1448
- ["model", `${state.settings.model.provider} / ${state.settings.model.model || "—"}`]
1449
- ]
1450
- }),
1451
- last.warnings.length === 0 ? null : h("p", { className: "dshgt-hint" }, last.warnings.join(";"))
2188
+ h(
2189
+ "div",
2190
+ { className: "dshgt-facts" },
2191
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.commit")), h("div", { className: "dshgt-fact-v" }, last.commit === void 0 ? "—" : last.commit)),
2192
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.hash")), h("div", { className: "dshgt-fact-v" }, shortHash(last.contentHash))),
2193
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("run.usage.core")), h("div", { className: "dshgt-fact-v" }, last.coreVersion || "—")),
2194
+ h("div", { className: "dshgt-fact" }, h("div", { className: "dshgt-fact-k" }, t("overview.model")), h("div", { className: "dshgt-fact-v" }, last.model?.model ?? "—"))
2195
+ )
2196
+ ),
2197
+ h(
2198
+ "section",
2199
+ { className: "dshgt-block" },
2200
+ h(
2201
+ "div",
2202
+ { className: "dshgt-section-label" },
2203
+ t("overview.ready.title"),
2204
+ h("span", { className: "dshgt-mono dshgt-section-tail" }, `${readyCount} / ${rows.length}`)
2205
+ ),
2206
+ h(
2207
+ "div",
2208
+ { className: "dshgt-ready-list" },
2209
+ rows.map(
2210
+ (row) => h(
2211
+ "div",
2212
+ { className: "dshgt-ready-row", key: row.label },
2213
+ h("span", { className: `dshgt-lamp ${row.ok ? "ok" : "warn"}` }),
2214
+ h("span", { className: "dshgt-ready-label" }, row.label),
2215
+ h(
2216
+ "span",
2217
+ { className: "dshgt-ready-value" },
2218
+ row.value,
2219
+ row.extra === void 0 ? null : h("span", { className: "dshgt-ready-extra" }, `· ${row.extra}`)
2220
+ ),
2221
+ h("button", { type: "button", className: "dshgt-linkbtn", onClick: props.onOpenSettings }, t("overview.ready.view"))
2222
+ )
2223
+ )
2224
+ )
2225
+ ),
2226
+ h(
2227
+ "section",
2228
+ { className: "dshgt-block" },
2229
+ h(
2230
+ "div",
2231
+ { className: "dshgt-section-label" },
2232
+ t("overview.week.title"),
2233
+ h("button", { type: "button", className: "dshgt-linkbtn dshgt-section-tail", onClick: props.onOpenRuns }, t("overview.week.all"))
1452
2234
  ),
1453
- shown !== null && shown.error !== void 0 ? h(
1454
- Card,
1455
- { title: t("run.error.title") },
2235
+ h(
2236
+ "div",
2237
+ { className: "dshgt-week" },
2238
+ week.map((date) => {
2239
+ const day = byDate.get(date);
2240
+ if (day === void 0) {
2241
+ const waiting = date === today && state.schedule.enabled;
2242
+ return h(
2243
+ "div",
2244
+ { className: "dshgt-day", key: date },
2245
+ h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2246
+ h("div", { className: "dshgt-day-status" }, waiting ? t("overview.week.wait", { time: state.schedule.time }) : t("overview.week.none"))
2247
+ );
2248
+ }
2249
+ return h(
2250
+ "button",
2251
+ {
2252
+ type: "button",
2253
+ className: "dshgt-day dshgt-day-clickable",
2254
+ key: date,
2255
+ onClick: () => props.onOpenDigest(date),
2256
+ title: day.record.errorCode === void 0 ? formatDayLabel(date, lang) : `${formatDayLabel(date, lang)} · ${day.record.errorCode}`
2257
+ },
2258
+ h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2259
+ h(
2260
+ "div",
2261
+ { className: "dshgt-day-status" },
2262
+ h("span", { className: `dshgt-lamp ${STATUS_LAMP[day.status]}` }),
2263
+ dayLabel(t, day)
2264
+ )
2265
+ );
2266
+ })
2267
+ )
2268
+ )
2269
+ );
2270
+ }
2271
+
2272
+ // client/runs.ts
2273
+ var FILTERS = [
2274
+ { id: "all", key: "runs.filter.all" },
2275
+ { id: "failed", key: "runs.filter.failed" },
2276
+ { id: "dry", key: "runs.filter.dry" }
2277
+ ];
2278
+ function recordSummary(t, record) {
2279
+ if (!record.ok) {
2280
+ return [record.errorCode, record.errorMessage].filter((item) => item !== void 0 && item !== "").join(" · ") || t("state.failed");
2281
+ }
2282
+ if (record.skipped) return skipHint(t, record.skipReason, t("runs.noChange"));
2283
+ if (record.kind === "dry-run") return t("runs.dryHint");
2284
+ if (record.branch !== void 0) return record.branch;
2285
+ if (record.commit !== void 0) return record.commit;
2286
+ if (record.prUrl !== void 0) return t("runs.prCreated");
2287
+ return "—";
2288
+ }
2289
+ function prStatus(t, state, record) {
2290
+ if (record.prUrl === void 0) return null;
2291
+ const pending = state.status.pendingPr;
2292
+ if (pending !== null && pending.url === record.prUrl) {
2293
+ return { label: state.status.pendingPrState === "unknown" ? t("runs.prUnknown") : t("runs.pendingPr"), kind: "warn" };
2294
+ }
2295
+ return { label: t("runs.prCreated"), kind: "ok" };
2296
+ }
2297
+ function LogDrawer(props) {
2298
+ const { t, runId } = props;
2299
+ const [loading, setLoading] = import_react.default.useState(true);
2300
+ const [text, setText] = import_react.default.useState("");
2301
+ const [error, setError] = import_react.default.useState(null);
2302
+ const [copied, setCopied] = import_react.default.useState(false);
2303
+ import_react.default.useEffect(() => {
2304
+ const controller = new AbortController();
2305
+ setLoading(true);
2306
+ setError(null);
2307
+ setText("");
2308
+ getLog(runId, controller.signal).then((payload) => {
2309
+ if (controller.signal.aborted) return;
2310
+ setText(payload.text);
2311
+ setLoading(false);
2312
+ }).catch((failure) => {
2313
+ if (controller.signal.aborted) return;
2314
+ setError(failure instanceof Error ? failure.message : String(failure));
2315
+ setLoading(false);
2316
+ });
2317
+ return () => controller.abort();
2318
+ }, [runId]);
2319
+ import_react.default.useEffect(() => {
2320
+ const onKey = (event) => {
2321
+ if (event.key === "Escape") props.onClose();
2322
+ };
2323
+ document.addEventListener("keydown", onKey);
2324
+ return () => document.removeEventListener("keydown", onKey);
2325
+ }, [props]);
2326
+ return h(
2327
+ "div",
2328
+ { className: "dshgt-modal-mask", onClick: (event) => {
2329
+ if (event.target === event.currentTarget) props.onClose();
2330
+ } },
2331
+ h(
2332
+ "div",
2333
+ { className: "dshgt-modal dshgt-log-modal", role: "dialog", "aria-modal": "true" },
2334
+ h(
2335
+ "div",
2336
+ { className: "dshgt-log-head" },
2337
+ h("h3", { className: "dshgt-log-title" }, t("log.title")),
2338
+ h("span", { className: "dshgt-hint dshgt-mono" }, runId),
2339
+ h(Btn, { small: true, onClick: () => props.onClose() }, t("log.close"))
2340
+ ),
2341
+ loading ? h("p", { className: "dshgt-hint" }, t("log.loading")) : null,
2342
+ error === null ? null : h(Alert, { kind: "err" }, error),
2343
+ loading || error !== null ? null : text === "" ? h("p", { className: "dshgt-hint" }, t("log.empty")) : h("pre", { className: "dshgt-log-pre" }, text),
2344
+ h(
2345
+ "div",
2346
+ { className: "dshgt-log-foot" },
1456
2347
  h(
1457
- Alert,
1458
- { kind: "err", title: `${t("error.prefix")} ${shown.error.code}` },
1459
- h("div", null, shown.error.message),
1460
- h("div", { className: "dshgt-hint" }, shown.error.hint),
1461
- shown.error.inferred ? h("div", { className: "dshgt-hint" }, "(错误码由插件按核心 stderr 推断)") : null
2348
+ Btn,
2349
+ {
2350
+ small: true,
2351
+ disabled: text === "",
2352
+ onClick: () => {
2353
+ void copyText(text).then((okResult) => {
2354
+ if (!okResult) return;
2355
+ setCopied(true);
2356
+ setTimeout(() => setCopied(false), 1600);
2357
+ });
2358
+ }
2359
+ },
2360
+ copied ? t("log.copied") : t("log.copy")
1462
2361
  ),
1463
- shown.logFile === void 0 ? null : h("p", { className: "dshgt-hint" }, `日志:${shown.logFile}`)
1464
- ) : null
1465
- ) : h(HistoryTable, { t, records: state.status.history })
2362
+ h("span", { className: "dshgt-hint" }, t("log.truncated"))
2363
+ )
2364
+ )
1466
2365
  );
1467
2366
  }
1468
- function stepFromError(area) {
1469
- switch (area) {
1470
- case "core":
1471
- return "resolve";
1472
- case "model":
1473
- return "summarize";
1474
- case "repo":
1475
- case "auth":
1476
- return "commit";
1477
- default:
1478
- return "state";
2367
+ function RunsView(props) {
2368
+ const { t, store, controller } = props;
2369
+ const [filter, setFilter] = import_react.default.useState("all");
2370
+ const [openId, setOpenId] = import_react.default.useState(null);
2371
+ const [logId, setLogId] = import_react.default.useState(null);
2372
+ const state = store.state;
2373
+ if (state === null) {
2374
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2375
+ }
2376
+ const history = state.status.history;
2377
+ const records = history.filter((record) => {
2378
+ if (filter === "failed") return !record.ok;
2379
+ if (filter === "dry") return record.kind === "dry-run";
2380
+ return true;
2381
+ });
2382
+ return h(
2383
+ "div",
2384
+ { className: "dshgt-view dshgt-runs" },
2385
+ controller.running ? h(LiveRun, { t, controller, startedAt: controller.startedAt }) : null,
2386
+ controller.error === null ? null : h("div", { className: "dshgt-alert err" }, controller.error),
2387
+ h(
2388
+ "div",
2389
+ { className: "dshgt-runs-head" },
2390
+ h("span", { className: "dshgt-hint" }, t("runs.count", { count: history.length })),
2391
+ h(
2392
+ "div",
2393
+ { className: "dshgt-runs-filter" },
2394
+ FILTERS.map(
2395
+ (item) => h(
2396
+ "button",
2397
+ {
2398
+ key: item.id,
2399
+ type: "button",
2400
+ className: "dshgt-chip",
2401
+ "aria-pressed": filter === item.id ? "true" : "false",
2402
+ onClick: () => setFilter(item.id)
2403
+ },
2404
+ t(item.key)
2405
+ )
2406
+ )
2407
+ )
2408
+ ),
2409
+ records.length === 0 ? h("div", { className: "dshgt-empty" }, t("runs.empty")) : h(
2410
+ "div",
2411
+ { className: "dshgt-run-list" },
2412
+ records.map((record) => {
2413
+ const open = openId === record.id;
2414
+ const pr = prStatus(t, state, record);
2415
+ return h(
2416
+ "div",
2417
+ { className: `dshgt-run-item${open ? " open" : ""}`, key: record.id },
2418
+ h(
2419
+ "button",
2420
+ {
2421
+ type: "button",
2422
+ className: "dshgt-run-row",
2423
+ "aria-expanded": open ? "true" : "false",
2424
+ onClick: () => setOpenId(open ? null : record.id)
2425
+ },
2426
+ h("span", { className: `dshgt-lamp ${record.ok ? record.skipped || record.kind === "dry-run" ? "warn" : "ok" : "err"}` }),
2427
+ h("span", { className: "dshgt-run-date" }, shortTime(record.at)),
2428
+ h(
2429
+ "span",
2430
+ { className: "dshgt-run-what" },
2431
+ record.kind === "dry-run" ? t("state.dry") : record.skipped ? t("state.skipped") : record.ok ? t("state.ok") : t("state.failed"),
2432
+ h("span", { className: "dshgt-run-muted" }, recordSummary(t, record)),
2433
+ record.legacy === true ? h("span", { className: "dshgt-tag dshgt-inline-tag" }, t("runs.legacy")) : null
2434
+ ),
2435
+ h("span", { className: "dshgt-run-files" }, t("runs.files", { count: record.files })),
2436
+ h("span", { className: "dshgt-run-duration" }, duration(record.durationMs)),
2437
+ h("span", { className: "dshgt-run-chev" }, "›")
2438
+ ),
2439
+ open ? h(
2440
+ "div",
2441
+ { className: "dshgt-run-detail" },
2442
+ !record.ok ? h(
2443
+ Alert,
2444
+ { kind: "err", title: `${t("error.prefix")} ${record.errorCode ?? "INTERNAL"}` },
2445
+ h("div", null, record.errorMessage ?? t("state.failed")),
2446
+ record.errorReason === void 0 ? null : h("div", { className: "dshgt-hint" }, reasonHint(t, record.errorReason) ?? "")
2447
+ ) : null,
2448
+ h(
2449
+ "div",
2450
+ { className: "dshgt-detail-grid" },
2451
+ detailItem(t, "run.usage.commit", record.commit === void 0 ? "—" : h("span", { className: "dshgt-mono" }, record.commit)),
2452
+ detailItem(t, "run.usage.hash", h("span", { className: "dshgt-mono" }, shortHash(record.contentHash))),
2453
+ detailItem(t, "run.usage.branch", record.branch === void 0 ? "—" : h("span", { className: "dshgt-mono" }, record.branch)),
2454
+ detailItem(t, "runs.detail.coreModel", `${record.coreVersion || "—"} · ${record.model?.model ?? "—"}`),
2455
+ detailItem(t, "runs.detail.date", record.date ?? "—"),
2456
+ detailItem(
2457
+ t,
2458
+ "run.usage.pr",
2459
+ record.prUrl === void 0 ? "—" : h(
2460
+ "span",
2461
+ { className: "dshgt-inline" },
2462
+ h("a", { href: record.prUrl, target: "_blank", rel: "noreferrer", title: record.prUrl }, t("digest.openPr")),
2463
+ pr === null ? null : h("span", { className: "dshgt-tag" }, pr.label)
2464
+ )
2465
+ ),
2466
+ detailItem(t, "runs.detail.warnings", record.warnings.length === 0 ? t("runs.detail.none") : record.warnings.join(";")),
2467
+ detailItem(t, "runs.detail.files", `${record.files} · ${duration(record.durationMs)}`)
2468
+ ),
2469
+ record.legacy === true ? h("div", { className: "dshgt-hint" }, t("runs.legacyHint")) : null,
2470
+ h(
2471
+ "div",
2472
+ { className: "dshgt-run-actions" },
2473
+ record.preview === void 0 ? h("span", { className: "dshgt-hint" }, t("runs.detail.noPreview")) : h(Btn, { small: true, onClick: () => props.onOpenDigest(record.date) }, t("runs.detail.readDigest")),
2474
+ h(Btn, { small: true, onClick: () => setLogId(record.id) }, t("runs.detail.log")),
2475
+ record.preview === void 0 ? null : h("span", { className: "dshgt-hint dshgt-mono" }, record.preview.file)
2476
+ )
2477
+ ) : null
2478
+ );
2479
+ })
2480
+ ),
2481
+ logId === null ? null : h(LogDrawer, { t, runId: logId, onClose: () => setLogId(null) })
2482
+ );
2483
+ }
2484
+ function detailItem(t, label, value) {
2485
+ return h(
2486
+ "div",
2487
+ { className: "dshgt-detail-item" },
2488
+ h("div", { className: "dshgt-detail-k" }, t(label)),
2489
+ h("div", { className: "dshgt-detail-v" }, value)
2490
+ );
2491
+ }
2492
+
2493
+ // client/markdown-core.ts
2494
+ function isSafeUrl(value) {
2495
+ try {
2496
+ const url = new URL(value);
2497
+ return url.protocol === "http:" || url.protocol === "https:";
2498
+ } catch {
2499
+ return false;
1479
2500
  }
1480
2501
  }
1481
- function HistoryTable(props) {
1482
- const { t, records } = props;
2502
+ function splitFrontmatter(markdown) {
2503
+ const normalized = markdown.replace(/\r\n?/g, "\n");
2504
+ const lines = normalized.split("\n");
2505
+ if ((lines[0] ?? "").trim() !== "---") return { meta: {}, body: normalized };
2506
+ let end = -1;
2507
+ for (let index = 1; index < lines.length; index += 1) {
2508
+ if ((lines[index] ?? "").trim() === "---") {
2509
+ end = index;
2510
+ break;
2511
+ }
2512
+ }
2513
+ if (end < 0) return { meta: {}, body: normalized };
2514
+ const meta = {};
2515
+ for (const line of lines.slice(1, end)) {
2516
+ const colon = line.indexOf(":");
2517
+ if (colon <= 0) continue;
2518
+ const key = line.slice(0, colon).trim();
2519
+ let value = line.slice(colon + 1).trim();
2520
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
2521
+ value = value.slice(1, -1);
2522
+ }
2523
+ if (key !== "") meta[key] = value;
2524
+ }
2525
+ return { meta, body: lines.slice(end + 1).join("\n") };
2526
+ }
2527
+ function pushText(nodes, value) {
2528
+ if (value === "") return;
2529
+ const last = nodes[nodes.length - 1];
2530
+ if (last !== void 0 && last.type === "text") {
2531
+ last.value += value;
2532
+ return;
2533
+ }
2534
+ nodes.push({ type: "text", value });
2535
+ }
2536
+ var INLINE_PATTERN = /(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(`[^`\n]+`)|(!?\[[^\]\n]*\]\([^)\s]+\))/g;
2537
+ function parseInline(text) {
2538
+ const nodes = [];
2539
+ let cursor = 0;
2540
+ const pattern = new RegExp(INLINE_PATTERN.source, "g");
2541
+ let match;
2542
+ while ((match = pattern.exec(text)) !== null) {
2543
+ if (match.index > cursor) pushText(nodes, text.slice(cursor, match.index));
2544
+ const token = match[0];
2545
+ if (token.startsWith("**") || token.startsWith("__")) {
2546
+ nodes.push({ type: "strong", children: parseInline(token.slice(2, -2)) });
2547
+ } else if (token.startsWith("`")) {
2548
+ nodes.push({ type: "code", value: token.slice(1, -1) });
2549
+ } else {
2550
+ const image = token.startsWith("!");
2551
+ const link = /^!?\[([^\]]*)\]\(([^)]*)\)$/.exec(token);
2552
+ if (link !== null) {
2553
+ const label = link[1] ?? "";
2554
+ const href = (link[2] ?? "").trim();
2555
+ if (image) pushText(nodes, label);
2556
+ else if (isSafeUrl(href)) nodes.push({ type: "link", href, children: parseInline(label) });
2557
+ else pushText(nodes, label);
2558
+ } else {
2559
+ pushText(nodes, token);
2560
+ }
2561
+ }
2562
+ cursor = pattern.lastIndex;
2563
+ }
2564
+ if (cursor < text.length) pushText(nodes, text.slice(cursor));
2565
+ return nodes;
2566
+ }
2567
+ var FENCE = /^\s*```\s*([^\s`]*)\s*$/;
2568
+ var HEADING = /^(#{1,6})\s+(.*)$/;
2569
+ var HR = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
2570
+ var QUOTE = /^\s*>\s?(.*)$/;
2571
+ var BULLET = /^\s*[-*+]\s+(.*)$/;
2572
+ var ORDERED = /^\s*\d+[.)]\s+(.*)$/;
2573
+ function isBlockStart(line) {
2574
+ return FENCE.test(line) || HEADING.test(line) || HR.test(line) || QUOTE.test(line) || BULLET.test(line) || ORDERED.test(line);
2575
+ }
2576
+ function parseBlocks(body) {
2577
+ const lines = body.split("\n");
2578
+ const blocks = [];
2579
+ let index = 0;
2580
+ while (index < lines.length) {
2581
+ const line = lines[index] ?? "";
2582
+ if (line.trim() === "") {
2583
+ index += 1;
2584
+ continue;
2585
+ }
2586
+ const fence = FENCE.exec(line);
2587
+ if (fence !== null) {
2588
+ const lang = fence[1] ?? "";
2589
+ index += 1;
2590
+ const buffer2 = [];
2591
+ while (index < lines.length) {
2592
+ const current = lines[index] ?? "";
2593
+ if (/^\s*```\s*$/.test(current)) {
2594
+ index += 1;
2595
+ break;
2596
+ }
2597
+ buffer2.push(current);
2598
+ index += 1;
2599
+ }
2600
+ blocks.push({ type: "code", value: buffer2.join("\n"), ...lang === "" ? {} : { lang } });
2601
+ continue;
2602
+ }
2603
+ const heading = HEADING.exec(line);
2604
+ if (heading !== null) {
2605
+ blocks.push({ type: "heading", level: (heading[1] ?? "#").length, children: parseInline((heading[2] ?? "").trim()) });
2606
+ index += 1;
2607
+ continue;
2608
+ }
2609
+ if (HR.test(line)) {
2610
+ blocks.push({ type: "hr" });
2611
+ index += 1;
2612
+ continue;
2613
+ }
2614
+ if (QUOTE.test(line)) {
2615
+ const buffer2 = [];
2616
+ while (index < lines.length) {
2617
+ const match = QUOTE.exec(lines[index] ?? "");
2618
+ if (match === null) break;
2619
+ buffer2.push(match[1] ?? "");
2620
+ index += 1;
2621
+ }
2622
+ blocks.push({ type: "quote", children: parseInline(buffer2.join(" ").trim()) });
2623
+ continue;
2624
+ }
2625
+ if (BULLET.test(line)) {
2626
+ const items = [];
2627
+ while (index < lines.length) {
2628
+ const match = BULLET.exec(lines[index] ?? "");
2629
+ if (match === null) break;
2630
+ items.push(parseInline((match[1] ?? "").trim()));
2631
+ index += 1;
2632
+ }
2633
+ blocks.push({ type: "list", ordered: false, items });
2634
+ continue;
2635
+ }
2636
+ if (ORDERED.test(line)) {
2637
+ const items = [];
2638
+ while (index < lines.length) {
2639
+ const match = ORDERED.exec(lines[index] ?? "");
2640
+ if (match === null) break;
2641
+ items.push(parseInline((match[1] ?? "").trim()));
2642
+ index += 1;
2643
+ }
2644
+ blocks.push({ type: "list", ordered: true, items });
2645
+ continue;
2646
+ }
2647
+ const buffer = [];
2648
+ while (index < lines.length) {
2649
+ const current = lines[index] ?? "";
2650
+ if (current.trim() === "") break;
2651
+ if (buffer.length > 0 && isBlockStart(current)) break;
2652
+ buffer.push(current.trim());
2653
+ index += 1;
2654
+ }
2655
+ blocks.push({ type: "paragraph", children: parseInline(buffer.join(" ")) });
2656
+ }
2657
+ return blocks;
2658
+ }
2659
+ function parseMarkdown(markdown) {
2660
+ const { meta, body } = splitFrontmatter(markdown);
2661
+ return { meta, blocks: parseBlocks(body) };
2662
+ }
2663
+
2664
+ // client/markdown.ts
2665
+ function renderInline(nodes, prefix) {
2666
+ return nodes.map((node, index) => {
2667
+ const key = `${prefix}-${index}`;
2668
+ switch (node.type) {
2669
+ case "text":
2670
+ return node.value;
2671
+ case "strong":
2672
+ return h("strong", { key }, renderInline(node.children, key));
2673
+ case "code":
2674
+ return h("code", { key, className: "dshgt-md-code" }, node.value);
2675
+ case "link":
2676
+ return h(
2677
+ "a",
2678
+ { key, href: node.href, target: "_blank", rel: "noreferrer" },
2679
+ renderInline(node.children, key)
2680
+ );
2681
+ }
2682
+ });
2683
+ }
2684
+ function renderBlock(block, key) {
2685
+ switch (block.type) {
2686
+ case "heading": {
2687
+ const level = Math.min(Math.max(block.level, 1), 6);
2688
+ return h(`h${level}`, { key, className: `dshgt-md-h dshgt-md-h${level}` }, renderInline(block.children, key));
2689
+ }
2690
+ case "paragraph":
2691
+ return h("p", { key, className: "dshgt-md-p" }, renderInline(block.children, key));
2692
+ case "list":
2693
+ return h(
2694
+ block.ordered ? "ol" : "ul",
2695
+ { key, className: "dshgt-md-list" },
2696
+ block.items.map((item, index) => h("li", { key: `${key}-${index}` }, renderInline(item, `${key}-${index}`)))
2697
+ );
2698
+ case "quote":
2699
+ return h("blockquote", { key, className: "dshgt-md-quote" }, renderInline(block.children, key));
2700
+ case "code":
2701
+ return h(
2702
+ "pre",
2703
+ { key, className: "dshgt-md-pre", ...block.lang === void 0 ? {} : { "data-lang": block.lang } },
2704
+ h("code", null, block.value)
2705
+ );
2706
+ case "hr":
2707
+ return h("hr", { key, className: "dshgt-md-hr" });
2708
+ }
2709
+ }
2710
+ function renderMarkdown(markdown) {
2711
+ const parsed = parseMarkdown(markdown);
2712
+ return {
2713
+ meta: parsed.meta,
2714
+ nodes: parsed.blocks.map((block, index) => renderBlock(block, `b${index}`))
2715
+ };
2716
+ }
2717
+
2718
+ // client/digest.ts
2719
+ var STATUS_TAG = {
2720
+ preview: "digest.tag.preview",
2721
+ failed: "digest.tag.failed",
2722
+ skipped: "digest.tag.skipped",
2723
+ dry: "digest.tag.dry",
2724
+ legacy: "digest.tag.legacy",
2725
+ missing: "digest.tag.missing"
2726
+ };
2727
+ var STATUS_LABEL = {
2728
+ preview: "state.ok",
2729
+ failed: "state.failed",
2730
+ skipped: "state.skipped",
2731
+ dry: "state.dry",
2732
+ legacy: "digest.tag.legacy",
2733
+ missing: "digest.tag.missing"
2734
+ };
2735
+ var STATUS_LAMP2 = {
2736
+ preview: "ok",
2737
+ failed: "err",
2738
+ skipped: "warn",
2739
+ dry: "warn",
2740
+ legacy: "warn",
2741
+ missing: "warn"
2742
+ };
2743
+ var META_KEYS = ["date", "period", "languages", "count", "degraded", "coreVersion", "contentHash", "generatedAt"];
2744
+ function EmptyDigest(props) {
2745
+ const { t, day, nearest } = props;
2746
+ const status = props.error === void 0 ? day.status : "missing";
2747
+ const key = status === "legacy" ? "legacy" : status === "missing" ? "missing" : status;
1483
2748
  return h(
1484
- Card,
1485
- { title: t("run.history.title"), hint: t("run.history.hint") },
1486
- records.length === 0 ? h("p", { className: "dshgt-hint" }, t("run.history.empty")) : h(
1487
- "table",
1488
- { className: "dshgt-table" },
1489
- h("thead", null, h("tr", null, [t("col.date"), t("col.result"), t("col.files"), t("col.duration"), t("col.commit"), t("col.error")].map((label) => h("th", { key: label }, label)))),
2749
+ "div",
2750
+ { className: "dshgt-digest-empty" },
2751
+ h(
2752
+ "div",
2753
+ { className: "dshgt-status-line" },
2754
+ h("span", { className: `dshgt-lamp ${STATUS_LAMP2[status]}` }),
2755
+ h("b", null, t(STATUS_LABEL[status])),
2756
+ h("span", { className: "dshgt-dim" }, formatDayLabel(day.date, props.lang))
2757
+ ),
2758
+ h("h2", null, t(`digest.empty.${key}.title`)),
2759
+ h("p", null, t(`digest.empty.${key}.reason`)),
2760
+ day.record.errorCode === void 0 && props.error === void 0 ? null : h(
2761
+ "p",
2762
+ { className: "dshgt-mono dshgt-digest-error" },
2763
+ [day.record.errorCode, day.record.errorMessage ?? props.error].filter((item) => item !== void 0 && item !== "").join(" · ")
2764
+ ),
2765
+ h(
2766
+ "div",
2767
+ { className: "dshgt-actions" },
2768
+ h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.empty.viewRuns")),
2769
+ nearest === void 0 ? null : h(Btn, { small: true, onClick: () => props.onSelectDate(nearest.date) }, t("digest.empty.latest", { date: formatDayLabel(nearest.date, props.lang) }))
2770
+ )
2771
+ );
2772
+ }
2773
+ function DigestView(props) {
2774
+ const { t, lang, store, selectedDate } = props;
2775
+ const [mode, setMode] = import_react.default.useState("render");
2776
+ const [payload, setPayload] = import_react.default.useState(null);
2777
+ const [loading, setLoading] = import_react.default.useState(false);
2778
+ const [error, setError] = import_react.default.useState(null);
2779
+ const [copied, setCopied] = import_react.default.useState(false);
2780
+ const state = store.state;
2781
+ const history = state?.status.history ?? [];
2782
+ const timeZone = state?.schedule.timezone ?? "UTC";
2783
+ const days = buildDigestDates(history, { timeZone });
2784
+ const defaultDate = days.find((item) => item.status === "preview")?.date ?? days[0]?.date;
2785
+ const effectiveDate = selectedDate !== void 0 && days.some((day2) => day2.date === selectedDate) ? selectedDate : defaultDate;
2786
+ const day = days.find((item) => item.date === effectiveDate);
2787
+ const index = day === void 0 ? -1 : days.indexOf(day);
2788
+ const previewCount = days.filter((item) => item.status === "preview").length;
2789
+ const previewRunId = day !== void 0 && day.status === "preview" && day.record.preview !== void 0 ? day.record.id : void 0;
2790
+ import_react.default.useEffect(() => {
2791
+ if (effectiveDate !== void 0 && effectiveDate !== selectedDate) props.onSelectDate(effectiveDate);
2792
+ }, [effectiveDate, selectedDate, props.onSelectDate]);
2793
+ import_react.default.useEffect(() => {
2794
+ setMode("render");
2795
+ }, [effectiveDate]);
2796
+ import_react.default.useEffect(() => {
2797
+ if (previewRunId === void 0) {
2798
+ setPayload(null);
2799
+ setError(null);
2800
+ setLoading(false);
2801
+ return () => void 0;
2802
+ }
2803
+ const controller = new AbortController();
2804
+ setLoading(true);
2805
+ setError(null);
2806
+ setPayload(null);
2807
+ getPreview(previewRunId, controller.signal).then((result) => {
2808
+ if (controller.signal.aborted) return;
2809
+ setPayload(result);
2810
+ setLoading(false);
2811
+ }).catch((failure) => {
2812
+ if (controller.signal.aborted) return;
2813
+ setError(failure instanceof Error ? failure.message : String(failure));
2814
+ setLoading(false);
2815
+ });
2816
+ return () => controller.abort();
2817
+ }, [previewRunId]);
2818
+ if (state === null) {
2819
+ return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2820
+ }
2821
+ const nearest = day === void 0 ? void 0 : days.find((item) => item.status === "preview" && item.date !== day.date);
2822
+ let rendered = null;
2823
+ let parseFailed = false;
2824
+ if (payload !== null) {
2825
+ try {
2826
+ rendered = renderMarkdown(payload.preview.text);
2827
+ } catch {
2828
+ parseFailed = true;
2829
+ }
2830
+ }
2831
+ const subbar = h(
2832
+ "div",
2833
+ { className: "dshgt-digest-subbar" },
2834
+ h(
2835
+ "div",
2836
+ { className: "dshgt-datenav" },
2837
+ h(
2838
+ "button",
2839
+ {
2840
+ type: "button",
2841
+ className: "dshgt-icon-btn",
2842
+ "aria-label": t("digest.prev"),
2843
+ title: t("digest.prev"),
2844
+ disabled: index < 0 || index >= days.length - 1,
2845
+ onClick: () => {
2846
+ const target = days[index + 1];
2847
+ if (target !== void 0) props.onSelectDate(target.date);
2848
+ }
2849
+ },
2850
+ "‹"
2851
+ ),
2852
+ h(
2853
+ "select",
2854
+ {
2855
+ className: "dshgt-dateselect",
2856
+ "aria-label": t("digest.select"),
2857
+ value: effectiveDate ?? "",
2858
+ onChange: (event) => props.onSelectDate(event.target.value),
2859
+ disabled: days.length === 0
2860
+ },
2861
+ days.map((item) => h("option", { key: item.date, value: item.date }, `${formatDayLabel(item.date, lang)} · ${t(STATUS_TAG[item.status])}`))
2862
+ ),
1490
2863
  h(
1491
- "tbody",
1492
- null,
1493
- records.map(
1494
- (record) => h(
1495
- "tr",
1496
- { key: String(record.id) },
1497
- h("td", { className: "dshgt-mono" }, localTime(record.at)),
1498
- h("td", null, h(Pill, { kind: record.ok ? record.skipped ? "warn" : "ok" : "err" }, record.kind === "dry-run" ? t("state.dry") : record.skipped ? t("state.skipped") : record.ok ? t("state.ok") : t("state.failed"))),
1499
- h("td", { className: "dshgt-mono" }, String(record.files ?? 0)),
1500
- h("td", { className: "dshgt-mono" }, duration(Number(record.durationMs ?? 0))),
1501
- h("td", { className: "dshgt-mono" }, [record.branch, record.prUrl].filter(Boolean).join(" · ") || "—"),
1502
- h("td", { className: "dshgt-mono" }, record.errorCode ?? "—")
2864
+ "button",
2865
+ {
2866
+ type: "button",
2867
+ className: "dshgt-icon-btn",
2868
+ "aria-label": t("digest.next"),
2869
+ title: t("digest.next"),
2870
+ disabled: index <= 0,
2871
+ onClick: () => {
2872
+ const target = days[index - 1];
2873
+ if (target !== void 0) props.onSelectDate(target.date);
2874
+ }
2875
+ },
2876
+ "›"
2877
+ )
2878
+ ),
2879
+ h(
2880
+ "span",
2881
+ { className: "dshgt-dim" },
2882
+ days.length === 0 ? t("digest.window.empty") : t("digest.window", { days: days.length, previews: previewCount })
2883
+ ),
2884
+ h("span", { className: "dshgt-grow" }),
2885
+ h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.allRuns"))
2886
+ );
2887
+ if (day === void 0) {
2888
+ return h(
2889
+ "div",
2890
+ { className: "dshgt-view dshgt-digest" },
2891
+ subbar,
2892
+ h(
2893
+ "div",
2894
+ { className: "dshgt-digest-empty" },
2895
+ h("h2", null, t("digest.empty.none.title")),
2896
+ h("p", null, t("digest.empty.none.reason")),
2897
+ h("div", { className: "dshgt-actions" }, h(Btn, { small: true, onClick: props.onOpenRuns }, t("digest.empty.viewRuns")))
2898
+ )
2899
+ );
2900
+ }
2901
+ const showEmpty = day.status !== "preview" || error !== null;
2902
+ const record = day.record;
2903
+ return h(
2904
+ "div",
2905
+ { className: "dshgt-view dshgt-digest" },
2906
+ subbar,
2907
+ h(
2908
+ "div",
2909
+ { className: "dshgt-digest-sheet" },
2910
+ showEmpty ? h(EmptyDigest, {
2911
+ t,
2912
+ lang,
2913
+ day,
2914
+ ...nearest === void 0 ? {} : { nearest },
2915
+ ...error === null ? {} : { error },
2916
+ onOpenRuns: props.onOpenRuns,
2917
+ onSelectDate: props.onSelectDate
2918
+ }) : h(
2919
+ "div",
2920
+ { className: "dshgt-digest-body" },
2921
+ h(
2922
+ "div",
2923
+ { className: "dshgt-digest-bar" },
2924
+ h(
2925
+ "span",
2926
+ { className: "dshgt-filechip dshgt-filechip-static" },
2927
+ h("span", { className: "dshgt-mono" }, record.preview?.file ?? "—")
2928
+ ),
2929
+ h("span", { className: "dshgt-dim" }, record.preview === void 0 ? "—" : formatBytes(record.preview.bytes)),
2930
+ payload?.preview.truncated === true ? h("span", { className: "dshgt-tag" }, t("digest.truncated")) : null,
2931
+ h("span", { className: "dshgt-grow" }),
2932
+ h(
2933
+ "div",
2934
+ { className: "dshgt-seg", role: "group", "aria-label": t("digest.mode") },
2935
+ h(
2936
+ "button",
2937
+ {
2938
+ type: "button",
2939
+ "aria-pressed": mode === "render" ? "true" : "false",
2940
+ className: mode === "render" ? "on" : "",
2941
+ onClick: () => setMode("render")
2942
+ },
2943
+ t("digest.render")
2944
+ ),
2945
+ h(
2946
+ "button",
2947
+ {
2948
+ type: "button",
2949
+ "aria-pressed": mode === "raw" ? "true" : "false",
2950
+ className: mode === "raw" ? "on" : "",
2951
+ onClick: () => setMode("raw")
2952
+ },
2953
+ t("digest.raw")
2954
+ )
2955
+ ),
2956
+ h(
2957
+ Btn,
2958
+ {
2959
+ small: true,
2960
+ onClick: () => {
2961
+ const file = record.preview?.file;
2962
+ if (file === void 0) return;
2963
+ void copyText(file).then((okResult) => {
2964
+ if (!okResult) return;
2965
+ setCopied(true);
2966
+ setTimeout(() => setCopied(false), 1600);
2967
+ });
2968
+ }
2969
+ },
2970
+ copied ? t("digest.copied") : t("digest.copyPath")
2971
+ ),
2972
+ record.prUrl === void 0 ? null : h(
2973
+ "a",
2974
+ { className: "dshgt-btn dshgt-btn-sm", href: record.prUrl, target: "_blank", rel: "noreferrer" },
2975
+ t("digest.openPr")
2976
+ )
2977
+ ),
2978
+ loading || payload === null ? h("div", { className: "dshgt-digest-loading" }, t("digest.loading")) : mode === "raw" || parseFailed ? h("pre", { className: "dshgt-digest-raw" }, payload.preview.text) : h(
2979
+ "div",
2980
+ { className: "dshgt-digest-doc" },
2981
+ rendered === null ? null : h(
2982
+ import_react.default.Fragment,
2983
+ null,
2984
+ metaLine(rendered),
2985
+ h("div", { className: "dshgt-md" }, rendered.nodes)
1503
2986
  )
1504
2987
  )
1505
2988
  )
1506
2989
  )
1507
2990
  );
1508
2991
  }
2992
+ function metaLine(rendered) {
2993
+ const entries = META_KEYS.filter((key) => rendered.meta[key] !== void 0 && rendered.meta[key] !== "");
2994
+ if (entries.length === 0) return null;
2995
+ return h(
2996
+ "div",
2997
+ { className: "dshgt-doc-meta" },
2998
+ entries.map((key) => {
2999
+ const value = rendered.meta[key] ?? "";
3000
+ if (key === "contentHash") {
3001
+ const short = value.replace(/^sha256:/, "").slice(0, 10);
3002
+ return h("span", { key, title: value }, `${key}: ${short}`);
3003
+ }
3004
+ return h("span", { key }, `${key}: ${value}`);
3005
+ })
3006
+ );
3007
+ }
3008
+
3009
+ // client/about.ts
3010
+ function AboutDialog(props) {
3011
+ const { t, state } = props;
3012
+ const [copied, setCopied] = import_react.default.useState(false);
3013
+ const plugin = state.plugin;
3014
+ const fullName = `${plugin.name}@v${plugin.version}`;
3015
+ const repository = plugin.repository;
3016
+ const ownerRepo = repository === void 0 ? void 0 : repository.replace(/^https?:\/\/github\.com\//, "");
3017
+ const repoVersion = ownerRepo === void 0 ? void 0 : `${ownerRepo}@v${plugin.version}`;
3018
+ import_react.default.useEffect(() => {
3019
+ const onKey = (event) => {
3020
+ if (event.key === "Escape") props.onClose();
3021
+ };
3022
+ document.addEventListener("keydown", onKey);
3023
+ return () => document.removeEventListener("keydown", onKey);
3024
+ }, [props]);
3025
+ const copy = () => {
3026
+ void copyText(fullName).then((ok) => {
3027
+ if (!ok) return;
3028
+ setCopied(true);
3029
+ setTimeout(() => setCopied(false), 1600);
3030
+ });
3031
+ };
3032
+ return h(
3033
+ "div",
3034
+ { className: "dshgt-modal-mask", onClick: (event) => {
3035
+ if (event.target === event.currentTarget) props.onClose();
3036
+ } },
3037
+ h(
3038
+ "div",
3039
+ { className: "dshgt-modal dshgt-about", role: "dialog", "aria-modal": "true", "aria-label": t("about.title") },
3040
+ h(
3041
+ "div",
3042
+ { className: "dshgt-about-head" },
3043
+ h("h3", { className: "dshgt-about-title" }, t("about.title")),
3044
+ h(Btn, { small: true, onClick: () => props.onClose() }, t("about.close"))
3045
+ ),
3046
+ h(
3047
+ "button",
3048
+ { type: "button", className: "dshgt-about-pkg", onClick: copy, title: t("about.copy") },
3049
+ h("span", { className: "dshgt-mono" }, fullName),
3050
+ h("span", { className: "dshgt-hint" }, copied ? t("about.copied") : t("about.copy"))
3051
+ ),
3052
+ h(KV, {
3053
+ items: [
3054
+ [
3055
+ t("about.repo"),
3056
+ repository === void 0 || repoVersion === void 0 ? h("span", { className: "dshgt-hint" }, t("about.repo.missing")) : h("a", { href: repository, target: "_blank", rel: "noreferrer" }, repoVersion)
3057
+ ],
3058
+ [t("about.license"), "MIT"],
3059
+ [t("about.core"), state.core.version === void 0 ? "—" : `trendradar ${state.core.version}`],
3060
+ [t("about.env"), t("about.env.value")]
3061
+ ]
3062
+ })
3063
+ )
3064
+ );
3065
+ }
1509
3066
 
1510
3067
  // client/panel-icon.ts
1511
3068
  var TREND_LINE = "3 16.5 9 10.5 13 14.5 21 6.5";
@@ -1534,11 +3091,99 @@ function SidebarPanelIcon(props) {
1534
3091
  // client/index.ts
1535
3092
  var PANEL_ID = "dsh-github-trending";
1536
3093
  var inject = ["slots", "locale"];
3094
+ var NAV = [
3095
+ { id: "overview", label: "nav.overview", icon: "grid" },
3096
+ { id: "digest", label: "nav.digest", icon: "doc" },
3097
+ { id: "runs", label: "nav.runs", icon: "list" },
3098
+ { id: "settings", label: "nav.settings", icon: "sliders" }
3099
+ ];
3100
+ function navIcon(name, size = 16) {
3101
+ const common = {
3102
+ width: size,
3103
+ height: size,
3104
+ viewBox: "0 0 24 24",
3105
+ fill: "none",
3106
+ stroke: "currentColor",
3107
+ strokeWidth: 1.7,
3108
+ strokeLinecap: "round",
3109
+ strokeLinejoin: "round",
3110
+ "aria-hidden": "true",
3111
+ focusable: "false"
3112
+ };
3113
+ if (name === "grid") {
3114
+ return h(
3115
+ "svg",
3116
+ common,
3117
+ h("rect", { x: 3.5, y: 3.5, width: 7, height: 7, rx: 1.5 }),
3118
+ h("rect", { x: 13.5, y: 3.5, width: 7, height: 7, rx: 1.5 }),
3119
+ h("rect", { x: 3.5, y: 13.5, width: 7, height: 7, rx: 1.5 }),
3120
+ h("rect", { x: 13.5, y: 13.5, width: 7, height: 7, rx: 1.5 })
3121
+ );
3122
+ }
3123
+ if (name === "doc") {
3124
+ return h(
3125
+ "svg",
3126
+ common,
3127
+ h("path", { d: "M5 4.5h11a2 2 0 0 1 2 2V20H7a2 2 0 0 1-2-2z" }),
3128
+ h("path", { d: "M8 8h7M8 12h7M8 16h4" })
3129
+ );
3130
+ }
3131
+ if (name === "list") {
3132
+ return h(
3133
+ "svg",
3134
+ common,
3135
+ h("path", { d: "M8 6h12M8 12h12M8 18h12" }),
3136
+ h("path", { d: "M4 6h.01M4 12h.01M4 18h.01" })
3137
+ );
3138
+ }
3139
+ return h(
3140
+ "svg",
3141
+ common,
3142
+ h("path", { d: "M4 7h10M18 7h2M4 17h4M12 17h8" }),
3143
+ h("circle", { cx: 16, cy: 7, r: 2.2 }),
3144
+ h("circle", { cx: 10, cy: 17, r: 2.2 })
3145
+ );
3146
+ }
3147
+ function brandLogo() {
3148
+ return h(
3149
+ "svg",
3150
+ { width: 28, height: 28, viewBox: "0 0 64 64", "aria-hidden": "true" },
3151
+ h("rect", { x: 3, y: 3, width: 58, height: 58, rx: 15, fill: "#0d1117", stroke: "rgba(255,255,255,.14)", strokeWidth: 1 }),
3152
+ h("path", {
3153
+ d: "M18 46V22l14 12 14-12v24",
3154
+ fill: "none",
3155
+ stroke: "#58a6ff",
3156
+ strokeWidth: 4,
3157
+ strokeLinecap: "round",
3158
+ strokeLinejoin: "round"
3159
+ }),
3160
+ h("circle", { cx: 32, cy: 45, r: 3, fill: "#3fb950" })
3161
+ );
3162
+ }
3163
+ function relativeFuture(iso, t) {
3164
+ if (iso === void 0) return void 0;
3165
+ const target = new Date(iso).getTime();
3166
+ if (Number.isNaN(target)) return void 0;
3167
+ const diff = target - Date.now();
3168
+ if (diff <= 0) return void 0;
3169
+ const minutes = Math.max(1, Math.round(diff / 6e4));
3170
+ if (minutes < 60) return t("time.in.minutes", { minutes });
3171
+ const hours = Math.floor(minutes / 60);
3172
+ if (hours < 24) return t("time.in.hours", { hours, minutes: minutes % 60 });
3173
+ return t("time.in.days", { days: Math.floor(hours / 24) });
3174
+ }
3175
+ function nextRunText(iso) {
3176
+ if (iso === void 0) return "—";
3177
+ const date = new Date(iso);
3178
+ if (Number.isNaN(date.getTime())) return "—";
3179
+ return date.toLocaleString(void 0, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
3180
+ }
1537
3181
  function apply(ctx) {
1538
3182
  const slots = ctx.get?.("slots");
1539
3183
  if (slots === void 0 || typeof slots.register !== "function") return;
1540
3184
  const locale = ctx.get?.("locale");
1541
3185
  const t = buildTranslate(locale);
3186
+ const lang = activeLang(locale);
1542
3187
  const disposeStyles = injectStyles();
1543
3188
  const disposers = [];
1544
3189
  const pickDirectory = async () => {
@@ -1550,32 +3195,188 @@ function apply(ctx) {
1550
3195
  }
1551
3196
  function PanelView() {
1552
3197
  const value = store();
1553
- const [tab, setTab] = import_react.default.useState("runs");
1554
- const tabButton = (id, label) => h(
1555
- "button",
1556
- {
1557
- type: "button",
1558
- className: "dshgt-tab",
1559
- role: "tab",
1560
- "aria-selected": tab === id ? "true" : "false",
1561
- onClick: () => setTab(id)
1562
- },
1563
- label
1564
- );
3198
+ const controller = useRunController(value);
3199
+ const [view, setView] = import_react.default.useState("overview");
3200
+ const [digestDate, setDigestDate] = import_react.default.useState(void 0);
3201
+ const [aboutOpen, setAboutOpen] = import_react.default.useState(false);
3202
+ const openDigest = import_react.default.useCallback((date) => {
3203
+ if (date !== void 0) setDigestDate(date);
3204
+ setView("digest");
3205
+ }, []);
3206
+ const openRuns = import_react.default.useCallback(() => setView("runs"), []);
3207
+ const openSettings = import_react.default.useCallback(() => setView("settings"), []);
3208
+ const state = value.state;
3209
+ const plugin = state?.plugin;
3210
+ const history = state?.status.history ?? [];
3211
+ const schedule = state?.schedule;
3212
+ const ready = state === null ? 0 : [
3213
+ state.core.state === "ready",
3214
+ credentialView(state.model, t).ok,
3215
+ state.settings.repo.path.trim() !== "",
3216
+ state.github.configured
3217
+ ].filter(Boolean).length;
3218
+ const fullName = plugin === void 0 ? void 0 : `${plugin.name}@v${plugin.version}`;
3219
+ const digestTail = digestDate ?? state?.status.lastResult?.date ?? state?.status.history[0]?.date;
3220
+ const showActions = view === "overview" || view === "runs";
3221
+ const viewNode = view === "overview" ? h(OverviewView, {
3222
+ t,
3223
+ lang,
3224
+ store: value,
3225
+ controller,
3226
+ onOpenDigest: openDigest,
3227
+ onOpenRuns: openRuns,
3228
+ onOpenSettings: openSettings
3229
+ }) : view === "digest" ? h(DigestView, {
3230
+ t,
3231
+ lang,
3232
+ store: value,
3233
+ ...digestDate === void 0 ? {} : { selectedDate: digestDate },
3234
+ onSelectDate: setDigestDate,
3235
+ onOpenRuns: openRuns
3236
+ }) : view === "runs" ? h(RunsView, { t, store: value, controller, onOpenDigest: openDigest }) : h(SettingsView, { t, store: value, pickDirectory });
1565
3237
  return h(
1566
3238
  "div",
1567
3239
  { className: "dshgt-root dshgt-shell" },
1568
3240
  h(
1569
3241
  "div",
1570
- { className: "dshgt-tabs", role: "tablist" },
1571
- tabButton("runs", t("tab.runs")),
1572
- tabButton("settings", t("tab.settings"))
3242
+ { className: "dshgt-app" },
3243
+ h(
3244
+ "aside",
3245
+ { className: "dshgt-rail" },
3246
+ h(
3247
+ "div",
3248
+ { className: "dshgt-brand" },
3249
+ brandLogo(),
3250
+ h(
3251
+ "div",
3252
+ null,
3253
+ h("div", { className: "dshgt-brand-name" }, t("section.title")),
3254
+ h("div", { className: "dshgt-brand-ver" }, plugin === void 0 ? "" : `v${plugin.version}`)
3255
+ )
3256
+ ),
3257
+ h(
3258
+ "nav",
3259
+ { className: "dshgt-nav", "aria-label": t("shell.nav") },
3260
+ NAV.map(
3261
+ (item) => h(
3262
+ "button",
3263
+ {
3264
+ key: item.id,
3265
+ type: "button",
3266
+ className: `dshgt-nav-item${view === item.id ? " on" : ""}`,
3267
+ "aria-current": view === item.id ? "page" : void 0,
3268
+ onClick: () => setView(item.id)
3269
+ },
3270
+ navIcon(item.icon),
3271
+ h("span", null, t(item.label)),
3272
+ item.id === "digest" && digestTail !== void 0 ? h("span", { className: "dshgt-nav-tail" }, h("span", { className: "dshgt-mono" }, digestTail.slice(5))) : item.id === "runs" && history.length > 0 ? h("span", { className: "dshgt-nav-tail" }, h("span", { className: "dshgt-mono" }, String(history.length))) : null
3273
+ )
3274
+ )
3275
+ ),
3276
+ h("div", { className: "dshgt-rail-spacer" }),
3277
+ h(
3278
+ "div",
3279
+ { className: "dshgt-rail-status" },
3280
+ h("div", { className: "dshgt-rail-cap" }, t("shell.nextRun")),
3281
+ h(
3282
+ "div",
3283
+ { className: "dshgt-rail-time" },
3284
+ schedule === void 0 || !schedule.enabled ? t("shell.scheduleOff") : schedule.nextRunAt === void 0 ? `${schedule.time} · ${schedule.timezone}` : nextRunText(schedule.nextRunAt)
3285
+ ),
3286
+ h(
3287
+ "div",
3288
+ { className: "dshgt-rail-count" },
3289
+ schedule !== void 0 && schedule.enabled && schedule.nextRunAt !== void 0 ? relativeFuture(schedule.nextRunAt, t) ?? "" : schedule === void 0 ? "" : `${schedule.time} · ${schedule.timezone}`
3290
+ ),
3291
+ h(
3292
+ "div",
3293
+ { className: "dshgt-rail-ready" },
3294
+ h("span", { className: `dshgt-lamp ${ready === 4 ? "ok" : ready > 0 ? "warn" : "err"}` }),
3295
+ t("shell.ready", { ready, total: 4 })
3296
+ )
3297
+ ),
3298
+ h(
3299
+ "div",
3300
+ { className: "dshgt-rail-pkg" },
3301
+ h(
3302
+ "button",
3303
+ {
3304
+ type: "button",
3305
+ className: "dshgt-pkg-name",
3306
+ onClick: () => setAboutOpen(true),
3307
+ title: plugin === void 0 ? "" : fullName
3308
+ },
3309
+ plugin?.name ?? t("section.title")
3310
+ ),
3311
+ h(
3312
+ "div",
3313
+ { className: "dshgt-pkg-sub" },
3314
+ h("span", { className: "dshgt-mono" }, plugin === void 0 ? "" : `@v${plugin.version}`),
3315
+ plugin?.repository === void 0 ? null : h(
3316
+ "a",
3317
+ { href: plugin.repository, target: "_blank", rel: "noreferrer" },
3318
+ `${t("shell.github")} ↗`
3319
+ ),
3320
+ h(
3321
+ "button",
3322
+ {
3323
+ type: "button",
3324
+ className: "dshgt-pkg-copy",
3325
+ title: t("shell.package.copy"),
3326
+ "aria-label": t("shell.package.copy"),
3327
+ disabled: fullName === void 0,
3328
+ onClick: () => {
3329
+ if (fullName !== void 0) void copyText(fullName);
3330
+ }
3331
+ },
3332
+ navIcon("doc", 13)
3333
+ )
3334
+ )
3335
+ )
3336
+ ),
3337
+ h(
3338
+ "main",
3339
+ { className: "dshgt-main" },
3340
+ h(
3341
+ "header",
3342
+ { className: "dshgt-page-head" },
3343
+ h(
3344
+ "div",
3345
+ { className: "dshgt-page-heading" },
3346
+ h("h1", null, t(`nav.${view}`)),
3347
+ h("p", { className: "dshgt-dek" }, t(`shell.${view}.dek`))
3348
+ ),
3349
+ h(
3350
+ "div",
3351
+ { className: "dshgt-head-actions", style: showActions ? void 0 : { visibility: "hidden" } },
3352
+ showActions ? [
3353
+ h(
3354
+ Btn,
3355
+ {
3356
+ key: "dry",
3357
+ onClick: () => void controller.run(true),
3358
+ disabled: controller.running || controller.busy
3359
+ },
3360
+ t("run.dryRunShort")
3361
+ ),
3362
+ h(
3363
+ Btn,
3364
+ {
3365
+ key: "run",
3366
+ variant: "primary",
3367
+ onClick: () => void controller.run(),
3368
+ disabled: controller.running || controller.busy
3369
+ },
3370
+ controller.running ? t("run.running") : t("run.button")
3371
+ ),
3372
+ controller.running ? h(Btn, { key: "cancel", variant: "danger", onClick: () => void controller.cancel() }, t("run.cancel")) : null
3373
+ ] : null
3374
+ )
3375
+ ),
3376
+ h("div", { className: "dshgt-main-body" }, view === "settings" ? viewNode : h("div", { className: "dshgt-scroll" }, viewNode))
3377
+ )
1573
3378
  ),
1574
- h(
1575
- "div",
1576
- { className: "dshgt-content" },
1577
- tab === "settings" ? h(SettingsView, { t, store: value, pickDirectory }) : h("div", { className: "dshgt-scroll" }, h(RunsView, { t, store: value, page: true }))
1578
- )
3379
+ aboutOpen && state !== null ? h(AboutDialog, { t, state, onClose: () => setAboutOpen(false) }) : null
1579
3380
  );
1580
3381
  }
1581
3382
  disposers.push(