@takagaki/cortex-decisions-viewer 0.4.57 → 0.4.58

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
@@ -157,8 +157,16 @@ async function readFleetSources(repoRoot) {
157
157
  label: String(s.label ?? ""),
158
158
  url: s.url != null ? String(s.url) : undefined,
159
159
  lastSync: s.lastSync != null ? String(s.lastSync) : undefined,
160
- matchKeys: Array.isArray(s.matchKeys)
161
- ? s.matchKeys.map((k) => String(k)).filter(Boolean)
160
+ // 会議名に入れる合図。**古い fleet-status.json の matchKeys は引き継がない**
161
+ // (client名と廃止した meetingNamePatterns の寄せ集めで、いま照合には使われていない。
162
+ // 出すと「この語を会議名に入れれば拾われる」と読めてしまう)。
163
+ meetingKey: s.meetingKey != null ? String(s.meetingKey) : undefined,
164
+ // 既存の定例を、いまの名前のまま拾うために登録した別名。設定ファイルにしか
165
+ // 無いと「なぜこの会議だけ入るのか」が画面から分からない
166
+ // **String() を先に噛ませない。** `String(null)` は "null" という文字列になり
167
+ // `.filter(Boolean)` を通り抜ける(画面に「null」という会議名が出る)
168
+ meetingAliases: Array.isArray(s.meetingAliases)
169
+ ? s.meetingAliases.filter((a) => typeof a === "string" && a !== "")
162
170
  : undefined,
163
171
  driveSync: typeof s.driveSync === "boolean" ? s.driveSync : undefined,
164
172
  enabled: typeof s.enabled === "boolean" ? s.enabled : undefined,
package/dist/render.js CHANGED
@@ -742,6 +742,9 @@ a:hover { text-decoration: underline; }
742
742
  .datasources-head { font-size: 18px; font-weight: 700; color: var(--text); margin: 0 0 4px; }
743
743
  .datasources-intro { color: var(--muted); font-size: 13.5px; line-height: 1.8; margin: 0 0 6px; }
744
744
  .datasources-asof { color: var(--muted); font-size: 12.5px; line-height: 1.7; margin: 0 0 14px; }
745
+ /* 最新を取得できなかったとき。古い値を現在値と読ませないよう、通常の注記より目立たせる */
746
+ .datasources-asof.is-stale { color: var(--warn, #a15c00); }
747
+ .conn-map-asof { color: var(--muted); font-size: 12px; margin: 6px 0 0; text-align: right; }
745
748
  .ds-sub { font-size: 13px; font-weight: 700; color: var(--muted); margin: 20px 0 8px; }
746
749
  .ds-list { display: flex; flex-direction: column; gap: 10px; }
747
750
  .ds-card {
@@ -795,6 +798,12 @@ a.ds-name:hover { text-decoration: underline; }
795
798
  .cm-sync { font-size: 10.5px; fill: var(--muted); font-variant-numeric: tabular-nums; }
796
799
  .cm-node.cm-off .cm-sync { fill: #9ca3af; }
797
800
  .cm-badge { font-size: 13px; }
801
+ /* 「入ってこない」ときに自分で確かめる口。一覧の直前に置く */
802
+ .ds-syncnow { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin: 0 0 .6rem; }
803
+ .ds-syncnow-label { font-size: .82rem; color: var(--muted); }
804
+ /* 自分で繋げばよいことを、手順より先に伝える。埋もれると管理者に依頼しに行ってしまう */
805
+ .sec-preferred { margin: .4rem 0 .2rem; padding: .5rem .7rem; font-size: .84rem; line-height: 1.6;
806
+ border-left: 3px solid var(--accent); background: var(--accent-weak); border-radius: 0 6px 6px 0; }
798
807
  /* 追加した直後にその場で見せる行。正式な一覧(スナップショット由来)に載るまでの繋ぎ */
799
808
  .ds-pending { margin-top: .5rem; display: flex; flex-direction: column; gap: .25rem; }
800
809
  .ds-pending-item { display: flex; align-items: center; gap: .5rem; font-size: .85rem;
@@ -1462,15 +1471,85 @@ const CLIENT_JS = `
1462
1471
  if (url) return el("a", { class: "ds-name", href: url, target: "_blank", rel: "noopener", text: text });
1463
1472
  return el("span", { class: "ds-name", text: text });
1464
1473
  }
1465
- function renderDataSources() {
1466
- var internals = SITE.internalSources || [];
1467
- var sources = SITE.externalSources || [];
1468
- var pipes = SITE.pipelines || [];
1474
+ /**
1475
+ * 「いま同期する」。中央の取り込み(Apps Script)を即時に起こす。
1476
+ *
1477
+ * **押しても即座に画面は変わらない。** 取り込み コミット → 接続状況の再生成 →
1478
+ * ビューアの再ビルド、と数分かかる。それを言わずにボタンだけ置くと、
1479
+ * 「押したのに何も起きない」と読まれる(設定の追加でまさにそうなった)。
1480
+ */
1481
+ function syncNowRow() {
1482
+ var row = el("div", { class: "ds-syncnow" });
1483
+ if (!SITE.intake) return row;
1484
+ var msg = el("span", { class: "ds-toggle-msg" });
1485
+ var mk = function (kind, label) {
1486
+ var btn = el("button", { class: "ds-toggle-btn", type: "button", text: label });
1487
+ btn.addEventListener("click", async function () {
1488
+ btn.disabled = true;
1489
+ msg.className = "ds-toggle-msg";
1490
+ msg.textContent = "実行中…";
1491
+ var intake = SITE.intake;
1492
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
1493
+ try {
1494
+ var res = await fetch(pfx + "op=sync&t=" + encodeURIComponent(intake.token), {
1495
+ method: "POST",
1496
+ headers: { "content-type": "application/json" },
1497
+ body: JSON.stringify({ kind: kind }),
1498
+ });
1499
+ var j = await res.json().catch(function () { return {}; });
1500
+ // **起こせなかったときも「失敗」と言わない。** 定期実行が拾うので、
1501
+ // 押した人にできることは無い。何が起きるかだけを伝える
1502
+ msg.className = "ds-toggle-msg" + (res.ok && j.started ? " is-ok" : "");
1503
+ msg.textContent = (res.ok && j.started ? "✓ " : "") + (j.message || "開始できませんでした。");
1504
+ } catch (e) {
1505
+ msg.className = "ds-toggle-msg";
1506
+ msg.textContent = "通信に失敗しました。定期実行(30分ごと)で取り込まれます。";
1507
+ }
1508
+ btn.disabled = false;
1509
+ });
1510
+ return btn;
1511
+ };
1512
+ row.appendChild(el("span", { class: "ds-syncnow-label", text: "いま同期する:" }));
1513
+ row.appendChild(mk("materials", "資料"));
1514
+ row.appendChild(mk("transcripts", "文字起こし"));
1515
+ row.appendChild(msg);
1516
+ return row;
1517
+ }
1518
+
1519
+ /**
1520
+ * 接続状況のライブ値(op=state の応答)。取得前・取得失敗時は null。
1521
+ *
1522
+ * **静的ビルド(SITE)は捨てない。** ビルド時に焼かれた値で先に描き切り、取れたら差し替える。
1523
+ * こうすると (1) いま即座に出ている操作タブに読み込み待ちが入らない (2) 失敗しても
1524
+ * 既に描き終わっているので空表示にならない。詳細表示の本文(loadBody)と同じ考え方。
1525
+ */
1526
+ var LIVE_FLEET = null;
1527
+ var LIVE_FLEET_FAILED = false;
1528
+ /**
1529
+ * 接続状況の値を引く。ライブがあればライブ、無ければ静的ビルドの値。
1530
+ * SITE 側はキー名が fleetGeneratedAt だけ異なる(他は fleet-status.json と同名)。
1531
+ */
1532
+ function fleetVal(key) {
1533
+ // **ライブが取れたら、そちらだけを見る(キー単位で静的へ落ちない)。**
1534
+ // キーごとにフォールバックすると、たとえば externalSources はライブ・secretSources は
1535
+ // 前日のビルド、という**2つの時点が混ざった画面**になる。上に出す「◯◯時点」は1つしか
1536
+ // 無いので、その注記が嘘になる——「変えたはずなのに違う値が出ている」という、
1537
+ // この節がまさに警告している誤読を作ることになる。
1538
+ // 欠けているキーは「無い」として扱い、既存の空配列ガードに任せる。
1539
+ if (LIVE_FLEET) return LIVE_FLEET[key];
1540
+ return SITE[key === "generatedAt" ? "fleetGeneratedAt" : key];
1541
+ }
1542
+
1543
+ /** 組み立てだけを行う(設置は呼び出し側)。ライブ差し替えで同じ節を作り直すため分けている */
1544
+ function buildDataSources() {
1545
+ var internals = fleetVal("internalSources") || [];
1546
+ var sources = fleetVal("externalSources") || [];
1547
+ var pipes = fleetVal("pipelines") || [];
1469
1548
  // 能力→ツールの宣言(例: チャット: "none")。"none" の能力は「未使用」のグレー行で存在だけ見せる
1470
- var tools = SITE.tools || {};
1549
+ var tools = fleetVal("tools") || {};
1471
1550
  var chatOff = tools["チャット"] === "none";
1472
1551
  var devOff = tools["開発"] === "none";
1473
- if (!internals.length && !sources.length && !pipes.length && !chatOff && !devOff) return; // すべて無ければセクションごと出さない
1552
+ if (!internals.length && !sources.length && !pipes.length && !chatOff && !devOff) return null; // すべて無ければセクションごと出さない
1474
1553
 
1475
1554
  var sec = el("section", { class: "datasources" });
1476
1555
  sec.appendChild(el("h2", { class: "datasources-head", text: "データソースと自動化" }));
@@ -1478,14 +1557,33 @@ const CLIENT_JS = `
1478
1557
  // **いつ時点の情報かを必ず出す。** この節は夜間バッチが作るスナップショットを読んでいるので、
1479
1558
  // 設定を変えても次の生成までは古い値のまま。時刻が無いと「変えたのに反映されない」ではなく
1480
1559
  // 「変えたはずなのに違う値が出ている」と読まれてしまう(実際に設定変更直後にそうなった)。
1481
- var asOf = SITE.fleetGeneratedAt ? dsFmtTime(SITE.fleetGeneratedAt) : "";
1560
+ //
1561
+ // **取得できたかで文言を変える。** 静的ビルドの値をそのまま「現在値」として見せると、
1562
+ // まさにここが警告している誤読を自分で起こす。取りに行って失敗したことは隠さない。
1563
+ var asOf = fleetVal("generatedAt") ? dsFmtTime(fleetVal("generatedAt")) : "";
1482
1564
  if (asOf) {
1483
- sec.appendChild(el("p", { class: "datasources-asof",
1484
- text: "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。" }));
1565
+ var asOfText = LIVE_FLEET_FAILED
1566
+ ? "最新の接続状況を取得できませんでした。表示しているのは " + asOf + " 時点の情報です。"
1567
+ : "接続状況は " + asOf + " 時点の自動チェック結果です。設定を変えた場合、ここに反映されるのは次回の実行後になります。";
1568
+ sec.appendChild(el("p", {
1569
+ class: "datasources-asof" + (LIVE_FLEET_FAILED ? " is-stale" : ""),
1570
+ text: asOfText,
1571
+ }));
1485
1572
  }
1486
1573
 
1487
1574
  if (internals.length) {
1488
1575
  sec.appendChild(el("div", { class: "ds-sub", text: "リポジトリ内の情報源(自動同期)" }));
1576
+ // **「いま同期する」ボタンは出さない。** 受け口(Apps Script の Webアプリ)を
1577
+ // 公開できないため、押しても届かない。
1578
+ //
1579
+ // Workspace のポリシーで**匿名アクセス可能なWebアプリを公開できない**
1580
+ // (デプロイ設定の「アクセスできるユーザー」に「全員」が出ず、「Classmethod.jp 内の全員」まで)。
1581
+ // Lambda は Google アカウントを持たないのでドメイン制限を通れない。
1582
+ //
1583
+ // 代わりに**待ち時間そのものを縮めた**(トリガー30分→15分・設定キャッシュ30分→5分で
1584
+ // 最悪60分→20分)。押す口が要るなら、bot の資格情報で Lambda が認証する経路を
1585
+ // 用意してからここを戻す(syncNowRow と Lambda の op=sync は残してある)。
1586
+ // sec.appendChild(syncNowRow());
1489
1587
  var ilist = el("div", { class: "ds-list" });
1490
1588
  internals.forEach(function (s) {
1491
1589
  // ラベルの下に補足行(会議の照合キー・Drive未設定の注記)を重ねられるよう縦組みにする
@@ -1495,30 +1593,36 @@ const CLIENT_JS = `
1495
1593
  // (Router.gs:「フィルタは持たない…招待が同意のゲート」)。enabled はルーティングの
1496
1594
  // 可否であって取り込みの可否ではないので、OFFにすると**Botは録り続けるのに
1497
1595
  // 文字起こしがINBOXに溜まる**という、招待しないより悪い状態を作れてしまう。
1498
- // **案件キーを先頭に足す。** 照合は「案件キー 顧客名 meetingNamePatterns」の
1499
- // 3段で行われるが、fleet-status が持つのは後ろ2つだけ。第1段は艦隊レジストリの
1500
- // キーで、投入設定から取れる。運用規約「会議名に案件キーを含める」の本体なので、
1501
- // これが抜けていると一番使われている目印が画面に出ない。
1502
- var keys = [];
1596
+ // **合図(会議名に入れる目印)を出す。** 照合は「会議名に [合図] / 【合図】
1597
+ // 入っているか」だけ(Router.gs)。合図は案件リポの meetingKey、未宣言なら艦隊
1598
+ // レジストリのキーで、後者は案件リポも fleet-status も知らない——投入設定からしか取れない。
1599
+ //
1600
+ // **括弧を付けた形で出す。** 語だけ見せると 【KC定例】 のように混ぜて書かれ、
1601
+ // どこも一致せず未仕分けへ落ちる(実際に起きた)。そのままコピーできる形にする。
1602
+ var signal = null;
1503
1603
  // **自動取り込みが動いているときだけ出す。** Teams等で手動運用している案件に
1504
1604
  // 会議名の条件を出すと、自動で拾われると誤解させる(実際には拾われない)。
1505
1605
  if (s.kind === "会議" && s.ingestState === "on") {
1506
- // 案件キーは艦隊レジストリのもので、案件リポ自身は知らない(fleet-status にも無い)。
1507
- // ビルド時に注入される投入設定からしか取れないため、設定UIが未配線の案件では出ない。
1508
- if (SITE.intake && SITE.intake.projectKey) keys.push(SITE.intake.projectKey);
1509
- (s.matchKeys || []).forEach(function (k) {
1510
- // 照合は大小文字を区別しないので、違いだけの重複は出さない(「cortex / Cortex」は雑音)
1511
- var dup = keys.some(function (x) { return x.toLowerCase() === String(k).toLowerCase(); });
1512
- if (!dup) keys.push(k);
1513
- });
1606
+ signal = s.meetingKey || (SITE.intake && SITE.intake.projectKey) || null;
1607
+ }
1608
+ var aliases = (s.kind === "会議" && s.ingestState === "on" && s.meetingAliases) || [];
1609
+ if (signal) {
1610
+ col.appendChild(el("span", {
1611
+ class: "ds-keys",
1612
+ text: "会議名に入れる目印: 【" + signal + "】 または [" + signal + "]",
1613
+ title: "会議名のどこかにこの括弧付きの目印が入っていると、この案件に振り分けられます(大小文字は問いません)。括弧の中に他の文字を混ぜると一致しません。",
1614
+ }));
1514
1615
  }
1515
- if (keys.length) {
1516
- var shown = keys.slice(0, 6).join(" / ");
1517
- if (keys.length > 6) shown += " 他" + (keys.length - 6) + "件";
1616
+ if (aliases.length) {
1617
+ // **目印が無くても入る会議。** 既に走っていて改名を頼みにくい定例を、
1618
+ // いまの名前のまま登録したもの。出さないと「なぜこれだけ入るのか」
1619
+ // 「新しい定例をどこに足すのか」が画面から分からない
1620
+ var shown = aliases.slice(0, 5).join(" / ");
1621
+ if (aliases.length > 5) shown += " 他" + (aliases.length - 5) + "件";
1518
1622
  col.appendChild(el("span", {
1519
1623
  class: "ds-keys",
1520
- text: "取り込み対象の会議名: " + shown,
1521
- title: "会議名にこれらの語が含まれていると、この案件に振り分けられます",
1624
+ text: "目印なしで取り込む会議: " + shown,
1625
+ title: "この名前の会議は、目印が無くてもこの案件に振り分けられます(大小文字・前後の空白は問いません。ファイル名に使えない記号(コロン・スラッシュ等)は同じものとして扱います)。既に走っている定例を改名せずに取り込むための登録です。",
1522
1626
  }));
1523
1627
  }
1524
1628
  if (s.driveSync === false) {
@@ -1651,7 +1755,11 @@ const CLIENT_JS = `
1651
1755
  sec.appendChild(plist);
1652
1756
  }
1653
1757
 
1654
- app.appendChild(sec);
1758
+ return sec;
1759
+ }
1760
+ function renderDataSources() {
1761
+ var sec = buildDataSources();
1762
+ if (sec) app.appendChild(sec);
1655
1763
  }
1656
1764
  // 未使用能力のプレースホルダ行(アダプターとして存在するが、この案件では設定されていないもの)
1657
1765
  function inactiveSourceRow(icon, typeLabel, anchorId) {
@@ -1715,7 +1823,7 @@ const CLIENT_JS = `
1715
1823
  // 「足したい案件でだけ足せない」という逆転になっていた。
1716
1824
  // 押しても必ず失敗するもの(別ツールが宣言済み)は usable() で弾く。
1717
1825
  if (!SITE.intake) return null;
1718
- var tools = SITE.tools || {};
1826
+ var tools = fleetVal("tools") || {};
1719
1827
  var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
1720
1828
  var box = el("div", {});
1721
1829
  var used = false;
@@ -1920,7 +2028,7 @@ const CLIENT_JS = `
1920
2028
  function renderExternalAdders() {
1921
2029
  var box = el("div", {});
1922
2030
  if (!SITE.intake) return box;
1923
- var tools = SITE.tools || {};
2031
+ var tools = fleetVal("tools") || {};
1924
2032
  var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
1925
2033
 
1926
2034
  // **Slack は tools 宣言で隠さない。** tools.チャット は能力ごとに1値しか持てないので、
@@ -2241,9 +2349,166 @@ const CLIENT_JS = `
2241
2349
  }
2242
2350
 
2243
2351
  // ---------- 操作タブ(接続マップ・データソースと自動化・取り込み。今後この受け皿に機能を足す) ----------
2352
+ /**
2353
+ * 接続状況をライブで取りに行き、取れたら操作タブを描き直す。
2354
+ *
2355
+ * **静的ビルドで描き切った後に呼ぶ。** 待たせないため、そして失敗しても既に画面が
2356
+ * 出ているため。1タブ表示につき1回だけで、ポーリングはしない(GitHubトークンは
2357
+ * 組織で共有していて、枯渇すると同じ組織の全案件のビルド・同期が止まる)。
2358
+ */
2359
+ /**
2360
+ * ライブ応答の最低限の正規化。
2361
+ *
2362
+ * **静的経路は build.ts が同じことをしている。** あちらは要素を許可リストで組み直し、
2363
+ * 非オブジェクトを落として型を強制する。ライブ経路にはそれが無いので、生成側
2364
+ * (別リポの cortex-engine)が壊れた1件を混ぜると、要素を読む段で TypeError になり
2365
+ * **操作タブが丸ごと出なくなる**(データソースだけでなく、その後の取り込みフォームも
2366
+ * 鍵の登録も消える)。fleet-passthrough.test.mjs が「壊れた1件でビルド全体が落ちると
2367
+ * 案件のビューアが丸ごと出なくなる」と書いているのと同じ事故を、ライブ経路で起こさない。
2368
+ *
2369
+ * ここは build.ts の許可リストを写さない(2枚になると列挙し忘れが静かに消える)。
2370
+ * **落とすのは「読めない形のもの」だけ**——配列の非オブジェクト要素と、
2371
+ * 文字列前提の配列に混ざった非文字列。
2372
+ */
2373
+ /** 入力中・送信中の節か(フォーカスがある/入力に値がある/進行中の表示がある) */
2374
+ function isBeingEdited(node) {
2375
+ if (node.contains(document.activeElement) && document.activeElement !== document.body) return true;
2376
+ var inputs = node.querySelectorAll("input, textarea");
2377
+ for (var i = 0; i < inputs.length; i++) { if (inputs[i].value) return true; }
2378
+ var msgs = node.querySelectorAll(".ds-toggle-msg");
2379
+ for (var m = 0; m < msgs.length; m++) { if (msgs[m].textContent) return true; }
2380
+ return false;
2381
+ }
2382
+
2383
+ /** 文字列だけを並べる配列。非文字列が混ざると "null" や "[object Object]" が表示される */
2384
+ var STRING_LIST_FIELDS = { meetingAliases: 1, driveFolderIds: 1 };
2385
+
2386
+ function normalizeFleet(fleet) {
2387
+ if (!fleet || typeof fleet !== "object") return null;
2388
+ var out = {};
2389
+ for (var k in fleet) {
2390
+ if (!Object.prototype.hasOwnProperty.call(fleet, k)) continue;
2391
+ var v = fleet[k];
2392
+ if (!Array.isArray(v)) { out[k] = v; continue; }
2393
+ out[k] = v
2394
+ .filter(function (item) { return item != null && typeof item === "object" && !Array.isArray(item); })
2395
+ .map(function (item) {
2396
+ var copy = {};
2397
+ for (var f in item) {
2398
+ if (!Object.prototype.hasOwnProperty.call(item, f)) continue;
2399
+ var fv = item[f];
2400
+ // 画面が文字列として並べる配列(会議の別名・DriveフォルダID)に非文字列が
2401
+ // 混ざると "null" や "[object Object]" が表示される。build.ts:178 が
2402
+ // 名指しで防いでいる罠なので、ライブ側でも同じ扱いにする
2403
+ if (Array.isArray(fv)) {
2404
+ copy[f] = STRING_LIST_FIELDS[f]
2405
+ ? fv.filter(function (x) { return typeof x === "string" && x !== ""; })
2406
+ : fv.filter(function (x) { return x != null; });
2407
+ } else {
2408
+ copy[f] = fv;
2409
+ }
2410
+ }
2411
+ return copy;
2412
+ });
2413
+ }
2414
+ return out;
2415
+ }
2416
+
2417
+ /**
2418
+ * ライブ値が届いたときの差し替え。**画面全体を描き直さない。**
2419
+ *
2420
+ * 当初は route() を呼んでいたが、これは renderList() → app.innerHTML = "" で
2421
+ * 操作タブを丸ごと作り直す。操作タブは DOM に状態を持つ設計なので
2422
+ * (貼りかけのトークン・「保存中…」・押した直後のボタンの向き)、
2423
+ * **保存中に応答が届くと入力が消え、成功メッセージは切り離された古いノードに書かれて
2424
+ * 画面に出ない**——保存は成功しているのに「押したのに何も起きなかった」と見える。
2425
+ * 差し替えるのはデータソース節と接続マップの注記だけにする。
2426
+ */
2427
+ function refreshDataSources() {
2428
+ if (listState.tab !== "ops") return;
2429
+ var old = app.querySelector(".datasources");
2430
+ // **触っている最中は差し替えない。** この節の中には追加フォームの入力欄があり、
2431
+ // 貼りかけのURLや「保存中…」の表示ごと作り直してしまう。保存が成功していても
2432
+ // 結果メッセージは切り離された古いノードに書かれ、画面には出ない——
2433
+ // 「押したのに何も起きなかった」と見えて、もう一度貼って押す(=二重保存)に誘導する。
2434
+ // 見送っても値は LIVE_FLEET に入っているので、次に操作タブを描くときに反映される。
2435
+ if (old && isBeingEdited(old)) return;
2436
+ var next = buildDataSources();
2437
+ if (old && next) old.parentNode.replaceChild(next, old);
2438
+ else if (old) old.parentNode.removeChild(old);
2439
+ else if (next) {
2440
+ // 静的では空でライブで出るようになった場合。接続マップの直後に入れる
2441
+ var map = app.querySelector(".conn-map");
2442
+ if (map && map.nextSibling) app.insertBefore(next, map.nextSibling);
2443
+ else app.insertBefore(next, app.firstChild);
2444
+ }
2445
+ updateConnMapAsOf();
2446
+ updateSecretStates();
2447
+ }
2448
+
2449
+ /**
2450
+ * 鍵カードの「現在: ◯◯ から取得中」だけを更新する。
2451
+ * **カードごと作り直さない**(貼りかけのトークンが消える)。表示は1行なので差し替えで足りる。
2452
+ */
2453
+ function updateSecretStates() {
2454
+ var sources = fleetVal("secretSources") || {};
2455
+ var cards = app.querySelectorAll("[data-secret-kind]");
2456
+ for (var i = 0; i < cards.length; i++) {
2457
+ var kind = cards[i].getAttribute("data-secret-kind");
2458
+ var line = cards[i].querySelector(".sec-state");
2459
+ if (!line) continue;
2460
+ var src = sources[kind] || "";
2461
+ line.className = src
2462
+ ? "sec-state" + (src === "Secrets Manager" ? " is-managed" : "")
2463
+ : "sec-state is-none";
2464
+ line.textContent = src ? "現在: " + src + " から取得中" : "現在: 未設定";
2465
+ }
2466
+ }
2467
+
2468
+ /**
2469
+ * 取得中フラグ。**完了しても寝かせたままにしない。**
2470
+ * 一度 true のままだと、タブを移動して戻ってきても再取得せず、設定を変えた人が
2471
+ * その場で反映を確かめる手段がフルリロードだけになる。取得が終わったら倒して、
2472
+ * **操作タブに入り直したときだけ**もう一度取りに行く(ポーリングはしない)。
2473
+ */
2474
+ var liveFleetRequested = false;
2475
+ function loadLiveFleet() {
2476
+ if (liveFleetRequested) return;
2477
+ var intake = SITE.intake;
2478
+ if (!intake) return;
2479
+ // **fetch の有無を確かめてから呼ぶ。** テスト環境(JSDOM)は window.fetch を持たない。
2480
+ // 素で呼ぶと描画中の ReferenceError になり、「画面が例外を出さずに描き切る」検査を割る。
2481
+ if (typeof fetch !== "function") return;
2482
+ liveFleetRequested = true;
2483
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
2484
+ fetch(pfx + "op=state&t=" + encodeURIComponent(intake.token), {
2485
+ method: "POST",
2486
+ headers: { "content-type": "application/json" },
2487
+ body: "{}",
2488
+ })
2489
+ // **非2xxも失敗として扱う。** ビューアとLambdaの版は独立に進むので、
2490
+ // op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
2491
+ .then(function (res) { if (!res.ok) throw new Error("http " + res.status); return res.json(); })
2492
+ .then(function (j) {
2493
+ if (!j || !j.fleet) throw new Error("no fleet");
2494
+ LIVE_FLEET = normalizeFleet(j.fleet);
2495
+ LIVE_FLEET_FAILED = false;
2496
+ liveFleetRequested = false;
2497
+ refreshDataSources();
2498
+ })
2499
+ .catch(function () {
2500
+ // 静的ビルドの値で描き続ける。ただし「古いかもしれない」ことは画面に出す
2501
+ LIVE_FLEET = null;
2502
+ LIVE_FLEET_FAILED = true;
2503
+ liveFleetRequested = false;
2504
+ refreshDataSources();
2505
+ });
2506
+ }
2507
+
2244
2508
  function renderOps() {
2245
2509
  renderConnMap();
2246
2510
  renderDataSources();
2511
+ loadLiveFleet();
2247
2512
  if (SITE.intake) {
2248
2513
  app.appendChild(el("p", { class: "ops-intro", text: "Cortex にコンテキストを取り込む操作です。ファイルを選ぶかテキストを貼って送信すると、自動で取り込まれます(数分で反映)。" }));
2249
2514
  app.appendChild(el("div", { class: "section-label", text: "取り込み" }));
@@ -2267,7 +2532,21 @@ const CLIENT_JS = `
2267
2532
  icon: "📋",
2268
2533
  title: "Backlog のAPIキー",
2269
2534
  desc: "課題・Wiki・ドキュメントを毎晩同期するために使います。",
2270
- how: "Backlog → 個人設定 → API で発行",
2535
+ how: "Backlog → 個人設定 → API で発行(この案件のプロジェクトが見えるアカウントで)",
2536
+ // **案件の担当者が自分で繋ぐ。** 以前は艦隊共通の読み取りアカウントを1つ運用していたが、
2537
+ // 案件が増えるたびに「その1人がプロジェクトへ参加追加してキーを配る」ことになり、
2538
+ // セットアップが一箇所に詰まる。**繋ぐのは、その案件を持っている人**にする。
2539
+ //
2540
+ // 代償は「その人が離れると同期が止まる」こと。ただしこれは**担当が変わったときに
2541
+ // 次の人が入れ直せば済む**話で、誰か1人に依存し続けるより回復しやすい。
2542
+ selfServe: {
2543
+ text:
2544
+ "この案件を担当しているあなたのアカウントで発行して、ここに貼ってください。" +
2545
+ "管理者への依頼は要りません。",
2546
+ note:
2547
+ "担当が変わったら、新しい担当者が同じ手順で入れ直してください" +
2548
+ "(前の担当者のアカウントが無効になると同期が止まります)。",
2549
+ },
2271
2550
  },
2272
2551
  "external-sources": {
2273
2552
  icon: "🐙",
@@ -2299,7 +2578,7 @@ const CLIENT_JS = `
2299
2578
  // **いま何で動いているか**を先に出す。入れ直したのに反映されていない状態が
2300
2579
  // 画面で分かるようにする(接続状況は夜間バッチのスナップショットなので、
2301
2580
  // 保存の直後は前日の値のまま。時刻は上の節に出している)。
2302
- var src = (SITE.secretSources || {})[kind] || "";
2581
+ var src = (fleetVal("secretSources") || {})[kind] || "";
2303
2582
  var state = src
2304
2583
  ? el("p", { class: "sec-state" + (src === "Secrets Manager" ? " is-managed" : ""),
2305
2584
  text: "現在: " + src + " から取得中" })
@@ -2349,14 +2628,19 @@ const CLIENT_JS = `
2349
2628
  }
2350
2629
  });
2351
2630
 
2352
- return el("div", { class: "ops-card" }, [
2631
+ // ライブ取得後に「現在: …」だけを差し替えるための目印(カードごと作り直すと入力が消える)
2632
+ return el("div", { class: "ops-card", "data-secret-kind": kind }, [
2353
2633
  el("div", { class: "ops-card-head" }, [
2354
2634
  el("span", { class: "ops-card-icon", text: meta.icon }),
2355
2635
  el("h3", { class: "ops-card-title", text: meta.title }),
2356
2636
  ]),
2357
2637
  el("p", { class: "ops-card-desc", text: meta.desc }),
2358
2638
  state,
2639
+ // **未設定のときだけ、先に試すべき手を出す。** 既に動いているカードに
2640
+ // 「まずこれをやれ」と出すと、直す必要が無いものを触らせることになる
2641
+ !src && meta.selfServe ? el("p", { class: "sec-preferred", text: meta.selfServe.text }) : null,
2359
2642
  el("p", { class: "sec-how", text: meta.how }),
2643
+ meta.selfServe ? el("p", { class: "sec-how", text: meta.selfServe.note }) : null,
2360
2644
  input,
2361
2645
  el("div", { class: "ds-toggle" }, [btn, msg]),
2362
2646
  ]);
@@ -2381,6 +2665,26 @@ const CLIENT_JS = `
2381
2665
  if (target && target.scrollIntoView) target.scrollIntoView({ behavior: "smooth", block: "center" });
2382
2666
  });
2383
2667
  app.appendChild(wrap);
2668
+ updateConnMapAsOf();
2669
+ }
2670
+
2671
+ /**
2672
+ * 接続マップに「いつ時点か」を付ける/外す。
2673
+ *
2674
+ * **この図はビルド時に焼かれている。** 下の一覧はライブに差し替わるので、ライブが
2675
+ * 取れているときは図だけが古い。同じ画面で図が⚠️・直下が「接続OK」になる状態を
2676
+ * 黙って出さないよう、図がいつ時点かを明示する(図の再描画は別途)。
2677
+ * ライブが取れていないときは図と一覧が同じ時点なので、注記は出さない。
2678
+ */
2679
+ function updateConnMapAsOf() {
2680
+ var wrap = app.querySelector(".conn-map");
2681
+ if (!wrap) return;
2682
+ var existing = wrap.querySelector(".conn-map-asof");
2683
+ if (existing) existing.parentNode.removeChild(existing);
2684
+ if (!LIVE_FLEET || !SITE.fleetGeneratedAt) return;
2685
+ var mapAsOf = dsFmtTime(SITE.fleetGeneratedAt);
2686
+ if (!mapAsOf) return;
2687
+ wrap.appendChild(el("p", { class: "conn-map-asof", text: "この図は " + mapAsOf + " 時点です(下の一覧は最新)。" }));
2384
2688
  }
2385
2689
  function opsCard(icon, title, desc, kind, btnLabel) {
2386
2690
  var btn = el("button", { class: "ops-card-btn", text: "+ " + btnLabel });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.57",
3
+ "version": "0.4.58",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,