@monster-orange/den 0.4.15 → 0.4.17
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/CHANGELOG.md +125 -0
- package/README.md +7 -7
- package/docs/distributions.md +137 -11
- package/package.json +1 -1
- package/src/shared/agents/conversations.ts +122 -4
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/records.ts +19 -0
- package/src/shared/agents/runtime.ts +5 -0
- package/src/shared/github-app.ts +7 -0
- package/src/ui/cli/agent-ops.ts +304 -1
- package/src/ui/cli/agents-command.ts +71 -7
- package/src/ui/server/agent-routes.ts +92 -2
- package/src/ui/server/check-pool.ts +72 -0
- package/src/ui/server/github-routes.ts +1 -1
- package/src/ui/server/index.ts +127 -15
- package/src/ui/server/public/agents-page.js +358 -38
- package/src/ui/server/public/agents.html +10 -0
- package/src/ui/server/public/environments.html +223 -140
- package/src/ui/server/public/error-page.js +70 -15
- package/src/ui/server/public/error.html +18 -0
- package/src/ui/server/public/graph.css +18 -0
- package/src/ui/server/public/graph.js +23 -6
- package/src/ui/server/public/i18n/es.js +47 -5
- package/src/ui/server/public/i18n/ja.js +47 -5
- package/src/ui/server/public/index.html +6 -0
- package/src/ui/server/public/local-dev.html +188 -25
- package/src/ui/server/public/panel.js +1 -1
- package/src/ui/server/public/persona-ui.css +11 -0
- package/src/ui/server/public/release.html +19 -1
- package/src/ui/server/public/shell.js +187 -12
- package/src/ui/server/public/topbar.js +34 -1
- package/src/ui/server/public/view-scope.js +61 -0
- package/src/ui/server/public/workspace-picker.js +2 -0
- package/src/ui/server/route-classes.ts +8 -0
- package/src/ui/server/run-history.ts +181 -0
- package/src/ui/server/session-routes.ts +97 -17
- package/src/ui/server/ui-assets.ts +2 -0
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
"b.down.one": "{n} caído",
|
|
54
54
|
"act.pin": "Fijar",
|
|
55
55
|
"act.unpin": "Quitar",
|
|
56
|
+
"act.visit": "Visitar",
|
|
57
|
+
"act.visitX": "Visitar {s} (se abre en una pestaña nueva)",
|
|
56
58
|
"act.pinX": "Fijar {s}",
|
|
57
59
|
"act.unpinX": "Quitar {s}",
|
|
58
60
|
"toast.pinned": "{s} fijado",
|
|
@@ -109,6 +111,8 @@
|
|
|
109
111
|
"kind.app": "App",
|
|
110
112
|
"kind.action": "Acción",
|
|
111
113
|
"kind.setting": "Ajuste",
|
|
114
|
+
"kind.workspace": "Espacio de trabajo",
|
|
115
|
+
"pal.goWs": "Ir al espacio de trabajo {ws}",
|
|
112
116
|
"pal.repos": "{n} repositorios",
|
|
113
117
|
"pal.repos.one": "{n} repositorio",
|
|
114
118
|
"pal.restart": "Reiniciar {s}",
|
|
@@ -254,12 +258,18 @@
|
|
|
254
258
|
"err.signin.title": "Inicia sesión en {name}",
|
|
255
259
|
"err.signin.where":
|
|
256
260
|
"Estás abriendo {name} desde otro ordenador. {name} corre en {host}, así que necesita saber que eres tú.",
|
|
257
|
-
"err.signin.
|
|
258
|
-
"
|
|
259
|
-
"err.signin.
|
|
260
|
-
"
|
|
261
|
+
"err.signin.ghStep1":
|
|
262
|
+
"En {host}, ejecuta {cmd}, o usa Ajustes › Acceso allí.",
|
|
263
|
+
"err.signin.ghStep2":
|
|
264
|
+
"Muestra un código. Introdúcelo en github.com/login/device y apruébalo.",
|
|
265
|
+
"err.signin.ghStep3":
|
|
266
|
+
"Después vuelve aquí y pulsa Iniciar sesión con GitHub, con la misma cuenta de GitHub.",
|
|
267
|
+
"err.signin.ghOwner": "Propietario: @{login}",
|
|
268
|
+
"err.signin.ghOnly":
|
|
269
|
+
"Solo la cuenta de GitHub del propietario puede iniciar sesión así. Para cualquier otra persona, usa «Aprueba este ordenador».",
|
|
270
|
+
"err.signin.ghLearn": "Más información",
|
|
261
271
|
"err.signin.githubRefused":
|
|
262
|
-
"@{login} no es el propietario de este {name}.
|
|
272
|
+
"@{login} no es el propietario de este {name}. Solo la cuenta del propietario puede usar Iniciar sesión con GitHub. Usa «Aprueba este ordenador» en su lugar.",
|
|
263
273
|
"err.signin.approveOr": "O aprueba este ordenador",
|
|
264
274
|
"err.signin.approve": "Aprueba este ordenador",
|
|
265
275
|
"err.signin.step1": "En {host} (no en este ordenador), ejecuta:",
|
|
@@ -489,6 +499,34 @@
|
|
|
489
499
|
"ag.bring.agent": "Agente {id}",
|
|
490
500
|
"ag.bring.note":
|
|
491
501
|
"La continúa en la carpeta donde se ejecutó, con las herramientas y protecciones de Den desde ahora. Sigue en ejecución si cierras el panel. Ciérrala antes en cualquier otro terminal: Den solo la ve abierta si se escribió en el último minuto.",
|
|
502
|
+
"ag.keep.model": "Mantiene {m} de la conversación.",
|
|
503
|
+
"ag.keep.modelNew": "Usa {m} a partir de ahora.",
|
|
504
|
+
"ag.keep.from":
|
|
505
|
+
"A partir de ahora trabaja como {p}: su rol, sus permisos ({mode}), las herramientas y protecciones de Den. Su historial se mantiene.",
|
|
506
|
+
"ag.keep.lost":
|
|
507
|
+
"No tendrá: {s}. Den ejecuta los agentes solo con las herramientas de Den.",
|
|
508
|
+
"ag.keep.opt": "Mantener {m}",
|
|
509
|
+
"ag.keep.optNone": "Mantener el modelo que usa",
|
|
510
|
+
"ag.keep.advNote":
|
|
511
|
+
"El modelo que usa se mantiene salvo que elijas otro aquí. Los permisos vienen de la persona.",
|
|
512
|
+
"ag.pc.item": "Cambiar persona…",
|
|
513
|
+
"ag.pc.title": "Cambiar la persona de {id}",
|
|
514
|
+
"ag.pc.note":
|
|
515
|
+
"Den lo detiene y reanuda la misma conversación con la nueva persona. Su id, nombre, carpeta y pestaña se mantienen.",
|
|
516
|
+
"ag.pc.go": "Cambiar persona",
|
|
517
|
+
"ag.pc.going": "Cambiando…",
|
|
518
|
+
"ag.pc.current": "actual",
|
|
519
|
+
"ag.pc.busy":
|
|
520
|
+
"Está trabajando. ¿Cambiar después de este turno o detenerlo ahora?",
|
|
521
|
+
"ag.pc.after": "Después de este turno",
|
|
522
|
+
"ag.pc.now": "Detenerlo ahora",
|
|
523
|
+
"ag.pc.fresh":
|
|
524
|
+
"{id} ahora es {p}: aún no hay nada que reanudar; se inició de nuevo (mismo id de conversación, carpeta y alcance)",
|
|
525
|
+
"ag.freshLine":
|
|
526
|
+
"Aún no hay nada que reanudar; se inició de nuevo (mismo id de conversación, carpeta y alcance)",
|
|
527
|
+
"ag.pc.done": "{id} ahora es {p}",
|
|
528
|
+
"ag.pc.pending": "{id} pasará a ser {p} cuando termine este turno",
|
|
529
|
+
"ag.more": "Más para {id}",
|
|
492
530
|
"ag.bring.go": "Traer",
|
|
493
531
|
"ag.bring.going": "Trayendo…",
|
|
494
532
|
"ag.bring.done": "Traída como {id}",
|
|
@@ -637,7 +675,11 @@
|
|
|
637
675
|
"Su token fue revocado (web logout --all): detenlo y reanúdalo, o inícialo de nuevo, para darle uno nuevo",
|
|
638
676
|
"ag.ended":
|
|
639
677
|
"Esta sesión ha terminado. Su registro se queda hasta que se reanude o se elimine.",
|
|
678
|
+
"ag.switched": "Cambiado a {p}",
|
|
640
679
|
"ag.exitedLine": "[la sesión terminó{code}]",
|
|
680
|
+
"ag.refusedUnknown": "el servidor rechazó la terminal",
|
|
681
|
+
"ag.refusedStatus": "el servidor rechazó la terminal ({status})",
|
|
682
|
+
"ag.goWs": "Abrir {ws} en Espacios de trabajo",
|
|
641
683
|
"ag.lost": "[desconectado: {why}]",
|
|
642
684
|
"ag.reconnect": "Reconectar",
|
|
643
685
|
"ag.noBackend":
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
"b.down": "{n} 件停止",
|
|
51
51
|
"act.pin": "ピン留め",
|
|
52
52
|
"act.unpin": "ピン留めを外す",
|
|
53
|
+
"act.visit": "開く",
|
|
54
|
+
"act.visitX": "{s} を開く(新しいタブ)",
|
|
53
55
|
"act.pinX": "{s} をピン留め",
|
|
54
56
|
"act.unpinX": "{s} のピン留めを外す",
|
|
55
57
|
"toast.pinned": "{s} をピン留めしました",
|
|
@@ -101,6 +103,8 @@
|
|
|
101
103
|
"kind.app": "アプリ",
|
|
102
104
|
"kind.action": "操作",
|
|
103
105
|
"kind.setting": "設定",
|
|
106
|
+
"kind.workspace": "ワークスペース",
|
|
107
|
+
"pal.goWs": "ワークスペース {ws} へ移動",
|
|
104
108
|
"pal.repos": "{n} 個のリポジトリ",
|
|
105
109
|
"pal.restart": "{s} を再起動",
|
|
106
110
|
"pal.results": "結果",
|
|
@@ -242,12 +246,18 @@
|
|
|
242
246
|
"err.signin.title": "{name} にサインイン",
|
|
243
247
|
"err.signin.where":
|
|
244
248
|
"別のコンピューターから {name} を開いています。{name} は {host} で動いているため、あなた本人であることを確認する必要があります。",
|
|
245
|
-
"err.signin.
|
|
246
|
-
"
|
|
247
|
-
"err.signin.
|
|
248
|
-
"
|
|
249
|
+
"err.signin.ghStep1":
|
|
250
|
+
"{host} で {cmd} を実行するか、そちらで 設定 › アクセス を使います。",
|
|
251
|
+
"err.signin.ghStep2":
|
|
252
|
+
"コードが表示されます。github.com/login/device で入力して承認します。",
|
|
253
|
+
"err.signin.ghStep3":
|
|
254
|
+
"ここに戻り、同じ GitHub アカウントで「GitHub でサインイン」を押します。",
|
|
255
|
+
"err.signin.ghOwner": "オーナー: @{login}",
|
|
256
|
+
"err.signin.ghOnly":
|
|
257
|
+
"この方法でサインインできるのはオーナーの GitHub アカウントだけです。それ以外の人は「このコンピューターを承認」を使ってください。",
|
|
258
|
+
"err.signin.ghLearn": "詳しく見る",
|
|
249
259
|
"err.signin.githubRefused":
|
|
250
|
-
"@{login} はこの {name}
|
|
260
|
+
"@{login} はこの {name} のオーナーではありません。GitHub でサインインを使えるのはオーナーのアカウントだけです。代わりに「このコンピューターを承認」を使ってください。",
|
|
251
261
|
"err.signin.approveOr": "または、このコンピューターを承認",
|
|
252
262
|
"err.signin.approve": "このコンピューターを承認",
|
|
253
263
|
"err.signin.step1":
|
|
@@ -473,6 +483,34 @@
|
|
|
473
483
|
"ag.bring.agent": "エージェント {id}",
|
|
474
484
|
"ag.bring.note":
|
|
475
485
|
"実行していたフォルダーで続けます。ここからは Den のツールとガードが適用されます。ペインを閉じても実行は続きます。先にほかのターミナルで閉じてください。Den が開いていると判断できるのは、直近 1 分以内に書き込まれた会話だけです。",
|
|
486
|
+
"ag.keep.model": "会話の {m} をそのまま使います。",
|
|
487
|
+
"ag.keep.modelNew": "これからは {m} で動きます。",
|
|
488
|
+
"ag.keep.from":
|
|
489
|
+
"これからは {p} として働きます: その役割、権限({mode})、Den のツールとガード。履歴はそのまま残ります。",
|
|
490
|
+
"ag.keep.lost":
|
|
491
|
+
"使えなくなるもの: {s}。Den はエージェントを Den のツールだけで動かします。",
|
|
492
|
+
"ag.keep.opt": "{m} のまま",
|
|
493
|
+
"ag.keep.optNone": "今のモデルのまま",
|
|
494
|
+
"ag.keep.advNote":
|
|
495
|
+
"ここで別のモデルを選ばない限り、今のモデルのままです。権限はペルソナから来ます。",
|
|
496
|
+
"ag.pc.item": "ペルソナを変更…",
|
|
497
|
+
"ag.pc.title": "{id} のペルソナを変更",
|
|
498
|
+
"ag.pc.note":
|
|
499
|
+
"Den はいったん停止し、同じ会話を新しいペルソナで再開します。ID、名前、フォルダー、タブはそのままです。",
|
|
500
|
+
"ag.pc.go": "ペルソナを変更",
|
|
501
|
+
"ag.pc.going": "変更中…",
|
|
502
|
+
"ag.pc.current": "現在",
|
|
503
|
+
"ag.pc.busy":
|
|
504
|
+
"作業中です。このターンの後に変更しますか、それとも今すぐ停止しますか?",
|
|
505
|
+
"ag.pc.after": "このターンの後",
|
|
506
|
+
"ag.pc.now": "今すぐ停止",
|
|
507
|
+
"ag.pc.fresh":
|
|
508
|
+
"{id} は {p} になりました。まだ再開するものがないため、新しく開始しました(会話 ID、フォルダー、範囲は同じ)",
|
|
509
|
+
"ag.freshLine":
|
|
510
|
+
"まだ再開するものがないため、新しく開始しました(会話 ID、フォルダー、範囲は同じ)",
|
|
511
|
+
"ag.pc.done": "{id} は {p} になりました",
|
|
512
|
+
"ag.pc.pending": "このターンが終わると {id} は {p} になります",
|
|
513
|
+
"ag.more": "{id} のその他の操作",
|
|
476
514
|
"ag.bring.go": "取り込む",
|
|
477
515
|
"ag.bring.going": "取り込み中…",
|
|
478
516
|
"ag.bring.done": "{id} として取り込みました",
|
|
@@ -621,7 +659,11 @@
|
|
|
621
659
|
"ag.revokedTip":
|
|
622
660
|
"トークンが失効しました(web logout --all)。新しいトークンを渡すには停止して再開するか、もう一度開始してください",
|
|
623
661
|
"ag.ended": "このセッションは終了しました。記録は再開か削除まで残ります。",
|
|
662
|
+
"ag.switched": "{p} に切り替えました",
|
|
624
663
|
"ag.exitedLine": "[セッションが終了しました{code}]",
|
|
664
|
+
"ag.refusedUnknown": "サーバーがターミナルを拒否しました",
|
|
665
|
+
"ag.refusedStatus": "サーバーがターミナルを拒否しました({status})",
|
|
666
|
+
"ag.goWs": "ワークスペースで {ws} を開く",
|
|
625
667
|
"ag.lost": "[切断: {why}]",
|
|
626
668
|
"ag.reconnect": "再接続",
|
|
627
669
|
"ag.noBackend":
|
|
@@ -290,6 +290,7 @@
|
|
|
290
290
|
}
|
|
291
291
|
.module-picker-dropdown {
|
|
292
292
|
display: none;
|
|
293
|
+
padding: 0;
|
|
293
294
|
position: absolute;
|
|
294
295
|
top: calc(100% + 4px);
|
|
295
296
|
left: 0;
|
|
@@ -1568,6 +1569,11 @@
|
|
|
1568
1569
|
e.stopPropagation();
|
|
1569
1570
|
const open = pickerDropdown.classList.toggle("open");
|
|
1570
1571
|
pickerBtn.classList.toggle("open", open);
|
|
1572
|
+
// 0.4.17: in the top layer, so the output pane can't cover it.
|
|
1573
|
+
if (open)
|
|
1574
|
+
window.denShell?.topLayer?.show(pickerDropdown, pickerBtn, {
|
|
1575
|
+
gap: 4,
|
|
1576
|
+
});
|
|
1571
1577
|
});
|
|
1572
1578
|
document.addEventListener("click", () => {
|
|
1573
1579
|
pickerDropdown.classList.remove("open");
|
|
@@ -1168,6 +1168,11 @@
|
|
|
1168
1168
|
}
|
|
1169
1169
|
.tablewrap {
|
|
1170
1170
|
overflow-x: auto;
|
|
1171
|
+
/* 0.4.17: in a short window (under ~620 px tall) the column's other
|
|
1172
|
+
parts squeezed it to nothing; it keeps room for rows and scrolls. */
|
|
1173
|
+
min-height: 200px;
|
|
1174
|
+
flex-shrink: 0;
|
|
1175
|
+
overflow-y: auto;
|
|
1171
1176
|
border: 1px solid var(--border);
|
|
1172
1177
|
border-radius: var(--radius);
|
|
1173
1178
|
background: var(--surface);
|
|
@@ -1285,6 +1290,9 @@
|
|
|
1285
1290
|
.tbl-health.ok {
|
|
1286
1291
|
color: var(--ok);
|
|
1287
1292
|
}
|
|
1293
|
+
.tbl-up:not(:empty) {
|
|
1294
|
+
margin-left: 8px;
|
|
1295
|
+
}
|
|
1288
1296
|
.tbl-muted {
|
|
1289
1297
|
color: var(--muted);
|
|
1290
1298
|
font-size: 12.5px;
|
|
@@ -1330,6 +1338,46 @@
|
|
|
1330
1338
|
.tbl-acts .tbl-more-sp {
|
|
1331
1339
|
display: inline-block;
|
|
1332
1340
|
}
|
|
1341
|
+
/* Visit ↗ (0.4.17): before Start, so Start/Stop/Restart/Logs and ⋯
|
|
1342
|
+
still line up across rows whether a service has a URL or not. */
|
|
1343
|
+
.tbl-acts .tbl-visit {
|
|
1344
|
+
flex: none;
|
|
1345
|
+
padding-left: 6px;
|
|
1346
|
+
padding-right: 6px;
|
|
1347
|
+
text-decoration: none;
|
|
1348
|
+
}
|
|
1349
|
+
/* The table never scrolls sideways: in a window under 1600px wide
|
|
1350
|
+
(the workspace list beside it) Visit is its ↗; its name and title
|
|
1351
|
+
still say Visit. */
|
|
1352
|
+
@media (max-width: 1599px) {
|
|
1353
|
+
.tbl-visit .tbl-visit-w {
|
|
1354
|
+
display: none;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
table.svc a.repo-name {
|
|
1358
|
+
text-decoration: underline;
|
|
1359
|
+
text-decoration-color: var(--border-strong);
|
|
1360
|
+
text-underline-offset: 2px;
|
|
1361
|
+
}
|
|
1362
|
+
table.svc a.repo-name:hover {
|
|
1363
|
+
text-decoration-color: currentColor;
|
|
1364
|
+
}
|
|
1365
|
+
/* The favourite star (0.4.17): the shell's pins, as on Services. */
|
|
1366
|
+
.tbl-star {
|
|
1367
|
+
background: none;
|
|
1368
|
+
border: 1px solid transparent;
|
|
1369
|
+
border-radius: var(--radius-button);
|
|
1370
|
+
color: var(--faint);
|
|
1371
|
+
cursor: pointer;
|
|
1372
|
+
font-size: 13px;
|
|
1373
|
+
line-height: 1.2;
|
|
1374
|
+
padding: 0 2px;
|
|
1375
|
+
}
|
|
1376
|
+
.tbl-star:hover,
|
|
1377
|
+
.tbl-star[aria-pressed="true"] {
|
|
1378
|
+
color: var(--accent-text);
|
|
1379
|
+
background: var(--sunken);
|
|
1380
|
+
}
|
|
1333
1381
|
.tbl-btn:disabled {
|
|
1334
1382
|
opacity: 0.45;
|
|
1335
1383
|
cursor: not-allowed;
|
|
@@ -1769,7 +1817,11 @@
|
|
|
1769
1817
|
.querySelectorAll(".ws-menu-dropdown.open")
|
|
1770
1818
|
.forEach((d) => d.classList.remove("open"));
|
|
1771
1819
|
const menu = document.getElementById(menuBtn.dataset.menuId);
|
|
1772
|
-
menu?.classList.toggle("open")
|
|
1820
|
+
if (menu?.classList.toggle("open"))
|
|
1821
|
+
window.denShell?.topLayer?.show(menu, menuBtn, {
|
|
1822
|
+
align: "right",
|
|
1823
|
+
gap: 2,
|
|
1824
|
+
});
|
|
1773
1825
|
return;
|
|
1774
1826
|
}
|
|
1775
1827
|
// ↗ open in editor
|
|
@@ -1858,8 +1910,16 @@
|
|
|
1858
1910
|
if (d.id !== btn.dataset.menuId) d.style.display = "none";
|
|
1859
1911
|
});
|
|
1860
1912
|
const menu = document.getElementById(btn.dataset.menuId);
|
|
1861
|
-
if (menu)
|
|
1862
|
-
|
|
1913
|
+
if (menu) {
|
|
1914
|
+
const show = menu.style.display === "none";
|
|
1915
|
+
menu.style.display = show ? "" : "none";
|
|
1916
|
+
// 0.4.17: in the top layer, so no row or card clips it.
|
|
1917
|
+
if (show)
|
|
1918
|
+
window.denShell?.topLayer?.show(menu, btn, {
|
|
1919
|
+
align: "right",
|
|
1920
|
+
gap: 3,
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1863
1923
|
e.stopPropagation();
|
|
1864
1924
|
return;
|
|
1865
1925
|
}
|
|
@@ -2102,7 +2162,7 @@
|
|
|
2102
2162
|
<th scope="col">Health</th>
|
|
2103
2163
|
<th scope="col">Branch</th>
|
|
2104
2164
|
<th scope="col">Mode</th>
|
|
2105
|
-
<th scope="col">Last run</th>
|
|
2165
|
+
<th scope="col">Last test run</th>
|
|
2106
2166
|
<th scope="col"><span class="tbl-sr">Actions</span></th>
|
|
2107
2167
|
</tr></thead>
|
|
2108
2168
|
</table>
|
|
@@ -2272,22 +2332,60 @@
|
|
|
2272
2332
|
return [{ key: "", title: null, rows }];
|
|
2273
2333
|
}
|
|
2274
2334
|
|
|
2275
|
-
|
|
2335
|
+
// 0.4.17: each repo's last test run, from the server's run history
|
|
2336
|
+
// (every browser's runs, kept across restarts), and when its
|
|
2337
|
+
// service started (the daemons list), shown apart from it.
|
|
2338
|
+
let _lastRuns = {};
|
|
2339
|
+
let _upSince = new Map();
|
|
2340
|
+
async function loadLastRuns() {
|
|
2341
|
+
const [last, daemons] = await Promise.all([
|
|
2342
|
+
fetch("/api/runs/last")
|
|
2343
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
2344
|
+
.catch(() => null),
|
|
2345
|
+
fetch("/api/service/daemons")
|
|
2346
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
2347
|
+
.catch(() => []),
|
|
2348
|
+
]);
|
|
2349
|
+
_lastRuns = last?.repos ?? {};
|
|
2350
|
+
_upSince = new Map();
|
|
2351
|
+
for (const d of Array.isArray(daemons) ? daemons : []) {
|
|
2352
|
+
const at = Date.parse(d.startedAt);
|
|
2353
|
+
if (!d.repoName || !Number.isFinite(at)) continue;
|
|
2354
|
+
const cur = _upSince.get(d.repoName);
|
|
2355
|
+
if (cur === undefined || at < cur) _upSince.set(d.repoName, at);
|
|
2356
|
+
}
|
|
2357
|
+
paintLastRuns();
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
/** A repo's last test run: "✓ 12:03", "✗ failed 12:03", "running…", "—". */
|
|
2276
2361
|
function lastRunText(repo) {
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
);
|
|
2281
|
-
const r = mine.sort((a, b) => (b.ranAt ?? 0) - (a.ranAt ?? 0))[0];
|
|
2282
|
-
if (!r || (!r.ranAt && !r.running)) return { text: "—", cls: "" };
|
|
2283
|
-
if (r.running) return { text: "running…", cls: "" };
|
|
2362
|
+
const r = _lastRuns[repo];
|
|
2363
|
+
if (!r) return { text: "—", cls: "", title: "" };
|
|
2364
|
+
const title = [r.pillar, r.env].filter(Boolean).join(" · ");
|
|
2365
|
+
if (r.running) return { text: "running…", cls: "", title };
|
|
2284
2366
|
const t = new Intl.DateTimeFormat(undefined, {
|
|
2285
2367
|
hour: "2-digit",
|
|
2286
2368
|
minute: "2-digit",
|
|
2287
|
-
}).format(new Date(r.endedAt ?? r.
|
|
2288
|
-
return r.
|
|
2289
|
-
? { text:
|
|
2290
|
-
: { text:
|
|
2369
|
+
}).format(new Date(r.endedAt ?? r.startedAt));
|
|
2370
|
+
return r.exitCode === 0
|
|
2371
|
+
? { text: `✓ ${t}`, cls: "ok", title }
|
|
2372
|
+
: { text: `✗ failed ${t}`, cls: "bad", title };
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/** How long a repo's service has run: "up 2h", or "" when it doesn't. */
|
|
2376
|
+
function upText(repo) {
|
|
2377
|
+
const at = _upSince.get(repo);
|
|
2378
|
+
if (at === undefined) return "";
|
|
2379
|
+
const s = Math.max(0, Math.round((Date.now() - at) / 1000));
|
|
2380
|
+
const d =
|
|
2381
|
+
s < 60
|
|
2382
|
+
? `${s}s`
|
|
2383
|
+
: s < 3600
|
|
2384
|
+
? `${Math.floor(s / 60)}m`
|
|
2385
|
+
: s < 86400
|
|
2386
|
+
? `${Math.floor(s / 3600)}h`
|
|
2387
|
+
: `${Math.floor(s / 86400)}d`;
|
|
2388
|
+
return `up ${d}`;
|
|
2291
2389
|
}
|
|
2292
2390
|
|
|
2293
2391
|
/** A health cell: the dot and the rollup, or the failing check. */
|
|
@@ -2335,6 +2433,47 @@
|
|
|
2335
2433
|
);
|
|
2336
2434
|
}
|
|
2337
2435
|
|
|
2436
|
+
/** A service's URL to visit (its edge URL if routed, else its local one), or null. */
|
|
2437
|
+
function visitUrl(svc) {
|
|
2438
|
+
const u = svc?.open;
|
|
2439
|
+
return typeof u === "string" && /^https?:\/\//.test(u) ? u : null;
|
|
2440
|
+
}
|
|
2441
|
+
/** The shell's words (its language), else English. */
|
|
2442
|
+
function shT(key, vars, en) {
|
|
2443
|
+
const v = window.denShell?.t?.(key, vars);
|
|
2444
|
+
return v && v !== key ? v : en;
|
|
2445
|
+
}
|
|
2446
|
+
/** Visit ↗ (0.4.17): opens the service in a new tab; nothing without a URL. */
|
|
2447
|
+
function visitBtn(svc, name) {
|
|
2448
|
+
const url = visitUrl(svc);
|
|
2449
|
+
if (!url) return "";
|
|
2450
|
+
const label = shT(
|
|
2451
|
+
"act.visitX",
|
|
2452
|
+
{ s: name },
|
|
2453
|
+
`Visit ${name} (opens a new tab)`,
|
|
2454
|
+
);
|
|
2455
|
+
return `<a class="den-btn sm tbl-btn tbl-visit" href="${escHtml(url)}" target="_blank" rel="noopener" aria-label="${escHtml(label)}" title="${escHtml(url)}" data-k="visit:${escHtml(name)}"><span class="tbl-visit-w">${escHtml(shT("act.visit", {}, "Visit"))} </span>↗</a>`;
|
|
2456
|
+
}
|
|
2457
|
+
/** A row's name: a link to visit when the service has a URL. */
|
|
2458
|
+
function nameLink(svc, text) {
|
|
2459
|
+
const url = visitUrl(svc);
|
|
2460
|
+
return url
|
|
2461
|
+
? `<a class="repo-name" href="${escHtml(url)}" target="_blank" rel="noopener" title="${escHtml(shT("act.visitX", { s: url }, `Visit ${url} (opens a new tab)`))}">${escHtml(text)}</a>`
|
|
2462
|
+
: `<span class="repo-name">${escHtml(text)}</span>`;
|
|
2463
|
+
}
|
|
2464
|
+
/** The favourite star (0.4.17): the shell's pins, the same store as Services. */
|
|
2465
|
+
function starBtn(id) {
|
|
2466
|
+
const sh = window.denShell;
|
|
2467
|
+
if (!sh?.isPinned) return "";
|
|
2468
|
+
const on = sh.isPinned(id);
|
|
2469
|
+
const label = shT(
|
|
2470
|
+
on ? "act.unpinX" : "act.pinX",
|
|
2471
|
+
{ s: id },
|
|
2472
|
+
`${on ? "Unpin" : "Pin"} ${id}`,
|
|
2473
|
+
);
|
|
2474
|
+
return `<button type="button" class="tbl-star" data-pin-toggle="${escHtml(id)}" aria-pressed="${on ? "true" : "false"}" aria-label="${escHtml(label)}" title="${escHtml(label)}" data-k="star:${escHtml(id)}">${on ? "★" : "☆"}</button>`;
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2338
2477
|
/** Action buttons; each one's name says what and on which repo or service. */
|
|
2339
2478
|
function actBtns(id, o) {
|
|
2340
2479
|
const b = (act, label, name, extra = "") =>
|
|
@@ -2407,12 +2546,12 @@
|
|
|
2407
2546
|
? `<span class="tbl-mode"><span class="badge ${escHtml(mode)}">${escHtml(mode)}</span><button type="button" class="tbl-switch" data-action="tbl-mode" data-repo="${escHtml(repo)}" data-mode="${switchTo}" aria-label="${escHtml(repo)}: use ${mode === "managed" ? "local" : "managed"}" data-k="mode:${escHtml(repo)}">use ${mode === "managed" ? "local" : "managed"}</button></span>`
|
|
2408
2547
|
: "";
|
|
2409
2548
|
const head = `<tr class="repo${r.problem ? " prob" : ""}" data-repo="${escHtml(repo)}">
|
|
2410
|
-
<th scope="row">${exp}
|
|
2411
|
-
<td>${repoHealth(r)}</td>
|
|
2549
|
+
<th scope="row">${exp}${nameLink(primary, repo)} ${starBtn(repo)}${badges}</th>
|
|
2550
|
+
<td>${repoHealth(r)}<span class="tbl-muted tbl-up" data-up="${escHtml(repo)}">${escHtml(upText(repo))}</span></td>
|
|
2412
2551
|
<td class="tbl-mono tbl-muted">${escHtml(primary?.branch ?? "")}</td>
|
|
2413
2552
|
<td>${modeCell}</td>
|
|
2414
|
-
<td class="tbl-muted${run.cls ? " " + run.cls : ""}" data-last="${escHtml(repo)}">${escHtml(run.text)}</td>
|
|
2415
|
-
<td><div class="tbl-acts">${actBtns(repo, { up: !!startOf(repo).length, restart: !!startOf(repo).length })}${repoMenu(r)}</div></td>
|
|
2553
|
+
<td class="tbl-muted${run.cls ? " " + run.cls : ""}" data-last="${escHtml(repo)}" title="${escHtml(run.title)}">${escHtml(run.text)}</td>
|
|
2554
|
+
<td><div class="tbl-acts">${visitBtn(primary, repo)}${actBtns(repo, { up: !!startOf(repo).length, restart: !!startOf(repo).length })}${repoMenu(r)}</div></td>
|
|
2416
2555
|
</tr>`;
|
|
2417
2556
|
const fixes = r.fixes
|
|
2418
2557
|
.map(
|
|
@@ -2434,10 +2573,10 @@
|
|
|
2434
2573
|
s,
|
|
2435
2574
|
i,
|
|
2436
2575
|
) => `<tr class="sub" id="${subIds[i]}" data-repo="${escHtml(repo)}" data-id="${escHtml(s.id)}"${shown(s) ? "" : " hidden"}>
|
|
2437
|
-
<th scope="row"
|
|
2576
|
+
<th scope="row">${nameLink(s, s.name)} ${starBtn(s.id)}${s.entry ? ' <span class="badge">entry</span>' : ""}</th>
|
|
2438
2577
|
<td>${!_lastHealthAt ? CHECKING() : healthCell(status(s), s.health.ok === false ? checkLineOf(s) : healthLine(s))}</td>
|
|
2439
2578
|
<td></td><td></td><td></td>
|
|
2440
|
-
<td><div class="tbl-acts">${actBtns(s.id, { up: s.commands.includes("up"), restart: s.commands.includes("up") || s.commands.includes("restart") })}<span class="tbl-more-sp" aria-hidden="true"></span></div></td>
|
|
2579
|
+
<td><div class="tbl-acts">${visitBtn(s, s.id)}${actBtns(s.id, { up: s.commands.includes("up"), restart: s.commands.includes("up") || s.commands.includes("restart") })}<span class="tbl-more-sp" aria-hidden="true"></span></div></td>
|
|
2441
2580
|
</tr>`,
|
|
2442
2581
|
)
|
|
2443
2582
|
.join("")
|
|
@@ -2516,13 +2655,17 @@
|
|
|
2516
2655
|
if (keep)
|
|
2517
2656
|
table.querySelector(`[data-k="${CSS.escape(keep)}"]`)?.focus();
|
|
2518
2657
|
}
|
|
2519
|
-
/** Only the Last run cells (the
|
|
2658
|
+
/** Only the Last test run and "up" cells (the history changed). */
|
|
2520
2659
|
function paintLastRuns() {
|
|
2521
2660
|
document.querySelectorAll("#repo-table [data-last]").forEach((td) => {
|
|
2522
2661
|
const run = lastRunText(td.dataset.last);
|
|
2523
2662
|
td.textContent = run.text;
|
|
2663
|
+
td.title = run.title;
|
|
2524
2664
|
td.className = "tbl-muted" + (run.cls ? " " + run.cls : "");
|
|
2525
2665
|
});
|
|
2666
|
+
document.querySelectorAll("#repo-table [data-up]").forEach((el) => {
|
|
2667
|
+
el.textContent = upText(el.dataset.up);
|
|
2668
|
+
});
|
|
2526
2669
|
}
|
|
2527
2670
|
|
|
2528
2671
|
/**
|
|
@@ -2581,6 +2724,14 @@
|
|
|
2581
2724
|
setTableGroup(g.dataset.group);
|
|
2582
2725
|
return;
|
|
2583
2726
|
}
|
|
2727
|
+
const star = e.target.closest("#repo-table [data-pin-toggle]");
|
|
2728
|
+
if (star) {
|
|
2729
|
+
const sh = window.denShell;
|
|
2730
|
+
const id = star.dataset.pinToggle;
|
|
2731
|
+
if (sh?.isPinned(id)) sh.unpin(id);
|
|
2732
|
+
else sh?.pin(id);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2584
2735
|
const b = e.target.closest("[data-action^='tbl-']");
|
|
2585
2736
|
if (!b || b.disabled) return;
|
|
2586
2737
|
const a = b.dataset.action;
|
|
@@ -2641,8 +2792,20 @@
|
|
|
2641
2792
|
const v = localStorage.getItem(LS_GROUP);
|
|
2642
2793
|
if (["none", "app", "org"].includes(v)) _tblGroup = v;
|
|
2643
2794
|
} catch {}
|
|
2644
|
-
// The Last run column
|
|
2645
|
-
|
|
2795
|
+
// The Last test run column: the server's history, read now, when a
|
|
2796
|
+
// run here changes, and every 30 s for runs from elsewhere.
|
|
2797
|
+
void loadLastRuns();
|
|
2798
|
+
// The dock changes on every line of a run: read at most every 2 s.
|
|
2799
|
+
let _lastRunsSoon = null;
|
|
2800
|
+
window.denShell?.runs?.onChange?.(() => {
|
|
2801
|
+
_lastRunsSoon ??= setTimeout(() => {
|
|
2802
|
+
_lastRunsSoon = null;
|
|
2803
|
+
void loadLastRuns();
|
|
2804
|
+
}, 2_000);
|
|
2805
|
+
});
|
|
2806
|
+
setInterval(() => {
|
|
2807
|
+
if (!document.hidden) void loadLastRuns();
|
|
2808
|
+
}, 30_000);
|
|
2646
2809
|
|
|
2647
2810
|
// ── Tab management (one persistent tab per service address) ───────────────
|
|
2648
2811
|
//
|
|
@@ -181,6 +181,17 @@ dialog.pe-dlg::backdrop {
|
|
|
181
181
|
.helpl.warn {
|
|
182
182
|
color: var(--warn);
|
|
183
183
|
}
|
|
184
|
+
/* AG-17.1c, AG-18: what a persona changes, under the list. */
|
|
185
|
+
.den-keep {
|
|
186
|
+
margin: 8px 0 0;
|
|
187
|
+
padding-left: 18px;
|
|
188
|
+
font-size: 12px;
|
|
189
|
+
line-height: 1.45;
|
|
190
|
+
color: var(--muted);
|
|
191
|
+
}
|
|
192
|
+
.den-keep li + li {
|
|
193
|
+
margin-top: 2px;
|
|
194
|
+
}
|
|
184
195
|
.faint {
|
|
185
196
|
color: var(--faint);
|
|
186
197
|
}
|
|
@@ -134,6 +134,10 @@
|
|
|
134
134
|
font-size: 11px;
|
|
135
135
|
color: var(--muted);
|
|
136
136
|
}
|
|
137
|
+
.view-note {
|
|
138
|
+
color: var(--muted);
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
}
|
|
137
141
|
#status-text {
|
|
138
142
|
font-size: 11px;
|
|
139
143
|
color: var(--muted);
|
|
@@ -678,6 +682,7 @@
|
|
|
678
682
|
⟳ Refresh
|
|
679
683
|
</button>
|
|
680
684
|
<span id="status-text">idle</span>
|
|
685
|
+
<span id="view-note"></span>
|
|
681
686
|
<div class="h-spacer" style="flex: 1"></div>
|
|
682
687
|
<button class="btn" id="waves-btn" title="Edit wave assignment">
|
|
683
688
|
Waves…
|
|
@@ -735,6 +740,7 @@
|
|
|
735
740
|
refreshHealth,
|
|
736
741
|
initWorkspacePill,
|
|
737
742
|
} from "/header.js";
|
|
743
|
+
import { viewNoteHtml, viewRepos, viewScope } from "/view-scope.js";
|
|
738
744
|
|
|
739
745
|
// The header's dots report LOCAL service health here: #den-shell pins
|
|
740
746
|
// data-health-env="local" (shell.js). The env picker is the shell's
|
|
@@ -819,6 +825,15 @@
|
|
|
819
825
|
.replace(/>/g, ">")
|
|
820
826
|
.replace(/"/g, """);
|
|
821
827
|
|
|
828
|
+
// 0.4.17: the repos follow the top bar's View (Org and Workspace),
|
|
829
|
+
// as on Environments: no picker of this page's own (view-scope.js).
|
|
830
|
+
let viewIn = null;
|
|
831
|
+
const viewReady = viewScope().then(async (v) => {
|
|
832
|
+
const note = document.getElementById("view-note");
|
|
833
|
+
if (note) note.innerHTML = viewNoteHtml(v);
|
|
834
|
+
viewIn = await viewRepos(v);
|
|
835
|
+
});
|
|
836
|
+
|
|
822
837
|
// ── Init ──────────────────────────────────────────────────────────────────
|
|
823
838
|
async function init() {
|
|
824
839
|
try {
|
|
@@ -909,7 +924,10 @@
|
|
|
909
924
|
const data = await fetch(
|
|
910
925
|
`/api/release/status?env=${encodeURIComponent(state.env)}&refresh=${refresh ? 1 : 0}`,
|
|
911
926
|
).then((r) => r.json());
|
|
912
|
-
|
|
927
|
+
await viewReady;
|
|
928
|
+
state.rows = (data.rows || []).filter(
|
|
929
|
+
(r) => !viewIn || viewIn.has(r.repo),
|
|
930
|
+
);
|
|
913
931
|
setStatus(
|
|
914
932
|
data.error
|
|
915
933
|
? `error: ${data.error}`
|