@takagaki/cortex-decisions-viewer 0.4.67 → 0.4.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/build.js CHANGED
@@ -1130,6 +1130,11 @@ async function build(opts) {
1130
1130
  projectKey: opts.projectKey,
1131
1131
  }
1132
1132
  : undefined,
1133
+ // **URLとトークンが両方そろったときだけ出す。** 片方だけでは貼れるURLを作れないので、
1134
+ // 手順だけが出て「専用URLが空」という一番混乱する形になる(intake と同じ扱い)
1135
+ backlogWebhook: opts.webhookUrl && opts.webhookToken
1136
+ ? { url: opts.webhookUrl, token: opts.webhookToken }
1137
+ : undefined,
1133
1138
  secretKinds: (opts.secretKinds ?? "")
1134
1139
  .split(",")
1135
1140
  .map((s) => s.trim())
package/dist/cli.js CHANGED
@@ -20,6 +20,8 @@ program
20
20
  .option("--intake-token <token>", "投入フォームの共有秘密トークン(環境変数 INTAKE_TOKEN でも可)")
21
21
  .option("--project-key <key>", "案件キー(投入フォームのルーティングキー)")
22
22
  .option("--secret-kinds <kinds>", "鍵の入力欄を出す連携(カンマ区切り。例: figma,external-sources,backlog)")
23
+ .option("--webhook-url <url>", "Backlog Webhook 受信のLambda URL(環境変数 WEBHOOK_URL でも可)")
24
+ .option("--webhook-token <token>", "リアルタイム同期の**案件別**トークン(環境変数 WEBHOOK_TOKEN でも可。艦隊共通トークンを渡さないこと)")
23
25
  .action(async (dir, opts) => {
24
26
  try {
25
27
  const result = await (0, build_1.build)({
@@ -34,6 +36,8 @@ program
34
36
  intakeToken: opts.intakeToken ?? process.env.INTAKE_TOKEN,
35
37
  projectKey: opts.projectKey,
36
38
  secretKinds: opts.secretKinds,
39
+ webhookUrl: opts.webhookUrl ?? process.env.WEBHOOK_URL,
40
+ webhookToken: opts.webhookToken ?? process.env.WEBHOOK_TOKEN,
37
41
  });
38
42
  console.log(`✓ ${result.count} 件のレコードを ${result.outDir} にビルドしました`);
39
43
  }
package/dist/render.js CHANGED
@@ -891,7 +891,28 @@ a.ds-name:hover { text-decoration: underline; }
891
891
  .ds-note { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 6px 0 0; }
892
892
  .ds-sync { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; flex: 0 0 auto; }
893
893
  .ds-col { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
894
- .ds-keys { font-size: 12px; color: var(--muted); line-height: 1.6; }
894
+ /* 会議名に入れる目印。**この行が会議カードで一番強い。** 取り込まれるかどうかは
895
+ 会議名にこれが入っているかで決まるので、補足文の一つに埋もれさせない(#117) */
896
+ .ds-keys {
897
+ display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px;
898
+ margin: 4px 0 2px; padding: 8px 10px;
899
+ background: var(--accent-weak); border: 1px solid #c7d7fe; border-radius: 8px;
900
+ font-size: 13px; color: var(--text); line-height: 1.5;
901
+ }
902
+ .ds-keys-label { font-weight: 700; }
903
+ .ds-keys-code {
904
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
905
+ font-size: 14px; font-weight: 700; color: var(--accent);
906
+ background: #fff; border: 1px solid #c7d7fe; border-radius: 6px; padding: 1px 8px;
907
+ }
908
+ .ds-keys-or { font-size: 12px; color: var(--muted); }
909
+ /* 経過措置で登録された会議名。読み取り専用の記録なので、目印より弱く出す(#117) */
910
+ .ds-legacy { margin-top: 4px; font-size: 12px; color: var(--muted); line-height: 1.6; }
911
+ .ds-legacy-chips { display: flex; flex-wrap: wrap; gap: 4px 6px; margin-top: 3px; }
912
+ .ds-legacy-chip {
913
+ font-size: 11.5px; color: var(--muted);
914
+ background: #f4f5f7; border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px;
915
+ }
895
916
  .ds-drive-note { font-size: 12px; color: #6b7280; line-height: 1.6; }
896
917
  .ds-group { border: none; margin: 0; }
897
918
  .ds-group > summary {
@@ -1015,6 +1036,11 @@ a.ds-name:hover { text-decoration: underline; }
1015
1036
  .ds-toggle-msg.is-ok { color: #1a7f37; }
1016
1037
  .ds-toggle-msg.is-error { color: #b42318; }
1017
1038
  .badge.ds-notify { background: #eff6ff; color: #1e40af; border: 1px solid #bfdbfe; }
1039
+ /* 登録の由来(#118)。**薄く出す**——読み取りの可否や Gold 対象かより弱い情報で、
1040
+ ここが目立つと「除外」「未宣言」の判断のバッジと同じ強さに見えてしまう */
1041
+ .badge.ds-origin { background: transparent; color: var(--muted); border: 1px dashed var(--border); }
1042
+ /* 全組織が接続済みのときに、トークン入力欄の代わりに出す1行(#118) */
1043
+ .ds-all-connected { color: var(--muted); }
1018
1044
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
1019
1045
  .ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
1020
1046
  .ds-gate-ok { color: #1f7a45; }
@@ -1063,6 +1089,26 @@ a.ds-name:hover { text-decoration: underline; }
1063
1089
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
1064
1090
  padding: 18px; box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 10px;
1065
1091
  }
1092
+ /* リアルタイム同期の設定手順(#113)。番号付きなのは「上から順に1回やる」作業だから */
1093
+ .rt-steps { margin: 0; padding-left: 1.4em; color: var(--text); font-size: 13px; line-height: 1.9; }
1094
+ .rt-steps li { margin: 2px 0; }
1095
+ /* URLは長いので、入力欄を伸ばしてボタンを右に置く(折り返しても崩れないように wrap) */
1096
+ .rt-url-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
1097
+ .rt-url { flex: 1 1 260px; margin-bottom: 0; font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; }
1098
+ /* 稼働中の表示(#123)。緑は「動いている」の合図で、見出しの右に置いて先に目に入れる */
1099
+ .rt-live-badge {
1100
+ margin-left: auto; flex: 0 0 auto; padding: 3px 10px; border-radius: 999px;
1101
+ background: #e8f6ed; border: 1px solid #b7e0c6; color: #1c7a43;
1102
+ font-size: 12px; font-weight: 700; white-space: nowrap;
1103
+ }
1104
+ .rt-live-lines { display: flex; flex-direction: column; gap: 4px; }
1105
+ .rt-live-line { margin: 0; color: var(--text); font-size: 13px; font-weight: 600; }
1106
+ /* まだ受信していない接続先(複数接続のとき混在する)。稼働中の行と見分けが付く濃さに落とす */
1107
+ .rt-live-line.is-waiting { color: var(--muted); font-weight: 500; }
1108
+ /* 手順は畳んで残す(張り替え・再確認用)。開いたときだけ間隔を空ける */
1109
+ .rt-steps-fold > summary { cursor: pointer; color: var(--muted); font-size: 12px; }
1110
+ .rt-steps-fold[open] > summary { margin-bottom: 10px; }
1111
+ .rt-steps-fold[open] > *:not(summary) { margin-top: 10px; }
1066
1112
  .ops-card-head { display: flex; align-items: center; gap: 10px; }
1067
1113
  .ops-card-icon { font-size: 22px; line-height: 1; }
1068
1114
  .ops-card-title { font-size: 15px; font-weight: 700; color: var(--text); margin: 0; }
@@ -1672,6 +1718,22 @@ ${capabilityState.toString()}
1672
1718
  var mm = ("0" + d.getMinutes()).slice(-2);
1673
1719
  return (d.getMonth() + 1) + "/" + d.getDate() + " " + hh + ":" + mm;
1674
1720
  }
1721
+ /**
1722
+ * 「いつだったか」を相対で言う(#123)。読めない値は空文字(呼び側が出し分けを切る)。
1723
+ *
1724
+ * **1日以上前は絶対時刻に切り替える。** 「32日前」は数えないと日付にならないし、
1725
+ * そこまで空いていれば知りたいのは「いつ止まったか」のほう。
1726
+ * 未来の時刻(端末とサーバの時計ずれ)は「たった今」に丸める——「-3分前」を出さない。
1727
+ */
1728
+ function rtAgo(iso) {
1729
+ var d = new Date(iso);
1730
+ if (isNaN(d.getTime())) return "";
1731
+ var min = Math.floor((Date.now() - d.getTime()) / 60000);
1732
+ if (min < 1) return "たった今";
1733
+ if (min < 60) return min + "分前";
1734
+ if (min < 60 * 24) return Math.floor(min / 60) + "時間前";
1735
+ return dsFmtTime(iso);
1736
+ }
1675
1737
  // 名前表示: url があれば新しいタブで正本を開くリンクにする
1676
1738
  function dsNameEl(text, url) {
1677
1739
  if (url) return el("a", { class: "ds-name", href: url, target: "_blank", rel: "noopener", text: text });
@@ -1731,6 +1793,14 @@ ${capabilityState.toString()}
1731
1793
  */
1732
1794
  var LIVE_FLEET = null;
1733
1795
  var LIVE_FLEET_FAILED = false;
1796
+ /**
1797
+ * リアルタイム同期の可否(op=state の realtime。#113)。取得前・取得失敗時は null。
1798
+ *
1799
+ * **fleet とは別に持つ。** fleet は fleet-status.json(夜間バッチの成果物)の射影で、
1800
+ * こちらはルーティング索引(DynamoDB)を引いた結果。同じ入れ物に混ぜると、
1801
+ * 「いつ時点の情報か」の注記がどちらを指すのか説明できなくなる。
1802
+ */
1803
+ var LIVE_REALTIME = null;
1734
1804
  /**
1735
1805
  * 接続状況の値を引く。ライブがあればライブ、無ければ静的ビルドの値。
1736
1806
  * SITE 側はキー名が fleetGeneratedAt だけ異なる(他は fleet-status.json と同名)。
@@ -1881,43 +1951,46 @@ ${capabilityState.toString()}
1881
1951
  }
1882
1952
  var aliases = (s.kind === "会議" && s.ingestState === "on" && s.meetingAliases) || [];
1883
1953
  if (signal) {
1884
- col.appendChild(el("span", {
1954
+ // **カードで一番強い要素にする(#117)。** 実機で見たとき、目印の案内が
1955
+ // 別名の一覧や注記と同じ細字で並んでいて、「まず会議名にこれを入れる」が
1956
+ // 伝わらなかった。最初に決めた合図が唯一の真実なので、そこだけを太く出す。
1957
+ //
1958
+ // **括弧を付けた形でそのまま出す。** 語だけ見せると 【KC定例】 のように
1959
+ // 混ぜて書かれ、どこも一致せず未仕分けへ落ちる(実際に起きた)。
1960
+ col.appendChild(el("div", {
1885
1961
  class: "ds-keys",
1886
- text: "会議名に入れる目印: 【" + signal + "】 または [" + signal + "]",
1887
1962
  title: "会議名のどこかにこの括弧付きの目印が入っていると、この案件に振り分けられます(大小文字は問いません)。括弧の中に他の文字を混ぜると一致しません。",
1888
- }));
1963
+ }, [
1964
+ el("span", { class: "ds-keys-label", text: "会議名にこれを入れてください" }),
1965
+ el("code", { class: "ds-keys-code", text: "【" + signal + "】" }),
1966
+ el("span", { class: "ds-keys-or", text: "または" }),
1967
+ el("code", { class: "ds-keys-code", text: "[" + signal + "]" }),
1968
+ ]));
1889
1969
  }
1890
1970
  if (aliases.length) {
1891
- // **目印が無くても入る会議。** 既に走っていて改名を頼みにくい定例を、
1892
- // いまの名前のまま登録したもの。出さないと「なぜこれだけ入るのか」
1893
- // 「新しい定例をどこに足すのか」が画面から分からない。
1971
+ // **読み取り専用の記録として出す(#117)。** 目印が無くても入る会議=括弧運用へ
1972
+ // 移る前から走っていて、改名を頼めなかった定例。出さないと「なぜこれだけ入るのか」が
1973
+ // 画面から分からないので残すが、**操作は一切出さない**。
1894
1974
  //
1895
- // **1件ずつ外せるようにする。** 追加はできて削除はできない、だと
1896
- // 間違えて登録した名前(日付つきのファイル名など)を消す手段が無い
1897
- col.appendChild(el("span", {
1898
- class: "ds-keys",
1899
- text: "目印なしで取り込む会議:",
1900
- title: "この名前の会議は、目印が無くてもこの案件に振り分けられます(大小文字・前後の空白は問いません。ファイル名に使えない記号(コロン・スラッシュ等)は同じものとして扱います)。既に走っている定例を改名せずに取り込むための登録です。",
1901
- }));
1902
- var alist = el("div", { class: "ds-subitems" });
1975
+ // 以前は1件ずつ外せるようにしていた。実機確認でユーザーが「最初に決めたキー(合図)が
1976
+ // 唯一真実」と確定させたので、画面から触れる余地を無くす——別名を足したり外したり
1977
+ // できる限り、合図と別名のどちらが正なのかが人によって揺れ続ける。
1978
+ // 変更が要るときはリポジトリの ingest-config.json を直接編集する(Lambda も断る)。
1979
+ var legacy = el("div", { class: "ds-legacy" }, [
1980
+ el("span", {
1981
+ text: "経過措置で登録された会議名(目印が無くても取り込まれます。変更はリポジトリの ingest-config.json で行います)",
1982
+ }),
1983
+ ]);
1984
+ var chips = el("div", { class: "ds-legacy-chips" });
1903
1985
  aliases.forEach(function (a) {
1904
- var row = el("div", { class: "ds-subitem" }, [el("span", { class: "ds-name", text: a })]);
1905
- row.appendChild(removeButton({
1906
- label: "この会議を外す",
1907
- confirmLines: function () {
1908
- return [
1909
- "「" + a + "」を取り込み対象から外します。",
1910
- "",
1911
- "以後この名前の会議は、会議名に目印を入れない限り取り込まれません。",
1912
- "",
1913
- "外しますか?",
1914
- ];
1915
- },
1916
- build: function () { return { op: "remove-meeting-alias", alias: a }; },
1986
+ chips.appendChild(el("span", {
1987
+ class: "ds-legacy-chip",
1988
+ text: a,
1989
+ title: "この名前の会議は、目印が無くてもこの案件に振り分けられます(大小文字・前後の空白は問いません。ファイル名に使えない記号(コロン・スラッシュ等)は同じものとして扱います)。",
1917
1990
  }));
1918
- alist.appendChild(row);
1919
1991
  });
1920
- col.appendChild(alist);
1992
+ legacy.appendChild(chips);
1993
+ col.appendChild(legacy);
1921
1994
  }
1922
1995
  if (s.driveSync === false) {
1923
1996
  col.appendChild(el("span", { class: "ds-drive-note", text: "Drive自動同期は未設定(手動で置いたファイルの変換のみ)" }));
@@ -2062,10 +2135,40 @@ ${capabilityState.toString()}
2062
2135
 
2063
2136
  pushCap("開発", adders.querySelector('[data-adder="github"]'));
2064
2137
 
2138
+ /**
2139
+ * GitHub の読み取りトークンを入れる欄が要るか(#118)。
2140
+ *
2141
+ * トークンは **GitHub App が入っていない組織のフォールバック**でしかない。全組織が
2142
+ * App(か登録済みトークン)で読めている案件に入力欄を出すと、実機で言われたとおり
2143
+ * 「App に移行したのに、なぜトークンを求められるのか」になる。押す必要が無いものを
2144
+ * 押させるのは、未設定の警告を出し続けるのと同じ種類の誤りなので畳む。
2145
+ *
2146
+ * **判断材料が無いときは出す。** githubOwners は fleet-status.json 由来で、
2147
+ * 夜間バッチがまだ走っていない案件には無い。「情報が無い」を「接続済み」に寄せると、
2148
+ * **一番繋ぎたい立ち上げ直後の案件**から唯一の入口が消える。
2149
+ *
2150
+ * **unknown(確かめられなかった組織)も「要る」側に倒す。** 実態は none かもしれず、
2151
+ * 隠す方に倒すと直せなくなる(画面には組織ごとに「確認できませんでした」と出ている)。
2152
+ */
2153
+ function githubTokenNeeded() {
2154
+ var list = fleetVal("githubOwners") || [];
2155
+ if (!list.length) return true;
2156
+ return list.some(function (o) { return !o || (o.source !== "app" && o.source !== "token"); });
2157
+ }
2158
+
2065
2159
  // 連携の鍵は、その能力のところへ置く(画面下まで往復させない)
2066
2160
  var SECRET_CAP = { figma: "デザイン", backlog: "課題管理", "external-sources": "開発" };
2067
2161
  (SITE.secretKinds || []).forEach(function (k) {
2068
2162
  if (!SECRET_FORMS[k]) return;
2163
+ // 全組織が繋がっているなら、入力欄ではなく**そう言う1行**に畳む(#118)。
2164
+ // 何も出さないと「トークンはどこで入れるのか」が分からなくなる
2165
+ if (k === "external-sources" && !githubTokenNeeded()) {
2166
+ pushCap(SECRET_CAP[k], el("p", {
2167
+ class: "sec-how ds-all-connected",
2168
+ text: "すべての組織に接続済みです(GitHub App / 登録済みトークン)。読み取りトークンの登録は要りません。",
2169
+ }));
2170
+ return;
2171
+ }
2069
2172
  pushCap(SECRET_CAP[k], secretCard(k, SECRET_FORMS[k]));
2070
2173
  });
2071
2174
 
@@ -2242,7 +2345,11 @@ ${capabilityState.toString()}
2242
2345
  // 新規案件——**まさに何を入れれば繋がるのかを知りたい場面**——から、
2243
2346
  // 鍵が未設定であることの手がかりまで消える
2244
2347
  var sk = SECRET_OF_CAP[cap];
2245
- if (tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2348
+ // **GitHub は「トークンが無い」=「読めていない」ではない(#118)。** App で読めている
2349
+ // 案件に未設定の警告を出すと、直す必要が無いものを直させることになる。
2350
+ // 読めていない組織があるときは、この下の checks とタイルの状態(warn)が拾う
2351
+ var tokenIsFallback = sk === "external-sources" && !githubTokenNeeded();
2352
+ if (!tokenIsFallback && tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2246
2353
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
2247
2354
  }
2248
2355
  // セットアップの残りのうち、この能力のもの。**鍵の注記と同じ位置**に出して
@@ -2458,24 +2565,13 @@ ${capabilityState.toString()}
2458
2565
  // 宣言で隠すと、動いているのに説明が出ない状態になる。
2459
2566
  if (s.kind === "会議" && s.ingestState === "on") {
2460
2567
  box.appendChild(meetingHowTo());
2461
- // **別名の追加は、既に別名がある案件にだけ出す(#117)。**
2568
+ // **会議名の登録を画面から変える口は置かない(#117 第2弾)。**
2462
2569
  // 別名は括弧付きの合図運用へ移った際の経過措置で、既存案件のメンバーに定例の
2463
- // 改名を強いないために置いたもの。新規案件は合図1つだけで運用する。
2464
- // ここでフォームを出し続けると「新しい定例は名前を登録して足す」と読まれ、
2465
- // 畳むはずの仕組みが案件をまたいで増えていく(Lambda 側でも同じ条件で断る)。
2466
- if ((s.meetingAliases || []).length) {
2467
- box.appendChild(addForm({
2468
- title: "+ 会議名をそのまま取り込み対象にする",
2469
- placeholder: "例: GDO様 定例",
2470
- requiredNote: "会議名を入力してください。",
2471
- // 先頭の1文が「これは増やすものではない」を伝える。使い方の説明はその後
2472
- hint: "既存の定例を改名せずに拾うための経過措置の設定です。新しい会議は会議名に合図(【…】)を入れてください。ここに入れた名前と**完全に一致する**会議だけが取り込まれます(部分一致はしません)。カレンダーに出ている会議名だけを入れてください——日付や「- Gemini によるメモ」のような後ろに付く文字列を含めると、どの会議とも一致しません。Bot の招待は別途必要です。",
2473
- confirmLines: function (v) {
2474
- return ["この名前の会議を取り込み対象にします。", "", v, "", "追加しますか?"];
2475
- },
2476
- build: function (alias) { return { op: "add-meeting-alias", alias: alias }; },
2477
- }));
2478
- }
2570
+ // 改名を強いないために置いたもの。第1弾では「既に別名がある案件にだけ追加口を出す」
2571
+ // としたが、実機確認でユーザーが**「最初に決めたキー(合図)が唯一真実」**と確定させた。
2572
+ // 経過措置の案件にフォームを残すと、その案件の中では別名が現役の登録手段に見え続け、
2573
+ // 合図と別名のどちらが正なのかが人によって揺れる。画面は合図1つだけを案内し、
2574
+ // 別名は上のチップ(読み取り専用の記録)で見せるに留める。Lambda 側も add/remove とも断る。
2479
2575
  used = true;
2480
2576
  } else if (s.kind === "会議" && usable("会議", "google-meet")) {
2481
2577
  if (s.ingestState === "unset") {
@@ -2534,6 +2630,11 @@ ${capabilityState.toString()}
2534
2630
  },
2535
2631
  build: function (url) { return { op: "set-backlog-target", url: url }; },
2536
2632
  }));
2633
+ // **接続先の下に置く。** リアルタイム同期は「どのプロジェクトを見るか」が
2634
+ // 決まった後の話で、順序が逆だと、接続先を設定していない案件が先に
2635
+ // Webhook を貼りに行く(貼っても届かない)
2636
+ var rt = realtimeSyncCard();
2637
+ if (rt) box.appendChild(rt);
2537
2638
  used = true;
2538
2639
  }
2539
2640
 
@@ -2698,7 +2799,7 @@ ${capabilityState.toString()}
2698
2799
  wrap.appendChild(el("p", { text: "次の2つが揃った会議だけが取り込まれます。" }));
2699
2800
  var ul = el("ul", { class: "sec-how-list" });
2700
2801
  ul.appendChild(el("li", { text: "cortex-notetaker Bot が招待されている(定例はシリーズに1回でOK)" }));
2701
- ul.appendChild(el("li", { text: "会議名に上の語のどれかが含まれている" }));
2802
+ ul.appendChild(el("li", { text: "会議名に上の目印が入っている" }));
2702
2803
  wrap.appendChild(ul);
2703
2804
  wrap.appendChild(el("p", { text: "どちらか一方だけでは取り込まれません。" }));
2704
2805
  return wrap;
@@ -3087,6 +3188,18 @@ ${capabilityState.toString()}
3087
3188
  return e;
3088
3189
  }
3089
3190
 
3191
+ /**
3192
+ * 登録の由来の表示(#118)。値は fleet-status.json の origin
3193
+ * (explicit … 登録ファイルに手で書いた / derived … .gitmodules 等からの自動導出 / both)。
3194
+ * **知らない値には何も出さない**(生の内部値を顧客の画面に出さないため)。
3195
+ */
3196
+ var ORIGIN_LABEL = { explicit: "明示登録", derived: "自動検出", both: "明示登録+自動検出" };
3197
+ var ORIGIN_HELP = {
3198
+ explicit: "開発の登録ファイル(dev-sources.json)に登録されているリポジトリです。ここから登録を削除できます。",
3199
+ derived: ".gitmodules から自動で見つかったリポジトリです。登録ファイルに無いので削除はできません(読み取りを止めることはできます)。",
3200
+ both: "登録ファイルにも書かれていて、.gitmodules からも見つかっているリポジトリです。",
3201
+ };
3202
+
3090
3203
  function extSourceCard(s, meta) {
3091
3204
  var badges = el("div", { class: "ds-badges" });
3092
3205
  var pending = pendingGoldFor(s);
@@ -3100,6 +3213,17 @@ ${capabilityState.toString()}
3100
3213
  else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
3101
3214
  else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
3102
3215
  if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
3216
+ // 登録の由来(#118)。**「登録から削除」が出る/出ないの理由がこれ。** 導出されたリポは
3217
+ // 登録ファイルから消しても翌日また現れるので削除を出していないが、画面には
3218
+ // 「同じ見た目の行なのに、片方にだけ削除がある」としか映らず、毎回考え直すことになる。
3219
+ // GitHub の行だけに出す(削除の出し分けがあるのはここだけで、他は全部 explicit)
3220
+ if (String(s.type).indexOf("github") === 0 && ORIGIN_LABEL[s.origin]) {
3221
+ badges.appendChild(el("span", {
3222
+ class: "badge ds-origin",
3223
+ text: ORIGIN_LABEL[s.origin],
3224
+ title: ORIGIN_HELP[s.origin],
3225
+ }));
3226
+ }
3103
3227
  var gate = DS_GATE[s.gate] || DS_GATE["unknown"];
3104
3228
  var card = el("div", { class: "ds-card" }, [
3105
3229
  el("div", { class: "ds-card-main" }, [
@@ -3285,6 +3409,9 @@ ${capabilityState.toString()}
3285
3409
  // op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
3286
3410
  .then(function (res) { if (!res.ok) throw new Error("http " + res.status); return res.json(); })
3287
3411
  .then(function (j) {
3412
+ // **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
3413
+ // 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
3414
+ LIVE_REALTIME = (j && j.realtime) || null;
3288
3415
  if (!j || !j.fleet) throw new Error("no fleet");
3289
3416
  LIVE_FLEET = normalizeFleet(j.fleet);
3290
3417
  LIVE_FLEET_FAILED = false;
@@ -3325,6 +3452,121 @@ ${capabilityState.toString()}
3325
3452
  return null;
3326
3453
  }
3327
3454
 
3455
+ /**
3456
+ * リアルタイム同期(Backlog Webhook)の設定手順(#113)。
3457
+ *
3458
+ * **出すのは「手順どおり設定すれば実際に効く案件」だけ。** 判断は Lambda(op=state)が返す
3459
+ * realtime に委ねる——画面が持っている静的データからは「ルーティング索引に自分の行が
3460
+ * あるか」が分からないし、2箇所で判定すると片方だけがずれる。
3461
+ * - supported: false … 固定IP経由で同期している案件。**黙って欠けさせず**理由を1行出す
3462
+ * - ready: false … 索引にまだ行が無い。設定しても届かないので手順を出さない
3463
+ *
3464
+ * **URLに載せるトークンは案件別**(SITE.backlogWebhook)。艦隊共通トークンを載せると、
3465
+ * この画面を見られる人が全案件ぶんの偽イベントを投げられる(types.ts の backlogWebhook 参照)。
3466
+ *
3467
+ * **既に回っている案件には、手順より先に「動いている」ことを出す(#123)。** 設定手順は
3468
+ * まだ繋いでいない人のためのもので、繋ぎ終わった案件の担当者が知りたいのは
3469
+ * 「ちゃんと届いているか」。手順は畳んで残す(URLの再確認・張り替えに要る)。
3470
+ */
3471
+ function realtimeSyncCard() {
3472
+ var rt = LIVE_REALTIME;
3473
+ if (!rt) return null; // 取得前・取得失敗・Backlogを使わない案件
3474
+ if (rt.supported === false) {
3475
+ return el("p", {
3476
+ class: "sec-how",
3477
+ text: "この案件は固定IP経由でBacklogを同期しているため、リアルタイム同期(Webhook)には未対応です。反映は毎晩の同期のタイミングになります。",
3478
+ });
3479
+ }
3480
+ var wh = SITE.backlogWebhook;
3481
+ if (!rt.ready || !wh || !wh.url || !wh.token) return null;
3482
+ var url = wh.url + (wh.url.indexOf("?") === -1 ? "?" : "&") + "t=" + encodeURIComponent(wh.token);
3483
+ // 接続先ごとの受信実績(#123)。**配列で受ける**——1案件=複数 Backlog プロジェクトは
3484
+ // 確定した将来要件(#107)で、今日は必ず1件。**時刻が読めない値は稼働中に数えない**
3485
+ // ——「最終受信: NaN分前」を出すくらいなら、未設定と同じ手順の画面のほうがまだ読める
3486
+ var targets = (rt.targets || []).map(function (t) {
3487
+ return { projectKey: t && t.projectKey ? String(t.projectKey) : "", ago: t && t.lastEventAt ? rtAgo(t.lastEventAt) : "" };
3488
+ });
3489
+ var live = targets.some(function (t) { return t.ago; });
3490
+
3491
+ // URLは**選択できる形**で出す(コピーボタンが使えない環境でも手で取れるように)
3492
+ var field = el("input", { class: "sec-input rt-url", type: "text", readonly: "readonly", spellcheck: "false" });
3493
+ field.value = url;
3494
+ var copyBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "コピー" });
3495
+ var msg = el("div", { class: "ds-toggle-msg" });
3496
+ copyBtn.addEventListener("click", function () {
3497
+ var done = function () { msg.className = "ds-toggle-msg is-ok"; msg.textContent = "✓ コピーしました。"; };
3498
+ var fallback = function () {
3499
+ // clipboard API が無い/拒否された環境(http・古いブラウザ・権限拒否)。
3500
+ // **黙って何も起きないにしない**——選択まではしてやって、次の一手を伝える
3501
+ try {
3502
+ field.focus();
3503
+ field.select();
3504
+ if (document.execCommand && document.execCommand("copy")) return done();
3505
+ } catch (e) { /* 下の案内へ */ }
3506
+ msg.className = "ds-toggle-msg is-error";
3507
+ msg.textContent = "⚠ 自動コピーできませんでした。選択された文字列を手でコピーしてください。";
3508
+ };
3509
+ try {
3510
+ if (navigator.clipboard && navigator.clipboard.writeText) {
3511
+ navigator.clipboard.writeText(url).then(done, fallback);
3512
+ } else fallback();
3513
+ } catch (e) { fallback(); }
3514
+ });
3515
+
3516
+ // 手順ひとそろい(貼り付け先・URL・注意書き)。稼働中の案件ではこれを畳む
3517
+ var steps = [
3518
+ el("ol", { class: "rt-steps" }, [
3519
+ el("li", { text: "Backlog のプロジェクト設定 → インテグレーション → Webhook を開く" }),
3520
+ el("li", { text: "「WebHook URL」に下の案件専用URLを貼り付ける" }),
3521
+ el("li", { text: "通知イベントで「課題の追加」「課題の更新」「課題にコメント」等にチェックを入れて保存する" }),
3522
+ ]),
3523
+ el("div", { class: "rt-url-row" }, [field, copyBtn]),
3524
+ msg,
3525
+ // **この画面は顧客も見る。** URLに秘密が入っていることと、その扱いを書いておく
3526
+ el("p", { class: "sec-note", text: "このURLはこの案件専用です(他の案件には届きません)。秘密のトークンを含むので、貼り付け先は Backlog のこのプロジェクトの設定欄だけにしてください。" }),
3527
+ el("p", { class: "sec-how", text: "反映は受信から約1分後です(1分ごとのミニバッチでまとめて取り込みます)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。" }),
3528
+ ];
3529
+
3530
+ if (live) {
3531
+ // 稼働中。**先頭は状態、手順は畳む。**「畳んである中に何があるか」を summary に書く
3532
+ // ——ただの「詳細」だと、URLを張り替えたい人がここを開く手掛かりを持てない
3533
+ var det = el("details", { class: "rt-steps-fold" }, [
3534
+ el("summary", { text: "設定手順と案件専用URLを表示(URLの再確認・張り替え用)" }),
3535
+ ]);
3536
+ for (var i = 0; i < steps.length; i++) det.appendChild(steps[i]);
3537
+ // 接続先ごとに1行。**1件のときはプロジェクトキーを出さない**——どれのことかは
3538
+ // 自明なので、キーを添えると「他にもあるのか」と読ませる余計な情報になる。
3539
+ // 複数のときは、どの接続先の話かが分からないと行が読めないので必ず添える
3540
+ var lines = el("div", { class: "rt-live-lines" }, targets.map(function (t) {
3541
+ var label = targets.length > 1 && t.projectKey ? t.projectKey + ": " : "";
3542
+ // 受信実績がまだ無い接続先も**黙って落とさない**。1つでも動いていれば
3543
+ // カードは稼働中の形になるので、落とすと「増やした接続先が来ていない」に気づけない
3544
+ return el("p", {
3545
+ class: t.ago ? "rt-live-line" : "rt-live-line is-waiting",
3546
+ text: t.ago ? label + "最終受信: " + t.ago : label + "まだ受信していません",
3547
+ });
3548
+ }));
3549
+ return el("div", { class: "ops-card rt-card is-live" }, [
3550
+ el("div", { class: "ops-card-head" }, [
3551
+ el("span", { class: "ops-card-icon", text: "⚡" }),
3552
+ el("h3", { class: "ops-card-title", text: "リアルタイム同期" }),
3553
+ el("span", { class: "rt-live-badge", text: "✓ 稼働中" }),
3554
+ ]),
3555
+ lines,
3556
+ el("p", { class: "ops-card-desc", text: "Backlog の変更をすぐ取り込んでいます(毎晩の同期を待ちません)。設定は済んでいるので、このままお使いください。" }),
3557
+ det,
3558
+ ]);
3559
+ }
3560
+
3561
+ return el("div", { class: "ops-card rt-card" }, [
3562
+ el("div", { class: "ops-card-head" }, [
3563
+ el("span", { class: "ops-card-icon", text: "⚡" }),
3564
+ el("h3", { class: "ops-card-title", text: "リアルタイム同期" }),
3565
+ ]),
3566
+ el("p", { class: "ops-card-desc", text: "Backlog で課題やコメントが変わったときに、その内容をすぐ取り込みます(毎晩の同期を待たずに反映されます)。Backlog 側で一度だけ設定してください。" }),
3567
+ ].concat(steps));
3568
+ }
3569
+
3328
3570
  /** 鍵の入力欄に出す説明。kind は Lambda 側(resolveSecretTarget)と綴りを合わせること */
3329
3571
  var SECRET_FORMS = {
3330
3572
  "figma": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.67",
3
+ "version": "0.4.69",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,