@takagaki/cortex-decisions-viewer 0.4.66 → 0.4.68

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
@@ -33,9 +33,6 @@ function renderSite(site) {
33
33
  <meta name="viewport" content="width=device-width, initial-scale=1" />
34
34
  <meta name="robots" content="noindex, nofollow" />
35
35
  <title>${safeTitle}</title>
36
- <link rel="preconnect" href="https://fonts.googleapis.com" />
37
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
38
- <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;600;700&display=swap" rel="stylesheet" />
39
36
  <style>${CSS}</style>
40
37
  </head>
41
38
  <body>
@@ -545,7 +542,13 @@ body {
545
542
  background: linear-gradient(135deg, #ffffff 0%, #f4f8fe 45%, #dceafd 100%);
546
543
  background-attachment: fixed;
547
544
  color: var(--text);
548
- font-family: "Noto Sans JP", "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
545
+ /* 閲覧者の端末にあるフォントだけを使う。外部URLを参照しないのは ADAPTER_ICONS と同じ理由——
546
+ この画面は顧客が見るので、参照すると顧客のブラウザが Google のサーバへ接続しに行く。
547
+ Webフォントの埋め込みは選ばなかった: 日本語フォントは unicode-range で496分割して配信されており、
548
+ しかもこの画面は顧客のデータ(チャンネル名・人名・用語)を出すので、
549
+ ビルド時に必要な文字を確定できない。サブセットすると想定外の漢字が化ける。 */
550
+ font-family: system-ui, -apple-system, "Hiragino Kaku Gothic ProN", "Hiragino Sans",
551
+ "Yu Gothic UI", "Yu Gothic", Meiryo, sans-serif;
549
552
  line-height: 1.7;
550
553
  font-size: 15px;
551
554
  }
@@ -888,7 +891,28 @@ a.ds-name:hover { text-decoration: underline; }
888
891
  .ds-note { color: var(--muted); font-size: 12px; line-height: 1.7; margin: 6px 0 0; }
889
892
  .ds-sync { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; flex: 0 0 auto; }
890
893
  .ds-col { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
891
- .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
+ }
892
916
  .ds-drive-note { font-size: 12px; color: #6b7280; line-height: 1.6; }
893
917
  .ds-group { border: none; margin: 0; }
894
918
  .ds-group > summary {
@@ -1012,6 +1036,11 @@ a.ds-name:hover { text-decoration: underline; }
1012
1036
  .ds-toggle-msg.is-ok { color: #1a7f37; }
1013
1037
  .ds-toggle-msg.is-error { color: #b42318; }
1014
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); }
1015
1044
  .ds-gate { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600; flex: 0 0 auto; }
1016
1045
  .ds-gate::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; }
1017
1046
  .ds-gate-ok { color: #1f7a45; }
@@ -1060,6 +1089,12 @@ a.ds-name:hover { text-decoration: underline; }
1060
1089
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
1061
1090
  padding: 18px; box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 10px;
1062
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; }
1063
1098
  .ops-card-head { display: flex; align-items: center; gap: 10px; }
1064
1099
  .ops-card-icon { font-size: 22px; line-height: 1; }
1065
1100
  .ops-card-title { font-size: 15px; font-weight: 700; color: var(--text); margin: 0; }
@@ -1728,6 +1763,14 @@ ${capabilityState.toString()}
1728
1763
  */
1729
1764
  var LIVE_FLEET = null;
1730
1765
  var LIVE_FLEET_FAILED = false;
1766
+ /**
1767
+ * リアルタイム同期の可否(op=state の realtime。#113)。取得前・取得失敗時は null。
1768
+ *
1769
+ * **fleet とは別に持つ。** fleet は fleet-status.json(夜間バッチの成果物)の射影で、
1770
+ * こちらはルーティング索引(DynamoDB)を引いた結果。同じ入れ物に混ぜると、
1771
+ * 「いつ時点の情報か」の注記がどちらを指すのか説明できなくなる。
1772
+ */
1773
+ var LIVE_REALTIME = null;
1731
1774
  /**
1732
1775
  * 接続状況の値を引く。ライブがあればライブ、無ければ静的ビルドの値。
1733
1776
  * SITE 側はキー名が fleetGeneratedAt だけ異なる(他は fleet-status.json と同名)。
@@ -1878,43 +1921,46 @@ ${capabilityState.toString()}
1878
1921
  }
1879
1922
  var aliases = (s.kind === "会議" && s.ingestState === "on" && s.meetingAliases) || [];
1880
1923
  if (signal) {
1881
- col.appendChild(el("span", {
1924
+ // **カードで一番強い要素にする(#117)。** 実機で見たとき、目印の案内が
1925
+ // 別名の一覧や注記と同じ細字で並んでいて、「まず会議名にこれを入れる」が
1926
+ // 伝わらなかった。最初に決めた合図が唯一の真実なので、そこだけを太く出す。
1927
+ //
1928
+ // **括弧を付けた形でそのまま出す。** 語だけ見せると 【KC定例】 のように
1929
+ // 混ぜて書かれ、どこも一致せず未仕分けへ落ちる(実際に起きた)。
1930
+ col.appendChild(el("div", {
1882
1931
  class: "ds-keys",
1883
- text: "会議名に入れる目印: 【" + signal + "】 または [" + signal + "]",
1884
1932
  title: "会議名のどこかにこの括弧付きの目印が入っていると、この案件に振り分けられます(大小文字は問いません)。括弧の中に他の文字を混ぜると一致しません。",
1885
- }));
1933
+ }, [
1934
+ el("span", { class: "ds-keys-label", text: "会議名にこれを入れてください" }),
1935
+ el("code", { class: "ds-keys-code", text: "【" + signal + "】" }),
1936
+ el("span", { class: "ds-keys-or", text: "または" }),
1937
+ el("code", { class: "ds-keys-code", text: "[" + signal + "]" }),
1938
+ ]));
1886
1939
  }
1887
1940
  if (aliases.length) {
1888
- // **目印が無くても入る会議。** 既に走っていて改名を頼みにくい定例を、
1889
- // いまの名前のまま登録したもの。出さないと「なぜこれだけ入るのか」
1890
- // 「新しい定例をどこに足すのか」が画面から分からない。
1941
+ // **読み取り専用の記録として出す(#117)。** 目印が無くても入る会議=括弧運用へ
1942
+ // 移る前から走っていて、改名を頼めなかった定例。出さないと「なぜこれだけ入るのか」が
1943
+ // 画面から分からないので残すが、**操作は一切出さない**。
1891
1944
  //
1892
- // **1件ずつ外せるようにする。** 追加はできて削除はできない、だと
1893
- // 間違えて登録した名前(日付つきのファイル名など)を消す手段が無い
1894
- col.appendChild(el("span", {
1895
- class: "ds-keys",
1896
- text: "目印なしで取り込む会議:",
1897
- title: "この名前の会議は、目印が無くてもこの案件に振り分けられます(大小文字・前後の空白は問いません。ファイル名に使えない記号(コロン・スラッシュ等)は同じものとして扱います)。既に走っている定例を改名せずに取り込むための登録です。",
1898
- }));
1899
- var alist = el("div", { class: "ds-subitems" });
1945
+ // 以前は1件ずつ外せるようにしていた。実機確認でユーザーが「最初に決めたキー(合図)が
1946
+ // 唯一真実」と確定させたので、画面から触れる余地を無くす——別名を足したり外したり
1947
+ // できる限り、合図と別名のどちらが正なのかが人によって揺れ続ける。
1948
+ // 変更が要るときはリポジトリの ingest-config.json を直接編集する(Lambda も断る)。
1949
+ var legacy = el("div", { class: "ds-legacy" }, [
1950
+ el("span", {
1951
+ text: "経過措置で登録された会議名(目印が無くても取り込まれます。変更はリポジトリの ingest-config.json で行います)",
1952
+ }),
1953
+ ]);
1954
+ var chips = el("div", { class: "ds-legacy-chips" });
1900
1955
  aliases.forEach(function (a) {
1901
- var row = el("div", { class: "ds-subitem" }, [el("span", { class: "ds-name", text: a })]);
1902
- row.appendChild(removeButton({
1903
- label: "この会議を外す",
1904
- confirmLines: function () {
1905
- return [
1906
- "「" + a + "」を取り込み対象から外します。",
1907
- "",
1908
- "以後この名前の会議は、会議名に目印を入れない限り取り込まれません。",
1909
- "",
1910
- "外しますか?",
1911
- ];
1912
- },
1913
- build: function () { return { op: "remove-meeting-alias", alias: a }; },
1956
+ chips.appendChild(el("span", {
1957
+ class: "ds-legacy-chip",
1958
+ text: a,
1959
+ title: "この名前の会議は、目印が無くてもこの案件に振り分けられます(大小文字・前後の空白は問いません。ファイル名に使えない記号(コロン・スラッシュ等)は同じものとして扱います)。",
1914
1960
  }));
1915
- alist.appendChild(row);
1916
1961
  });
1917
- col.appendChild(alist);
1962
+ legacy.appendChild(chips);
1963
+ col.appendChild(legacy);
1918
1964
  }
1919
1965
  if (s.driveSync === false) {
1920
1966
  col.appendChild(el("span", { class: "ds-drive-note", text: "Drive自動同期は未設定(手動で置いたファイルの変換のみ)" }));
@@ -2059,10 +2105,40 @@ ${capabilityState.toString()}
2059
2105
 
2060
2106
  pushCap("開発", adders.querySelector('[data-adder="github"]'));
2061
2107
 
2108
+ /**
2109
+ * GitHub の読み取りトークンを入れる欄が要るか(#118)。
2110
+ *
2111
+ * トークンは **GitHub App が入っていない組織のフォールバック**でしかない。全組織が
2112
+ * App(か登録済みトークン)で読めている案件に入力欄を出すと、実機で言われたとおり
2113
+ * 「App に移行したのに、なぜトークンを求められるのか」になる。押す必要が無いものを
2114
+ * 押させるのは、未設定の警告を出し続けるのと同じ種類の誤りなので畳む。
2115
+ *
2116
+ * **判断材料が無いときは出す。** githubOwners は fleet-status.json 由来で、
2117
+ * 夜間バッチがまだ走っていない案件には無い。「情報が無い」を「接続済み」に寄せると、
2118
+ * **一番繋ぎたい立ち上げ直後の案件**から唯一の入口が消える。
2119
+ *
2120
+ * **unknown(確かめられなかった組織)も「要る」側に倒す。** 実態は none かもしれず、
2121
+ * 隠す方に倒すと直せなくなる(画面には組織ごとに「確認できませんでした」と出ている)。
2122
+ */
2123
+ function githubTokenNeeded() {
2124
+ var list = fleetVal("githubOwners") || [];
2125
+ if (!list.length) return true;
2126
+ return list.some(function (o) { return !o || (o.source !== "app" && o.source !== "token"); });
2127
+ }
2128
+
2062
2129
  // 連携の鍵は、その能力のところへ置く(画面下まで往復させない)
2063
2130
  var SECRET_CAP = { figma: "デザイン", backlog: "課題管理", "external-sources": "開発" };
2064
2131
  (SITE.secretKinds || []).forEach(function (k) {
2065
2132
  if (!SECRET_FORMS[k]) return;
2133
+ // 全組織が繋がっているなら、入力欄ではなく**そう言う1行**に畳む(#118)。
2134
+ // 何も出さないと「トークンはどこで入れるのか」が分からなくなる
2135
+ if (k === "external-sources" && !githubTokenNeeded()) {
2136
+ pushCap(SECRET_CAP[k], el("p", {
2137
+ class: "sec-how ds-all-connected",
2138
+ text: "すべての組織に接続済みです(GitHub App / 登録済みトークン)。読み取りトークンの登録は要りません。",
2139
+ }));
2140
+ return;
2141
+ }
2066
2142
  pushCap(SECRET_CAP[k], secretCard(k, SECRET_FORMS[k]));
2067
2143
  });
2068
2144
 
@@ -2239,7 +2315,11 @@ ${capabilityState.toString()}
2239
2315
  // 新規案件——**まさに何を入れれば繋がるのかを知りたい場面**——から、
2240
2316
  // 鍵が未設定であることの手がかりまで消える
2241
2317
  var sk = SECRET_OF_CAP[cap];
2242
- if (tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2318
+ // **GitHub は「トークンが無い」=「読めていない」ではない(#118)。** App で読めている
2319
+ // 案件に未設定の警告を出すと、直す必要が無いものを直させることになる。
2320
+ // 読めていない組織があるときは、この下の checks とタイルの状態(warn)が拾う
2321
+ var tokenIsFallback = sk === "external-sources" && !githubTokenNeeded();
2322
+ if (!tokenIsFallback && tools[cap] !== "none" && sk && (SITE.secretKinds || []).indexOf(sk) >= 0 && !((fleetVal("secretSources") || {})[sk])) {
2243
2323
  body.appendChild(el("div", { class: "ds-tile-note", text: "⚠ トークンが未設定です" }));
2244
2324
  }
2245
2325
  // セットアップの残りのうち、この能力のもの。**鍵の注記と同じ位置**に出して
@@ -2455,18 +2535,13 @@ ${capabilityState.toString()}
2455
2535
  // 宣言で隠すと、動いているのに説明が出ない状態になる。
2456
2536
  if (s.kind === "会議" && s.ingestState === "on") {
2457
2537
  box.appendChild(meetingHowTo());
2458
- // **改名を頼めない定例のための逃げ道。** 通常は会議名に【合図】を入れてもらうが、
2459
- // 顧客が主催する定例では改名を頼めない。実際の会議名をそのまま登録して拾う
2460
- box.appendChild(addForm({
2461
- title: "+ 会議名をそのまま取り込み対象にする",
2462
- placeholder: "例: GDO様 定例",
2463
- requiredNote: "会議名を入力してください。",
2464
- hint: "合図(【…】)を入れられない会議のための設定です。ここに入れた名前と**完全に一致する**会議だけが取り込まれます(部分一致はしません)。カレンダーに出ている会議名だけを入れてください——日付や「- Gemini によるメモ」のような後ろに付く文字列を含めると、どの会議とも一致しません。Bot の招待は別途必要です。",
2465
- confirmLines: function (v) {
2466
- return ["この名前の会議を取り込み対象にします。", "", v, "", "追加しますか?"];
2467
- },
2468
- build: function (alias) { return { op: "add-meeting-alias", alias: alias }; },
2469
- }));
2538
+ // **会議名の登録を画面から変える口は置かない(#117 第2弾)。**
2539
+ // 別名は括弧付きの合図運用へ移った際の経過措置で、既存案件のメンバーに定例の
2540
+ // 改名を強いないために置いたもの。第1弾では「既に別名がある案件にだけ追加口を出す」
2541
+ // としたが、実機確認でユーザーが**「最初に決めたキー(合図)が唯一真実」**と確定させた。
2542
+ // 経過措置の案件にフォームを残すと、その案件の中では別名が現役の登録手段に見え続け、
2543
+ // 合図と別名のどちらが正なのかが人によって揺れる。画面は合図1つだけを案内し、
2544
+ // 別名は上のチップ(読み取り専用の記録)で見せるに留める。Lambda 側も add/remove とも断る。
2470
2545
  used = true;
2471
2546
  } else if (s.kind === "会議" && usable("会議", "google-meet")) {
2472
2547
  if (s.ingestState === "unset") {
@@ -2494,8 +2569,12 @@ ${capabilityState.toString()}
2494
2569
  // backlog-settings.json が1つも無い(固定IP経由の同期で、UIからの投入は
2495
2570
  // 鍵と同じく対応していない)。会議・共有資料は unset / broken を文言で
2496
2571
  // 伝えてフォームを出さない形なので、課題管理だけ不揃いだった
2572
+ //
2573
+ // **接続先の正本は issues-config.json(#111)**だが、その置き場は
2574
+ // 同期ミラー(backlog-settings.json)の位置から決めている。ミラーが1つも
2575
+ // 無い案件では正本をどこへ置けばよいかも決められないので、条件は変わらない
2497
2576
  if (!s.url) {
2498
- box.appendChild(el("p", { class: "sec-how", text: "この案件の課題管理の設定ファイル(backlog-settings.json)がまだありません。先に初回の同期を実行するか、担当者にご連絡ください。" }));
2577
+ box.appendChild(el("p", { class: "sec-how", text: "この案件の課題管理の設定ファイル(同期ミラーの backlog-settings.json)がまだありません。接続先の正本(issues-config.json)の置き場もここから決められないため、先に初回の同期を実行するか、担当者にご連絡ください。" }));
2499
2578
  used = true;
2500
2579
  } else box.appendChild(addForm({
2501
2580
  title: "接続先を変更",
@@ -2521,6 +2600,11 @@ ${capabilityState.toString()}
2521
2600
  },
2522
2601
  build: function (url) { return { op: "set-backlog-target", url: url }; },
2523
2602
  }));
2603
+ // **接続先の下に置く。** リアルタイム同期は「どのプロジェクトを見るか」が
2604
+ // 決まった後の話で、順序が逆だと、接続先を設定していない案件が先に
2605
+ // Webhook を貼りに行く(貼っても届かない)
2606
+ var rt = realtimeSyncCard();
2607
+ if (rt) box.appendChild(rt);
2524
2608
  used = true;
2525
2609
  }
2526
2610
 
@@ -2685,7 +2769,7 @@ ${capabilityState.toString()}
2685
2769
  wrap.appendChild(el("p", { text: "次の2つが揃った会議だけが取り込まれます。" }));
2686
2770
  var ul = el("ul", { class: "sec-how-list" });
2687
2771
  ul.appendChild(el("li", { text: "cortex-notetaker Bot が招待されている(定例はシリーズに1回でOK)" }));
2688
- ul.appendChild(el("li", { text: "会議名に上の語のどれかが含まれている" }));
2772
+ ul.appendChild(el("li", { text: "会議名に上の目印が入っている" }));
2689
2773
  wrap.appendChild(ul);
2690
2774
  wrap.appendChild(el("p", { text: "どちらか一方だけでは取り込まれません。" }));
2691
2775
  return wrap;
@@ -3074,6 +3158,18 @@ ${capabilityState.toString()}
3074
3158
  return e;
3075
3159
  }
3076
3160
 
3161
+ /**
3162
+ * 登録の由来の表示(#118)。値は fleet-status.json の origin
3163
+ * (explicit … 登録ファイルに手で書いた / derived … .gitmodules 等からの自動導出 / both)。
3164
+ * **知らない値には何も出さない**(生の内部値を顧客の画面に出さないため)。
3165
+ */
3166
+ var ORIGIN_LABEL = { explicit: "明示登録", derived: "自動検出", both: "明示登録+自動検出" };
3167
+ var ORIGIN_HELP = {
3168
+ explicit: "開発の登録ファイル(dev-sources.json)に登録されているリポジトリです。ここから登録を削除できます。",
3169
+ derived: ".gitmodules から自動で見つかったリポジトリです。登録ファイルに無いので削除はできません(読み取りを止めることはできます)。",
3170
+ both: "登録ファイルにも書かれていて、.gitmodules からも見つかっているリポジトリです。",
3171
+ };
3172
+
3077
3173
  function extSourceCard(s, meta) {
3078
3174
  var badges = el("div", { class: "ds-badges" });
3079
3175
  var pending = pendingGoldFor(s);
@@ -3087,6 +3183,17 @@ ${capabilityState.toString()}
3087
3183
  else if (s.goldState === "undeclared") badges.appendChild(el("span", { class: "badge ds-undeclared", text: "未宣言(対象外)" }));
3088
3184
  else if (s.gold === false) badges.appendChild(el("span", { class: "badge ds-excluded", text: "除外" }));
3089
3185
  if (s.notify === true) badges.appendChild(el("span", { class: "badge ds-notify", text: "通知先" }));
3186
+ // 登録の由来(#118)。**「登録から削除」が出る/出ないの理由がこれ。** 導出されたリポは
3187
+ // 登録ファイルから消しても翌日また現れるので削除を出していないが、画面には
3188
+ // 「同じ見た目の行なのに、片方にだけ削除がある」としか映らず、毎回考え直すことになる。
3189
+ // GitHub の行だけに出す(削除の出し分けがあるのはここだけで、他は全部 explicit)
3190
+ if (String(s.type).indexOf("github") === 0 && ORIGIN_LABEL[s.origin]) {
3191
+ badges.appendChild(el("span", {
3192
+ class: "badge ds-origin",
3193
+ text: ORIGIN_LABEL[s.origin],
3194
+ title: ORIGIN_HELP[s.origin],
3195
+ }));
3196
+ }
3090
3197
  var gate = DS_GATE[s.gate] || DS_GATE["unknown"];
3091
3198
  var card = el("div", { class: "ds-card" }, [
3092
3199
  el("div", { class: "ds-card-main" }, [
@@ -3272,6 +3379,9 @@ ${capabilityState.toString()}
3272
3379
  // op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
3273
3380
  .then(function (res) { if (!res.ok) throw new Error("http " + res.status); return res.json(); })
3274
3381
  .then(function (j) {
3382
+ // **fleet より先に読む。** fleet-status.json をまだ持たない案件(立ち上げ直後)は
3383
+ // 下で throw するが、リアルタイム同期の可否は索引由来なので独立に決まる
3384
+ LIVE_REALTIME = (j && j.realtime) || null;
3275
3385
  if (!j || !j.fleet) throw new Error("no fleet");
3276
3386
  LIVE_FLEET = normalizeFleet(j.fleet);
3277
3387
  LIVE_FLEET_FAILED = false;
@@ -3312,6 +3422,75 @@ ${capabilityState.toString()}
3312
3422
  return null;
3313
3423
  }
3314
3424
 
3425
+ /**
3426
+ * リアルタイム同期(Backlog Webhook)の設定手順(#113)。
3427
+ *
3428
+ * **出すのは「手順どおり設定すれば実際に効く案件」だけ。** 判断は Lambda(op=state)が返す
3429
+ * realtime に委ねる——画面が持っている静的データからは「ルーティング索引に自分の行が
3430
+ * あるか」が分からないし、2箇所で判定すると片方だけがずれる。
3431
+ * - supported: false … 固定IP経由で同期している案件。**黙って欠けさせず**理由を1行出す
3432
+ * - ready: false … 索引にまだ行が無い。設定しても届かないので手順を出さない
3433
+ *
3434
+ * **URLに載せるトークンは案件別**(SITE.backlogWebhook)。艦隊共通トークンを載せると、
3435
+ * この画面を見られる人が全案件ぶんの偽イベントを投げられる(types.ts の backlogWebhook 参照)。
3436
+ */
3437
+ function realtimeSyncCard() {
3438
+ var rt = LIVE_REALTIME;
3439
+ if (!rt) return null; // 取得前・取得失敗・Backlogを使わない案件
3440
+ if (rt.supported === false) {
3441
+ return el("p", {
3442
+ class: "sec-how",
3443
+ text: "この案件は固定IP経由でBacklogを同期しているため、リアルタイム同期(Webhook)には未対応です。反映は毎晩の同期のタイミングになります。",
3444
+ });
3445
+ }
3446
+ var wh = SITE.backlogWebhook;
3447
+ if (!rt.ready || !wh || !wh.url || !wh.token) return null;
3448
+ var url = wh.url + (wh.url.indexOf("?") === -1 ? "?" : "&") + "t=" + encodeURIComponent(wh.token);
3449
+
3450
+ // URLは**選択できる形**で出す(コピーボタンが使えない環境でも手で取れるように)
3451
+ var field = el("input", { class: "sec-input rt-url", type: "text", readonly: "readonly", spellcheck: "false" });
3452
+ field.value = url;
3453
+ var copyBtn = el("button", { class: "ds-toggle-btn", type: "button", text: "コピー" });
3454
+ var msg = el("div", { class: "ds-toggle-msg" });
3455
+ copyBtn.addEventListener("click", function () {
3456
+ var done = function () { msg.className = "ds-toggle-msg is-ok"; msg.textContent = "✓ コピーしました。"; };
3457
+ var fallback = function () {
3458
+ // clipboard API が無い/拒否された環境(http・古いブラウザ・権限拒否)。
3459
+ // **黙って何も起きないにしない**——選択まではしてやって、次の一手を伝える
3460
+ try {
3461
+ field.focus();
3462
+ field.select();
3463
+ if (document.execCommand && document.execCommand("copy")) return done();
3464
+ } catch (e) { /* 下の案内へ */ }
3465
+ msg.className = "ds-toggle-msg is-error";
3466
+ msg.textContent = "⚠ 自動コピーできませんでした。選択された文字列を手でコピーしてください。";
3467
+ };
3468
+ try {
3469
+ if (navigator.clipboard && navigator.clipboard.writeText) {
3470
+ navigator.clipboard.writeText(url).then(done, fallback);
3471
+ } else fallback();
3472
+ } catch (e) { fallback(); }
3473
+ });
3474
+
3475
+ return el("div", { class: "ops-card rt-card" }, [
3476
+ el("div", { class: "ops-card-head" }, [
3477
+ el("span", { class: "ops-card-icon", text: "⚡" }),
3478
+ el("h3", { class: "ops-card-title", text: "リアルタイム同期" }),
3479
+ ]),
3480
+ el("p", { class: "ops-card-desc", text: "Backlog で課題やコメントが変わったときに、その内容をすぐ取り込みます(毎晩の同期を待たずに反映されます)。Backlog 側で一度だけ設定してください。" }),
3481
+ el("ol", { class: "rt-steps" }, [
3482
+ el("li", { text: "Backlog のプロジェクト設定 → インテグレーション → Webhook を開く" }),
3483
+ el("li", { text: "「WebHook URL」に下の案件専用URLを貼り付ける" }),
3484
+ el("li", { text: "通知イベントで「課題の追加」「課題の更新」「課題にコメント」等にチェックを入れて保存する" }),
3485
+ ]),
3486
+ el("div", { class: "rt-url-row" }, [field, copyBtn]),
3487
+ msg,
3488
+ // **この画面は顧客も見る。** URLに秘密が入っていることと、その扱いを書いておく
3489
+ el("p", { class: "sec-note", text: "このURLはこの案件専用です(他の案件には届きません)。秘密のトークンを含むので、貼り付け先は Backlog のこのプロジェクトの設定欄だけにしてください。" }),
3490
+ el("p", { class: "sec-how", text: "反映は受信から約1分後です(1分ごとのミニバッチでまとめて取り込みます)。初回は上の接続先の設定と、夜間同期が一度済んでいることが前提になります。" }),
3491
+ ]);
3492
+ }
3493
+
3315
3494
  /** 鍵の入力欄に出す説明。kind は Lambda 側(resolveSecretTarget)と綴りを合わせること */
3316
3495
  var SECRET_FORMS = {
3317
3496
  "figma": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.66",
3
+ "version": "0.4.68",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,