@takagaki/cortex-decisions-viewer 0.12.21 → 0.12.23

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.
Files changed (2) hide show
  1. package/dist/render.js +381 -134
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -756,10 +756,10 @@ function sanitizeHarnessShelf(v) {
756
756
  * 写し忘れても赤くならなかったので、突き合わせは viewer/test/state-vocab-parity.test.mjs。
757
757
  */
758
758
  const STATE_STATUSES = {
759
- reachable: ["ok", "not_in_channel", "missing_scope", "no_token", "unreachable", "unknown", "not_in_space", "no_credentials"],
759
+ reachable: ["ok", "not_in_channel", "missing_scope", "no_token", "unreachable", "unknown", "not_in_space", "no_credentials", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
760
760
  ingested: ["ok", "failed", "skipped"],
761
761
  };
762
- const STATE_ROW_FIELDS = ["cap", "ref", "workspace", "question", "status", "at", "by", "needed", "lastOkAt", "lastFailAt"];
762
+ const STATE_ROW_FIELDS = ["cap", "ref", "workspace", "question", "status", "at", "by", "needed", "detail", "lastOkAt", "lastFailAt"];
763
763
  /** stateNow はライブ専用キーで build.ts の網にかからない。画面側でも許可リストを通す */
764
764
  function sanitizeStateNow(v) {
765
765
  if (!Array.isArray(v))
@@ -820,6 +820,11 @@ function sanitizeStateNow(v) {
820
820
  * 書いた行だけスコープ名が消え、「権限が足りません」とだけ出て何を直せばよいか
821
821
  * 分からない状態に戻る。上書きするときは空でも必ず載せる——夜間の古い needed が
822
822
  * 新しい判定の横に残ると、直したはずのスコープ名がいつまでも出る
823
+ *
824
+ * **detail(最後に測った理由)も載せる(#346)。** 夜間が書く理由文はここを通らないと
825
+ * 画面に届かない(fleet-status.json には焼き込まれないので、ライブが届かない案件では出ない
826
+ * ——それは意図。古いスナップショットの理由を出すより、出さないほうが嘘にならない)。
827
+ * needed と同じく、上書きするときは空でも必ず載せる
823
828
  */
824
829
  function overlayStateOnFleet(view, rows) {
825
830
  const v = view || {};
@@ -855,6 +860,7 @@ function overlayStateOnFleet(view, rows) {
855
860
  gateAt: hit.at,
856
861
  gateBy: hit.by,
857
862
  needed: hit.needed || "",
863
+ detail: hit.detail || "",
858
864
  });
859
865
  });
860
866
  return changed ? Object.assign({}, v, { externalSources: externals }) : v;
@@ -937,6 +943,13 @@ function gchatRowsToAdd(externals, rows) {
937
943
  *
938
944
  * **種別(issues / discussions)は登録ファイルの宣言をそのまま使う。** 無いときだけ
939
945
  * Issues に倒す——ここで決め打ちすると、Discussions だけを登録した案件のラベルが嘘になる。
946
+ *
947
+ * 🔴 **`origin: "explicit"` を付ける(#354)。** 「登録から削除」は `origin === "explicit"` の
948
+ * 行にだけ出る(導出リポに出すと押しても翌日戻る)。ここで足す行は `dev-sources.json` の
949
+ * `sources`(人が登録簿に書いたもの)からしか来ないので、明示登録であることは事実。
950
+ * 付けないと、**登録した直後の行だけ「登録から削除」が無い**(夜間のまとめ表が追いつく
951
+ * 翌朝まで、読めていない主役の行を外す手段が画面に無い——cortex-test で実際に踏んだ)。
952
+ * Slack の同じ穴を #221 で `chatRowsToAdd` が塞いだのと同型。
940
953
  */
941
954
  function devRowsToAdd(externals, rows) {
942
955
  const have = {};
@@ -951,7 +964,7 @@ function devRowsToAdd(externals, rows) {
951
964
  continue;
952
965
  if (have[String(r.ref)])
953
966
  continue;
954
- out.push({ type: r.type || "github-issues", name: r.ref, ref: r.ref });
967
+ out.push({ type: r.type || "github-issues", name: r.ref, ref: r.ref, origin: "explicit" });
955
968
  }
956
969
  return out;
957
970
  }
@@ -1621,7 +1634,7 @@ function keyMissingText(cap) {
1621
1634
  */
1622
1635
  const BAD_STATE_STATUSES = {
1623
1636
  ingested: ["failed", "skipped"],
1624
- reachable: ["not_in_channel", "missing_scope", "no_token", "unreachable", "not_in_space", "no_credentials"],
1637
+ reachable: ["not_in_channel", "missing_scope", "no_token", "unreachable", "not_in_space", "no_credentials", "no_consent", "transcripts_api_disabled", "no_access_policy", "speaker_attribution_off", "folder_not_granted"],
1625
1638
  };
1626
1639
  /**
1627
1640
  * タイル1行(と接続グラフの1ノード)の状態を、**正本から導出する**(0.12.0)。
@@ -3320,6 +3333,56 @@ const CLIENT_JS = `
3320
3333
  var SITE = JSON.parse(document.getElementById("site-data").textContent);
3321
3334
  var ADAPTER_ICONS = ${JSON.stringify(ADAPTER_ICONS)};
3322
3335
 
3336
+ // ---- intake(操作の受け口)への共通の呼び出し(#349)----
3337
+ //
3338
+ // **全 op はここを通す。** 名乗り券(ポータルが出す Cookie の cortex-actor)を body に添えるのは
3339
+ // この1箇所だけ。fetch を直書きすると、その op だけ操作者が記録されない。
3340
+ // 券は document.cookie から**1回だけ**読んでメモリに持つ。site-data にも localStorage にも書かない
3341
+ // (ビルド時の値ではなく、開いた人ごとに違う値なので)。
3342
+ //
3343
+ // **1回だけ読むのは仕様。** ポータルからログアウトしても、開いたままのタブは券の期限まで
3344
+ // 名乗り続ける——署名付き Cookie と同じ性質で、券の寿命がそのまま名乗りの寿命になる。
3345
+ var ACTOR_COOKIE = "cortex-actor";
3346
+ var actorTicketCache;
3347
+ function actorTicket() {
3348
+ if (actorTicketCache !== undefined) return actorTicketCache;
3349
+ actorTicketCache = null;
3350
+ try {
3351
+ var parts = String(document.cookie || "").split(";");
3352
+ for (var i = 0; i < parts.length; i++) {
3353
+ var kv = parts[i].trim();
3354
+ if (kv.indexOf(ACTOR_COOKIE + "=") === 0) { actorTicketCache = kv.slice(ACTOR_COOKIE.length + 1) || null; break; }
3355
+ }
3356
+ } catch (e) { actorTicketCache = null; }
3357
+ return actorTicketCache;
3358
+ }
3359
+ /**
3360
+ * intake の op を1回呼ぶ。POST は JSON の body に名乗り券(あれば)を actor として足す。
3361
+ * GET(opts.method === "GET" のとき)はカスタムヘッダも body も付けない(preflight を増やさないため・#198)。
3362
+ * 戻り値は fetch の Promise そのもの(呼び出し側の .then / await / res.json() はそのまま使える)。
3363
+ */
3364
+ function callIntake(op, body, opts) {
3365
+ var intake = SITE.intake;
3366
+ var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
3367
+ var url = pfx + "op=" + encodeURIComponent(op) + "&t=" + encodeURIComponent(intake.token);
3368
+ var o = opts || {};
3369
+ if (o.method === "GET") return fetch(url, { signal: o.signal });
3370
+ var payload = body && typeof body === "object" ? body : {};
3371
+ var ticket = actorTicket();
3372
+ if (ticket) {
3373
+ var withActor = {};
3374
+ for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) withActor[k] = payload[k];
3375
+ withActor.actor = ticket;
3376
+ payload = withActor;
3377
+ }
3378
+ return fetch(url, {
3379
+ method: "POST",
3380
+ headers: { "content-type": "application/json" },
3381
+ body: JSON.stringify(payload),
3382
+ signal: o.signal,
3383
+ });
3384
+ }
3385
+
3323
3386
  // ---- ビルド時TSと共有する判定(唯一の正本は上の関数定義) ----
3324
3387
  //
3325
3388
  // **写経しない。** 接続マップ(ビルド時にSVGを描く)と画面(ブラウザで一覧を描く)が
@@ -3682,14 +3745,9 @@ ${deriveCapTile.toString()}
3682
3745
  var endBusy = beginBusy(btn, "確認中…");
3683
3746
  setStatus("", false);
3684
3747
  var who = String(name || "").trim();
3685
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
3686
3748
  var res, j;
3687
3749
  try {
3688
- res = await fetch(pfx + "op=promote&t=" + encodeURIComponent(intake.token), {
3689
- method: "POST",
3690
- headers: { "content-type": "application/json" },
3691
- body: JSON.stringify({ projectKey: intake.projectKey, path: path, submitter: who }),
3692
- });
3750
+ res = await callIntake("promote", { projectKey: intake.projectKey, path: path, submitter: who });
3693
3751
  j = await res.json().catch(function () { return {}; });
3694
3752
  } catch (e) {
3695
3753
  endBusy();
@@ -4400,6 +4458,44 @@ ${deriveCapTile.toString()}
4400
4458
  cls: "ds-gate-muted",
4401
4459
  help: "管理者の設定が済むと読み取りが始まります。",
4402
4460
  },
4461
+ // --- ここから Microsoft 365(#335)。**値の正本は infra 側の語彙表**
4462
+ // (slack-gate.mjs の SLACK_GATE_VALUES と connection-state-keys.mjs の
4463
+ // REACHABLE_STATUSES)で、1対1の一致を infra のテストと
4464
+ // viewer/test/state-vocab-parity.test.mjs が見張る。
4465
+ // **判定を返すのは engine の夜間実測(1-E 以降)**——画面は語彙と文面だけを先に持つ
4466
+ // (判定器そのものは infra/lambda/shared の teams-gate.mjs / sharepoint-gate.mjs /
4467
+ // teams-chat-gate.mjs に既にある)。
4468
+ //
4469
+ // **バッジは短語・次の一手は help に分ける**(DS_GATE_NO_TOKEN と同じ作り。
4470
+ // バッジに長文を入れると行が折り返す)。**内部語を出さない**——証明書・トークン・
4471
+ // テナント・サービスプリンシパルは、読む人が自分の画面で探せない言葉
4472
+ "no_consent": {
4473
+ text: "管理者の承認が必要です",
4474
+ cls: "ds-gate-warn",
4475
+ help: "貴社の Microsoft 365 管理者が承認リンクを開いて許可すると読み取りが始まります。承認を取り消した場合もこの表示になります。",
4476
+ },
4477
+ "transcripts_api_disabled": {
4478
+ text: "文字起こしの外部取得がオフです",
4479
+ cls: "ds-gate-warn",
4480
+ help: "Teams 管理センターの会議設定で「文字起こしへの API アクセス」をオンにしてください。",
4481
+ },
4482
+ "no_access_policy": {
4483
+ text: "会議へのアクセス許可が未設定です",
4484
+ cls: "ds-gate-warn",
4485
+ help: "管理者がこの主催者にアクセス ポリシーを割り当てると読み取りが始まります(反映まで最大30分)。",
4486
+ },
4487
+ // **承認済み・API アクセスもオンなのに、話者名のトグルだけがオフ**という状態が実在する。
4488
+ // transcripts_api_disabled に寄せると、既にオンにしてある設定をもう一度案内することになる
4489
+ "speaker_attribution_off": {
4490
+ text: "話者名付きの文字起こしがオフです",
4491
+ cls: "ds-gate-warn",
4492
+ help: "Teams 管理センターの会議設定で話者名付き文字起こしをオンにしてください。オフのままだと取り込みません。",
4493
+ },
4494
+ "folder_not_granted": {
4495
+ text: "フォルダへのアクセス許可がありません",
4496
+ cls: "ds-gate-warn",
4497
+ help: "SharePoint 管理者がこのフォルダに読み取り許可を付与すると取り込みが始まります。",
4498
+ },
4403
4499
  "unknown": { text: "不明", cls: "ds-gate-muted" },
4404
4500
  };
4405
4501
  /* ---------------------------------------------------------------------------
@@ -4472,6 +4568,126 @@ ${deriveCapTile.toString()}
4472
4568
  cls: "ds-gate-muted",
4473
4569
  },
4474
4570
  };
4571
+ /**
4572
+ * 休止状態の行に、状態の語に添えて**押す前から**出す定型の補足(#346)と、
4573
+ * 「接続を確認」を押した結果に添える定型の1文(#356)。
4574
+ * 鍵は「アダプタ種別(helpTypeOf)× 状態」。値は { resting, probed } で、どちらも文の
4575
+ * パーツの配列。文字列はテキスト、{ link: <リンクの文字列> } は GitHub App の
4576
+ * インストール設定へのリンク(GITHUB_APP_INSTALL_URL)。
4577
+ *
4578
+ * ## 文の長さ(#356・オーナー却下を受けた原則)
4579
+ * **帯は症状だけ・行は具体的な一手だけ・説明を繰り返さない。** 0.12.22 では帯と行と
4580
+ * 押した結果に同じ admin の4文が3回出ており、オーナーが「長すぎ・冗長」と却下した。
4581
+ * - resting … **1文+リンクだけ**(誰が・どこで。「何が起きているか」は帯が言う)
4582
+ * - probed … **状態に応じた1文だけ**(1行目のサーバの needed が言わないことだけ)。
4583
+ * 押した結果に resting を繰り返さない——resting は押した瞬間に隠れ、needed が
4584
+ * 同じ操作(Configure から追加)を宛先つきで言う
4585
+ * 「admin」の語は画面に1回だけ(github-state-reason.test.mjs が押す前・押した後で数える)。
4586
+ *
4587
+ * ## なぜ画面の表で持つのか
4588
+ * サーバの needed / detail は「何が起きているか」と「どこを開くか」までを持つが、
4589
+ * **誰がその操作をできるか**(リポジトリの admin)と **request の意味**(組織オーナーの
4590
+ * 承認待ち)は、夜間(engine)とボタン(Lambda)の2つの書き手が別々に文を作るので、
4591
+ * サーバ側で足すと片方だけ古くなる。判定と URL はサーバ、定型の補足はここ、という役割分担。
4592
+ *
4593
+ * 🔴 **DS_GATE には入れない**(実測が返す語彙の表で、突き合わせテストの対象)。
4594
+ * DS_GATE_NO_TOKEN と同じく、仕組みはアダプタ非依存・データだけがアダプタ固有。
4595
+ *
4596
+ * 🔴 **DS_GATE[状態] が help を持つ状態にはエントリを置かない。** 置くと行の補足が2段になる
4597
+ * (github-state-reason.test.mjs の「行の補足は最大1段」が見張る)。
4598
+ *
4599
+ * 🔴 **URL を文字列で書き写さない**(帯・押した結果と同じ定数を使う。写経すると張り替えで
4600
+ * 片方だけ古くなる)。
4601
+ *
4602
+ * エントリがある状態では**この文だけ**を出し、サーバの needed / detail は出さない
4603
+ * ——理由文と補足が issues / discussions の2行 × N リポに並ぶと、一覧が案内で埋まる
4604
+ * (DS_GATE_NO_TOKEN.github の項と同じ判断)。needed は押した結果が持つ。
4605
+ *
4606
+ * ⚠ **「移動・改名・削除の可能性 → 登録から外してください」は出さない。** App のトークン
4607
+ * では「未選択」と「消えた・改名」がどちらも 404 で**区別できない**(github-gate.mjs)。
4608
+ * 状態で出し分ける材料が無いので書かない。消えたリポジトリの逃げ道は、行に常に出る
4609
+ * 「登録から削除」(#354)が担う。区別する信号ができたら probed に足す
4610
+ */
4611
+ var DS_GATE_RESTING_HELP = {
4612
+ github: {
4613
+ unreachable: {
4614
+ // 誰が(admin)・どこで(Configure の Repository access)だけ。理由は帯、宛先はリンクが持つ。
4615
+ // 着地点(Repository access)を名指しするのは、押す前の画面からこの語が消えないため
4616
+ //(設計レビュー②)。「admin が…してください」はオーナーの原文の形
4617
+ resting: [
4618
+ "このリポジトリの admin(管理者権限を持つ人)が、Configure の Repository access に追加してください。",
4619
+ { link: "インストール設定を開く" },
4620
+ ],
4621
+ // 2026-09-02 の実測: admin でないリポジトリを Configure で選ぶと「request」タグが付き、
4622
+ // 組織オーナーの承認待ちになる("Okay, updated" と出るのに対象に入らない)。
4623
+ // 1行目(サーバの needed)はこれを言わないので、ここだけが持つ
4624
+ probed: ["GitHub 側で request と表示された場合は、組織オーナーの承認待ちです。"],
4625
+ },
4626
+ },
4627
+ };
4628
+ /**
4629
+ * 表を引くときのアダプタ種別。**probeTypeOf を使わない**——あちらは「op=probe の受け口」の
4630
+ * 語彙で、Google Chat に空文字を返す(実測の口が無いため)。ここで使うと Google Chat の
4631
+ * エントリを永久に置けない
4632
+ */
4633
+ function helpTypeOf(type) {
4634
+ var t = String(type || "");
4635
+ return t.indexOf("github") === 0 ? "github" : t;
4636
+ }
4637
+ function restingHelpEntryOf(type, gate) {
4638
+ var byType = DS_GATE_RESTING_HELP[helpTypeOf(type)];
4639
+ return (byType && byType[gate]) || null;
4640
+ }
4641
+ /** 休止状態の行に出すパーツ(無ければ null) */
4642
+ function restingHelpPartsOf(type, gate) {
4643
+ var e = restingHelpEntryOf(type, gate);
4644
+ return (e && e.resting) || null;
4645
+ }
4646
+ /** 押した結果に添えるパーツ(無ければ null)。行と同じ表から引く(写経しない) */
4647
+ function probedHelpPartsOf(type, gate) {
4648
+ var e = restingHelpEntryOf(type, gate);
4649
+ return (e && e.probed) || null;
4650
+ }
4651
+ /** パーツを1段落にする。包む要素の class は置き場所で違う(行は ds-gate-help・押した結果は ds-probe-help) */
4652
+ function restingHelpNode(parts, cls) {
4653
+ var p = el("p", { class: cls });
4654
+ parts.forEach(function (part) {
4655
+ if (typeof part === "string") p.appendChild(document.createTextNode(part));
4656
+ else if (part && part.link) p.appendChild(el("a", { href: GITHUB_APP_INSTALL_URL, target: "_blank", rel: "noopener", text: part.link }));
4657
+ });
4658
+ return p;
4659
+ }
4660
+ /**
4661
+ * 休止状態の行に出す「最後に測った理由」(表にエントリが無い状態のフォールバック)。
4662
+ * needed(次の一手・URL 込み)を優先し、無ければ detail。
4663
+ *
4664
+ * 🔴 **書き手が button の detail は出さない。** ボタンは detail に判定の語そのもの
4665
+ * ("unreachable")や生のエラー("ratelimited")を書く——lastFailDetail を空で潰さない
4666
+ * ための痕跡で、表示用の値ではない(intake の op=probe のコメント)。人向けの文を持つのは
4667
+ * needed の側。夜間(gold)の detail は人向けの文なので出す
4668
+ */
4669
+ function stateReasonOf(s) {
4670
+ if (s.needed) return String(s.needed);
4671
+ if (s.detail && s.gateBy !== "button") return String(s.detail);
4672
+ return "";
4673
+ }
4674
+ /**
4675
+ * この行に、押す前から理由・補足を出してよいか(#346)。**語の列挙で持たない**——
4676
+ * 別の作業が DS_GATE に語や help を足した日に、黙って漏れたり2段になったりしないよう
4677
+ * 導出で決める:
4678
+ * - 状態がある(未測定の行には出すものが無い)
4679
+ * - ok でない
4680
+ * - missing_scope でない(スコープ名の札 .ds-probe-scope が既に持つ)
4681
+ * - no_token でない(GitHub は組織の帯が導線を1つだけ持つ設計・Slack は DS_GATE_NO_TOKEN の help)
4682
+ * - not_in_channel でない(help は無いが、**状態の語そのものが操作の指示**
4683
+ * 「Slackで招待してください」。その下に夜間の「読めませんでした: not_in_channel」を
4684
+ * 並べても情報が増えず、招待という具体的な指示を薄める。help の有無とは別の軸)
4685
+ * - dsGateOf(s).help が無い(あれば DS_GATE の固定文が次の一手を持っている)
4686
+ */
4687
+ function showsRestingReason(s) {
4688
+ if (!s.gate || s.gate === "ok" || s.gate === "missing_scope" || s.gate === "no_token" || s.gate === "not_in_channel") return false;
4689
+ return !dsGateOf(s).help;
4690
+ }
4475
4691
  /**
4476
4692
  * Slack のチャンネルが社内ワークスペースのものか。**workspace の有無だけで決まる**
4477
4693
  * (省略=社内。#104 でこの値が画面まで届いていることは slack-workspace.test.mjs が見張る)。
@@ -4796,12 +5012,7 @@ ${deriveCapTile.toString()}
4796
5012
  msg.className = "ds-toggle-msg";
4797
5013
  msg.textContent = "承認画面を準備しています…";
4798
5014
  try {
4799
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
4800
- var res = await fetch(pfx + "op=slack-oauth-start&t=" + encodeURIComponent(intake.token), {
4801
- method: "POST",
4802
- headers: { "content-type": "application/json" },
4803
- body: "{}",
4804
- });
5015
+ var res = await callIntake("slack-oauth-start", {});
4805
5016
  var j = await res.json().catch(function () { return {}; });
4806
5017
  if (!res.ok || !j.url) {
4807
5018
  // サーバの文言をそのまま出す(未設定のときは「準備中です」が返る)
@@ -4860,7 +5071,6 @@ ${deriveCapTile.toString()}
4860
5071
  orgResolveDone = true;
4861
5072
  var intake = SITE.intake;
4862
5073
  if (!intake) return;
4863
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
4864
5074
  // **描画の途中で投げない。** ここは行を組み立てている最中に呼ばれるので、
4865
5075
  // 例外が出ると**画面そのものが描かれない**(fetch が無い環境で実際にそうなった)。
4866
5076
  // 判定し直しは「できたら直る」性質のもので、失敗しても行は
@@ -4872,11 +5082,7 @@ ${deriveCapTile.toString()}
4872
5082
  // fetch が存在しない(ReferenceError)と、呼び出しが投げる
4873
5083
  if (typeof fetch !== "function") return;
4874
5084
  try {
4875
- fetch(pfx + "op=slack-resolve&t=" + encodeURIComponent(intake.token), {
4876
- method: "POST",
4877
- headers: { "content-type": "application/json" },
4878
- body: "{}",
4879
- })
5085
+ callIntake("slack-resolve", {})
4880
5086
  .then(function (res) { return res.ok ? res.json() : null; })
4881
5087
  .then(function (j) {
4882
5088
  if (!j || !j.resolved) return;
@@ -5017,9 +5223,14 @@ ${deriveCapTile.toString()}
5017
5223
  var owners = githubOwnersView();
5018
5224
  var out = [];
5019
5225
  groupGithubByOwner(items, owners).forEach(function (g) {
5020
- // **行ごとに meta を引く。** issues と discussions でアイコンとラベルが違う
5226
+ // **行ごとに meta を引く。** issues と discussions でアイコンとラベルが違う。
5227
+ // Issues と Discussions は同じ接続先(同じ dev 行)なので、理由・補足は1本目だけが
5228
+ // 持つ(#346)。同じ長文が2行に並ぶと一覧が案内で埋まる
5229
+ var seenRef = {};
5021
5230
  var cards = g.items.map(function (x) {
5022
- return extSourceCard(x, DS_TYPE[x.type] || { icon: "🔗", label: x.type || "その他" });
5231
+ var again = !!(x.ref && seenRef[x.ref]);
5232
+ if (x.ref) seenRef[x.ref] = 1;
5233
+ return extSourceCard(x, DS_TYPE[x.type] || { icon: "🔗", label: x.type || "その他" }, { quietHelp: again });
5023
5234
  });
5024
5235
  if (g.state === "warn") {
5025
5236
  out.push(orgNoteBox(githubNotConnectedNodes(g), cards));
@@ -5082,34 +5293,58 @@ ${deriveCapTile.toString()}
5082
5293
  var repos = o.repos != null ? o.repos : g.repos != null ? g.repos : g.items.length;
5083
5294
  var unread = o.unreadable != null ? o.unreadable : g.unread != null ? g.unread : g.items.length;
5084
5295
  var installed = o.appInstalled;
5085
- out.push(el("div", { class: "ds-orgnote-title", text: "⚠️ " + g.title + " はまだ接続されていません" }));
5086
5296
  // **読める件数を出す。** 総数だけ出すと、同じ画面の集計と食い違う(#101 で3度直した形)
5087
5297
  var head = repos + "リポジトリ中 " + unread + "件が読めていません。";
5298
+ // 🔴 **見出しは 「installed」 の分岐の中で出す(#346)。** ここで1つに畳んでいたので、
5299
+ // App 導入済みの組織にも「まだ接続されていません」と出て、**同じ帯の本文と矛盾**した
5300
+ // (実機でオーナーが踏み、利用者が「組織の接続からやり直し」と誤解した)
5088
5301
  if (installed === true) {
5302
+ // 🔴 **帯は症状だけ(#356・オーナー却下を受けた原則)。** 0.12.22 ではここに9文
5303
+ // (App の説明・Configure の手順・admin の4文・移動改名削除・お客様の組織)を入れ、
5304
+ // 同じ説明が行と押した結果にも出て3回繰り返された。見出しは件数だけ・本文は2文だけ。
5305
+ // **組織名は見出しに入れない**——各行のバッジ「{組織}(GitHub App)」が持っている。
5306
+ // 組織が複数ある画面でも、組織ごとの帯のまま・見出しは同じ形。
5307
+ //
5308
+ // **見出しでは「読み取り対象に選ばれていません」と言い切らない。** 「appInstalled」 が
5309
+ // 分けられるのは「①未インストール」と「②未選択/③消えた・改名」までで、**②と③は
5310
+ // 分かれない**(App のトークンではどちらも 404)。見出しは事実だけを言う
5311
+ out.push(el("div", {
5312
+ class: "ds-orgnote-title",
5313
+ text: "⚠️ " + unread + "件のリポジトリを読み取れていません",
5314
+ }));
5089
5315
  // App は入っている。**インストールをもう一度促さないこと**——済んでいる操作を促すのは
5090
- // 「設定する必要が無いものに設定を促す」偽の警告と同じで、しかも直らない
5316
+ // 「設定する必要が無いものに設定を促す」偽の警告と同じで、しかも直らない。
5317
+ //
5318
+ // 「誰が・どこで」は各行の1文(DS_GATE_RESTING_HELP)が持つ。ここで繰り返さない。
5319
+ // 「移動・改名・削除の可能性 → 登録から外して」も言わない——②と③を区別する材料が
5320
+ // 無く、外す手段は行の「登録から削除」(#354)が常に持つ。
5321
+ // 「お客様の組織では、組織のオーナーに承認を依頼」も外した——request(承認待ち)の
5322
+ // 1文は押した結果が持つ(同じ表の probed)。
5091
5323
  //
5092
- // **原因を1つに断定しない(2026-08-21)。**appInstalled」 が分けられるのは
5093
- // 「①未インストール」と「②未選択/③消えた・改名」までで、**②と③は分かれない**。
5094
- // 「対象に選ばれていません」と言い切ると、③の人は一覧に無いリポジトリを
5095
- // 探し続けることになる(押しても直らない導線と同じ形)。逃げ道を1文で示す。
5324
+ // **件数の文 head」(「{N}リポジトリ中 {M}件が読めていません。」)もこの枝では
5325
+ // 出さない(#356)。** #101 で3度直した「読める件数を出す」を意図して覆した——見出しが
5326
+ // 件数を持つので、本文で二重に言うと帯が3文になる。分母(登録数)は画面から消えるが、
5327
+ // 箱の中に行が全部並んでいるので数えられる。false / null の枝は従来どおり head を出す。
5096
5328
  //
5097
- // **設定画面の宛先も断定しない。** リンク先は 「/settings/installations」=
5098
- // **押した本人の**インストール一覧で(appSettingsLink の項に、組織の設定ページが
5099
- // 管理者以外で 404 になる理由がある)、App を入れたのが別のメンバーや
5100
- // 顧客側のオーナーなら**着いた先は空**。そこで詰まる人に次の宛先を渡す。
5329
+ // **「各行の案内」ではなく「アクセス不可の行の案内」と言う。** 帯の箱には読めている行も
5330
+ // 一緒に入る(unread が1件でもあれば組織ごと warn)。案内を持つのは「アクセス不可」の
5331
+ // 行だけなので、行に出ている状態の語そのもので指す(設計レビュー②の指摘)。
5332
+ //
5333
+ // **「読み取り対象に入っていません」はオーナーの文(#356)。** 2026-08-21 の
5334
+ // 「②未選択と③消えた・改名を区別できないので断定しない」は、帯の本文については
5335
+ // この文で覆した(見出しは引き続き断定しない)。③の逃げ道は「登録から削除」。
5336
+ //
5337
+ // **設定画面の宛先は App 個別ページ(#346)。** いまの宛先と、そこに着ける人の条件は
5338
+ // 「appSettingsLink」 の項にある
5101
5339
  out.push(el("p", {
5102
5340
  class: "ds-group-note",
5103
- text: head + "GitHub App はこの組織に入っていますが、これらのリポジトリが読み取りの対象に選ばれていません。"
5104
- + "インストール設定を開いて、対象のリポジトリを追加してください。"
5105
- + "設定は App を入れた方の画面に出ます。見当たらない場合は、この組織で App を入れた方にご依頼ください。"
5106
- + "リポジトリ自体が一覧に見当たらない場合は、移動・改名・削除されている可能性があるので、登録から外してください。"
5107
- + "お客様の組織では、組織のオーナーに承認を依頼してください。",
5341
+ text: "GitHub App の読み取り対象に入っていません。アクセス不可の行の案内に従って追加してください。",
5108
5342
  }));
5109
5343
  out.push(appSettingsLink("インストール設定を開く →"));
5110
5344
  return out;
5111
5345
  }
5112
5346
  if (installed === false) {
5347
+ out.push(el("div", { class: "ds-orgnote-title", text: "⚠️ " + g.title + " はまだ接続されていません" }));
5113
5348
  out.push(el("p", {
5114
5349
  class: "ds-group-note",
5115
5350
  text: head + "この組織に Cortex の GitHub App を入れると読めるようになります(Issues の読み取りだけ)。"
@@ -5120,7 +5355,13 @@ ${deriveCapTile.toString()}
5120
5355
  }
5121
5356
  // 「null」(確かめられなかった)と、この値を知らない古い fleet-status.json。
5122
5357
  // **導線を出さない。** どちらが原因か分からないので、押させると外れたほうを
5123
- // 押させることになる(済んでいるインストールをもう一度、あるいは効かない設定画面へ)
5358
+ // 押させることになる(済んでいるインストールをもう一度、あるいは効かない設定画面へ)。
5359
+ //
5360
+ // 見出しは件数を言わない(件数は本文だけが持つ・見出しと二重に言わない)。
5361
+ // 「githubUnsureNodes」 の「接続を確認できませんでした」(=その夜、確かめる処理自体が
5362
+ // 走らなかった)とは意味が違う——こちらは**読めなかった事実はあるが、App の導入状況が
5363
+ // 分からない**ので、語を分ける
5364
+ out.push(el("div", { class: "ds-orgnote-title", text: "⚠️ " + g.title + " のリポジトリが読めていません(原因は確かめられていません)" }));
5124
5365
  out.push(el("p", {
5125
5366
  class: "ds-group-note",
5126
5367
  text: head + "原因(GitHub App が入っていない/入っているがこれらのリポジトリが選ばれていない)は、"
@@ -5291,14 +5532,8 @@ ${deriveCapTile.toString()}
5291
5532
  btn.disabled = true;
5292
5533
  msg.className = "ds-toggle-msg";
5293
5534
  msg.textContent = "実行中…";
5294
- var intake = SITE.intake;
5295
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
5296
5535
  try {
5297
- var res = await fetch(pfx + "op=sync&t=" + encodeURIComponent(intake.token), {
5298
- method: "POST",
5299
- headers: { "content-type": "application/json" },
5300
- body: JSON.stringify({ kind: kind }),
5301
- });
5536
+ var res = await callIntake("sync", { kind: kind });
5302
5537
  var j = await res.json().catch(function () { return {}; });
5303
5538
  // **起こせなかったときも「失敗」と言わない。** 定期実行が拾うので、
5304
5539
  // 押した人にできることは無い。何が起きるかだけを伝える
@@ -5360,29 +5595,37 @@ ${deriveCapTile.toString()}
5360
5595
  /**
5361
5596
  * すでに入っている App の**インストール設定**(対象リポジトリの追加)へ行くリンク。
5362
5597
  *
5363
- * **インストールとは別の宛先。** 「installations/newは「入れる」ための画面で、
5364
- * すでに入っている組織の人が押しても対象リポジトリは増えない。
5365
- * リポジトリの追加は、その人が入れた先(個人 or 組織)の設定画面で行う。
5366
- *
5367
- * **組織名を URL に埋め込まない。** 組織ごとの設定ページは
5368
- * 「github.com/organizations/<組織>/settings/installations」 だが、**その組織の
5369
- * 管理者でないと 404 になる**。読み取り対象の組織のオーナーが Cortex の利用者とは
5370
- * 限らない(お客様の組織がまさにそれ)ので、404 に飛ばすより、
5371
- * 本人のインストール一覧から辿らせるほうが確実に着く。
5372
- * ——「GITHUB_APP_INSTALL_URL」 の「確かめられないものを顧客に押させない」と同じ判断。
5373
- *
5374
- * ⚠ **ただし「押した本人が入れた App」しか出ない。** App を入れたのが別のメンバーや
5375
- * 顧客側のオーナーなら、着いた先は空になる。だから**案内の本文の側が**
5376
- * 「App を入れた方の画面に出ます/見当たらない場合はその方にご依頼ください」と断っている
5377
- * (githubNotConnectedNodes)。**リンクだけ残して本文の断りを消さないこと**——
5378
- * 消すと「開けば必ずそこにある」前提の案内に戻り、着いた先が空の人が詰む。
5598
+ * **宛先は App 個別ページ(「GITHUB_APP_INSTALL_URLと同じ定数・#346)。**
5599
+ * 「infra/lambda/shared/github-gate.mjs」 の 「githubConfigureUrl」 とも同じ宛先なので、
5600
+ * **押す前(この帯・行の補足)と押した後(確認結果の文中)でリンク先が割れない**。
5601
+ * 関数は残してある——呼び手が「入れる」ではなく「設定を開く」意図で呼ぶ場所だから。
5602
+ *
5603
+ * ## なぜ他の2つの宛先を使わないか
5604
+ *
5605
+ * - 個人の一覧 「github.com/settings/installations」 は**「個人アカウントに入れた App」の
5606
+ * 画面**で、組織のインストールは別立て
5607
+ * (https://docs.github.com/en/apps/using-github-apps/reviewing-and-modifying-installed-github-apps )。
5608
+ * 押した本人が入れた App しか出ないので、**別の人が入れた組織では空**になる。
5609
+ * 0.12.21 まではこちらを指しており、着いた先が空の人が詰まった(#346)
5610
+ * - 組織の設定ページ 「github.com/organizations/<組織>/settings/installations」 は
5611
+ * **owner 以外 404**(2026-09-01 オーナーが踏んだ実害)。読み取り対象の組織の
5612
+ * オーナーが Cortex の利用者とは限らない(お客様の組織がまさにそれ)
5613
+ *
5614
+ * ⚠ **App 個別ページが「導入済みの組織で Configure に着く」ことは、GitHub の文書に
5615
+ * 記述が無い。根拠は 2026-09-02 のオーナーの実測**(cortex-test と GitHub の Configure
5616
+ * 画面で、admin のリポジトリは即追加・admin でないリポジトリには 「request」 タグ)。
5617
+ * 文書化されている一次ソースは次の2つで、**着いた先の画面そのものは書かれていない**:
5618
+ * - インストール URL と repository admin の設置条件
5619
+ * https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party
5620
+ * - 非オーナーの申請は組織オーナーへ通知される
5621
+ * https://docs.github.com/en/apps/using-github-apps/requesting-a-github-app-from-your-organization-owner
5622
+ * **GitHub 側の画面が変わったらここが最初に嘘になる。** 宛先を変えるときは
5623
+ * 「githubConfigureUrl」(サーバ)と揃えること。
5379
5624
  */
5380
- var GITHUB_APP_SETTINGS_URL = "https://github.com/settings/installations";
5381
-
5382
5625
  function appSettingsLink(label) {
5383
5626
  return el("a", {
5384
5627
  class: "ds-owner-install",
5385
- href: GITHUB_APP_SETTINGS_URL,
5628
+ href: GITHUB_APP_INSTALL_URL,
5386
5629
  target: "_blank", rel: "noopener",
5387
5630
  text: label || "インストール設定を開く →",
5388
5631
  });
@@ -6975,11 +7218,9 @@ ${deriveCapTile.toString()}
6975
7218
  var giveUp = ctrl ? setTimeout(function () { ctrl.abort(); }, LIVE_CONFIRM_TIMEOUT_MS) : null;
6976
7219
  // **成功・失敗の両方で外す。** 残すと、window を閉じないテストで node --test が終わらない
6977
7220
  var clearGiveUp = function () { if (giveUp != null) { clearTimeout(giveUp); giveUp = null; } };
6978
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
6979
- // GET で取る。**カスタムヘッダは付けない**(preflight を1往復増やさないため)
6980
- fetch(pfx + "op=harness-catalog&t=" + encodeURIComponent(intake.token), {
6981
- signal: ctrl ? ctrl.signal : undefined,
6982
- })
7221
+ // GET で取る。**カスタムヘッダは付けない**(preflight 1往復増やさないため)。
7222
+ // 名乗り券は body に載せる仕組みなので、GET のこの op には付かない(#349)
7223
+ callIntake("harness-catalog", null, { method: "GET", signal: ctrl ? ctrl.signal : undefined })
6983
7224
  .then(function (res) {
6984
7225
  clearGiveUp();
6985
7226
  if (!res.ok) throw new Error("http " + res.status);
@@ -8216,9 +8457,13 @@ ${deriveCapTile.toString()}
8216
8457
  * 「<a>」 の組み立てだけで作る(この画面の他の表示と同じ流儀)。
8217
8458
  *
8218
8459
  * **URL 以外は素のまま出す。** 言い換えると検索できないので、サーバの文言を削らない。
8460
+ *
8461
+ * **行に置くときは 「ds-gate-help」 を付ける**(第2引数)。「.ds-card」 は横並びの flex で、
8462
+ * 自分の行を占めるのは 「flex: 1 1 100%」 の 「.ds-gate-help」 だけ。押した結果で置き換える
8463
+ * とき 「probeSection」 が 「.ds-gate-help」 をまとめて隠すので、この class が要る。
8219
8464
  */
8220
- function neededNode(text) {
8221
- var p = el("p", { class: "ds-probe-help" });
8465
+ function neededNode(text, cls) {
8466
+ var p = el("p", { class: cls || "ds-probe-help" });
8222
8467
  splitNeededParts(text).forEach(function (part) {
8223
8468
  if (part.url) p.appendChild(el("a", { href: part.text, target: "_blank", rel: "noopener", text: part.text }));
8224
8469
  else p.appendChild(document.createTextNode(part.text));
@@ -8233,10 +8478,23 @@ ${deriveCapTile.toString()}
8233
8478
  * 「App が入っていない」と「App は入っているがこのリポジトリが選ばれていない」の
8234
8479
  * 言い分けも、設定ページの URL もあちらが決める——**画面で分岐を書き足さない**
8235
8480
  * (書いた瞬間に判定が2箇所になり、夜間の実測とボタンでずれる)。
8481
+ * **ただし状態に応じた定型の1文だけは画面の表(DS_GATE_RESTING_HELP の probed)から引く**
8482
+ * (#346 → #356)——「request は組織オーナーの承認待ち」のような GitHub 側の意味は、
8483
+ * サーバの2つの書き手が別々に文を作るので、片方だけ古くなる。
8484
+ *
8485
+ * 🔴 **行の常時表示(resting)を押した結果に繰り返さない(#356)。** 0.12.22 では admin の
8486
+ * 4文が行と押した結果の両方に出ていた。行の1文は押した瞬間に隠れ、needed が同じ操作
8487
+ * (Configure から追加)を宛先つきで言うので、ここは needed が言わない1文だけ
8236
8488
  */
8237
8489
  function githubProbeHelpNodes(r) {
8238
8490
  if (r.gate === "ok") return [];
8239
- if (r.needed) return [neededNode(r.needed)];
8491
+ if (r.needed) {
8492
+ var nodes = [neededNode(r.needed)];
8493
+ // 状態に応じた1文は**行と同じ表から引く**(ここに文を直書きすると表と割れる)
8494
+ var probed = probedHelpPartsOf("github", r.gate);
8495
+ if (probed) nodes.push(restingHelpNode(probed, "ds-probe-help"));
8496
+ return nodes;
8497
+ }
8240
8498
  // needed が無いのは gate に落とし込めなかったとき。**生のエラーは隠さない**
8241
8499
  return [el("p", { class: "ds-probe-help", text: "GitHub の応答:" + (r.error || "不明") + "。時間をおいて再度お試しください。" })];
8242
8500
  }
@@ -8380,12 +8638,7 @@ ${deriveCapTile.toString()}
8380
8638
  if (!intake) return;
8381
8639
  var payload = { op: "refresh-channel-name", ref: s.ref };
8382
8640
  if (s.workspace) payload.workspace = s.workspace;
8383
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
8384
- fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
8385
- method: "POST",
8386
- headers: { "content-type": "application/json" },
8387
- body: JSON.stringify(payload),
8388
- }).then(function (res) {
8641
+ callIntake("config", payload).then(function (res) {
8389
8642
  if (!res.ok) return;
8390
8643
  // 一覧は夜間のスナップショットなので、その場では変わらない。行だけ追いつかせる
8391
8644
  var nameEl = card.querySelector(".ds-name");
@@ -8446,13 +8699,8 @@ ${deriveCapTile.toString()}
8446
8699
  // ワークスペースは Slack にしか無い概念なので、他アダプタでは載せない
8447
8700
  var payload = { type: probeTypeOf(s.type), ref: s.ref };
8448
8701
  if (s.workspace) payload.workspace = s.workspace;
8449
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
8450
8702
  try {
8451
- var res = await fetch(pfx + "op=probe&t=" + encodeURIComponent(intake.token), {
8452
- method: "POST",
8453
- headers: { "content-type": "application/json" },
8454
- body: JSON.stringify(payload),
8455
- });
8703
+ var res = await callIntake("probe", payload);
8456
8704
  var j = await res.json().catch(function () { return {}; });
8457
8705
  if (!res.ok) {
8458
8706
  // サーバの文言をそのまま出す(回数制限の案内・未対応のアダプタの説明を含んでいる)
@@ -8485,9 +8733,12 @@ ${deriveCapTile.toString()}
8485
8733
  at.textContent = "いま測った結果(" + (dsFmtTime(j.checkedAt) || "たった今") + ")";
8486
8734
  probeHelpNodes(s, j).forEach(function (n) { body.appendChild(n); });
8487
8735
  // **夜間の判定に添えてあった案内は隠す。** 置き換えた判定と食い違う案内が
8488
- // 下に残ると、同じ行に2つのことが書いてある状態になる
8489
- var stale = card.querySelector(".ds-gate-help");
8490
- if (stale) stale.hidden = true;
8736
+ // 下に残ると、同じ行に2つのことが書いてある状態になる。
8737
+ // **1つ目だけでなく全部隠す(#346)。** 理由文(.ds-gate-reason)・定型補足
8738
+ // (.ds-gate-resting)・DS_GATE help のどれが付いていても、押した結果と
8739
+ // 食い違う案内を残さない
8740
+ var stale = card.querySelectorAll(".ds-gate-help");
8741
+ for (var si = 0; si < stale.length; si++) stale[si].hidden = true;
8491
8742
  // 表示名を Slack の実名へ自動で揃える(差分表示も更新ボタンも出さない)。
8492
8743
  // **GitHub は揃えない。** 行の名前は owner/repo そのもので、書き換える口
8493
8744
  //(op=config の refresh-channel-name)は channels.json 専用
@@ -9784,17 +10035,12 @@ ${deriveCapTile.toString()}
9784
10035
  async function sendConfig(payload, msg, btn, onDone, hooks) {
9785
10036
  var intake = SITE.intake;
9786
10037
  if (!intake) return;
9787
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
9788
10038
  // 押している間はボタンごと「処理中…」にする(#222・beginBusy の項)
9789
10039
  var endBusy = beginBusy(btn);
9790
10040
  msg.className = "ds-toggle-msg";
9791
10041
  msg.textContent = "保存中…";
9792
10042
  try {
9793
- var res = await fetch(pfx + "op=config&t=" + encodeURIComponent(intake.token), {
9794
- method: "POST",
9795
- headers: { "content-type": "application/json" },
9796
- body: JSON.stringify(payload),
9797
- });
10043
+ var res = await callIntake("config", payload);
9798
10044
  var j = await res.json().catch(function () { return {}; });
9799
10045
  // 一覧が添えられていれば、断られていても先に画面を追いつかせる(#127)
9800
10046
  var raced = hooks && hooks.onTargets && Array.isArray(j.targets) ? hooks.onTargets(j.targets) : false;
@@ -10278,7 +10524,11 @@ ${deriveCapTile.toString()}
10278
10524
  }
10279
10525
 
10280
10526
 
10281
- function extSourceCard(s, meta) {
10527
+ /**
10528
+ * 接続先1件の行。「opts.quietHelp」 が真なら休止状態の補足(#346)を出さない
10529
+ * ——同じリポジトリの2本目の行(Discussions)用で、同じ長文が2行に並ぶのを避ける
10530
+ */
10531
+ function extSourceCard(s, meta, opts) {
10282
10532
  var badges = el("div", { class: "ds-badges" });
10283
10533
  // **Gold 対象かのバッジは持たない。** 一覧に並ぶ Slack チャンネルは例外なく全て対象で、
10284
10534
  // 行ごとに「対象です」と書いても情報が増えない。
@@ -10458,6 +10708,20 @@ ${deriveCapTile.toString()}
10458
10708
  // **ボタンより後ろに置く。** カードは横並びの flex なので、間に挟むと
10459
10709
  // 「登録を解除」が案内文の下の行へ落ちて、既存案件の行の形が変わる
10460
10710
  if (gate.help) card.appendChild(el("p", { class: "ds-gate-help", text: gate.help }));
10711
+ // **最後に測った理由と直し方を、押す前から出す(#346)。** 「アクセス不可 8時間前に確認」
10712
+ // だけでは、押さない人(初見の PM 等)が止まる。表にエントリがある状態は定型の補足
10713
+ // (リンク込み)だけ、無い状態は needed → detail のフォールバック。どちらも ds-gate-help
10714
+ // を付ける——押した結果で置き換えるとき probeSection がまとめて隠す。
10715
+ // **同じリポジトリの2本目の行(Discussions)には出さない**(opts.quietHelp)。
10716
+ // 同じ長文が2行に並ぶと一覧が案内で埋まる。URL のリンク化は neededNode(innerHTML は使わない)
10717
+ if (!(opts && opts.quietHelp) && showsRestingReason(s)) {
10718
+ var restingParts = restingHelpPartsOf(s.type, s.gate);
10719
+ if (restingParts) card.appendChild(restingHelpNode(restingParts, "ds-gate-help ds-gate-resting"));
10720
+ else {
10721
+ var reason = stateReasonOf(s);
10722
+ if (reason) card.appendChild(neededNode(reason, "ds-gate-help ds-gate-reason"));
10723
+ }
10724
+ }
10461
10725
  // 「接続を確認」(2026-08-19)。**押せる人を絞らない**——読むだけで設定を変えないので、
10462
10726
  // 気づいた人がその場で確かめられるほうがよい(オーナー判断)。書き込みになる
10463
10727
  // 「表示名を更新する」は、押した結果の中から別のボタンとして出る(probeSection)。
@@ -10796,17 +11060,15 @@ ${deriveCapTile.toString()}
10796
11060
  var giveUp = ctrl ? setTimeout(function () { ctrl.abort(); }, LIVE_CONFIRM_TIMEOUT_MS) : null;
10797
11061
  /** 見切りのタイマを外す。**応答が来たら必ず呼ぶ**(残すと次の取得を巻き込んで中断する) */
10798
11062
  var clearGiveUp = function () { if (giveUp != null) { clearTimeout(giveUp); giveUp = null; } };
10799
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
10800
- fetch(pfx + "op=state&t=" + encodeURIComponent(intake.token), {
10801
- method: "POST",
10802
- headers: { "content-type": "application/json" },
10803
- signal: ctrl ? ctrl.signal : undefined,
10804
- // **常に全能力を要求する(ALL_STATE_CAPS の項)。** 知らない値は Lambda 側が黙って
10805
- // 捨てる(断らない)ので、画面と Lambda の版が食い違っても接続状況の表示は消えない
10806
- // **保存直後だけ据え置きを迂回させる。** これが無いと、サーバが保存前の答えを返し、
10807
- // 足りない行が0件になって取り直しも張られない=手で再読み込みするまで追いつかない
10808
- body: JSON.stringify(useFresh ? { caps: ALL_STATE_CAPS, fresh: true } : { caps: ALL_STATE_CAPS }),
10809
- })
11063
+ // **常に全能力を要求する(ALL_STATE_CAPS の項)。** 知らない値は Lambda 側が黙って
11064
+ // 捨てる(断らない)ので、画面と Lambda の版が食い違っても接続状況の表示は消えない
11065
+ // **保存直後だけ据え置きを迂回させる。** これが無いと、サーバが保存前の答えを返し、
11066
+ // 足りない行が0件になって取り直しも張られない=手で再読み込みするまで追いつかない
11067
+ callIntake(
11068
+ "state",
11069
+ useFresh ? { caps: ALL_STATE_CAPS, fresh: true } : { caps: ALL_STATE_CAPS },
11070
+ { signal: ctrl ? ctrl.signal : undefined },
11071
+ )
10810
11072
  // **非2xxも失敗として扱う。** ビューアとLambdaの版は独立に進むので、
10811
11073
  // op=state 未対応のLambdaは 400 を返す。通信エラーだけを見ていると空表示になる。
10812
11074
  .then(function (res) {
@@ -11313,18 +11575,13 @@ ${deriveCapTile.toString()}
11313
11575
  msg.className = "ds-toggle-msg";
11314
11576
  msg.textContent = "保存中…";
11315
11577
  var intake = SITE.intake;
11316
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
11317
11578
  try {
11318
- var res = await fetch(pfx + "op=secret&t=" + encodeURIComponent(intake.token), {
11319
- method: "POST",
11320
- headers: { "content-type": "application/json" },
11321
- // **シークレット名は送らない。** どの連携かだけを送り、書き込み先はサーバが案件から決める。
11322
- // **Backlog はスペースだけ名乗る(#138)。** 名前を組み立てるのはサーバで、
11323
- // こちらが渡すのは「どのスペースの鍵か」という材料だけ。
11324
- // **押した時点で読む。** 組み立て時に固めると、接続先を登録した直後に
11325
- // 貼った鍵が「スペースを特定できません」で断られる
11326
- body: JSON.stringify(secretPayload(intake, kind, value, cardSpace)),
11327
- });
11579
+ // **シークレット名は送らない。** どの連携かだけを送り、書き込み先はサーバが案件から決める。
11580
+ // **Backlog はスペースだけ名乗る(#138)。** 名前を組み立てるのはサーバで、
11581
+ // こちらが渡すのは「どのスペースの鍵か」という材料だけ。
11582
+ // **押した時点で読む。** 組み立て時に固めると、接続先を登録した直後に
11583
+ // 貼った鍵が「スペースを特定できません」で断られる
11584
+ var res = await callIntake("secret", secretPayload(intake, kind, value, cardSpace));
11328
11585
  var j = await res.json().catch(function () { return {}; });
11329
11586
  if (!res.ok) {
11330
11587
  msg.className = "ds-toggle-msg is-error";
@@ -12073,7 +12330,6 @@ ${deriveCapTile.toString()}
12073
12330
  if (!entries || !entries.length) { setStatus("⚠ ファイルを選択してください。", true); return; }
12074
12331
  if (isT && !meeting) { setStatus("⚠ 会議名を選んでください。", true); return; }
12075
12332
  submitBtn.disabled = true;
12076
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
12077
12333
  function fail(msg) { setStatus("✗ " + msg, true); submitBtn.disabled = false; }
12078
12334
  try {
12079
12335
  var uploaded = [];
@@ -12082,10 +12338,8 @@ ${deriveCapTile.toString()}
12082
12338
  setStatus("アップロード中… (" + (i + 1) + "/" + entries.length + ") " + f.name, false);
12083
12339
  var pres;
12084
12340
  try {
12085
- pres = await fetch(pfx + "op=presign&t=" + encodeURIComponent(intake.token), {
12086
- method: "POST", headers: { "content-type": "application/json" },
12087
- body: JSON.stringify({ projectKey: intake.projectKey, filename: f.name, contentType: f.type || "application/octet-stream" }),
12088
- }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12341
+ pres = await callIntake("presign", { projectKey: intake.projectKey, filename: f.name, contentType: f.type || "application/octet-stream" })
12342
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12089
12343
  } catch (net) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
12090
12344
  if (!pres.ok || !pres.j.url) return fail((pres.j && pres.j.message) || "アップロードURLの取得に失敗しました。対応していないファイル形式かもしれません。");
12091
12345
  var put;
@@ -12103,10 +12357,8 @@ ${deriveCapTile.toString()}
12103
12357
  try {
12104
12358
  var payload = { projectKey: intake.projectKey, kind: kind, files: uploaded, submitter: submitter.trim() };
12105
12359
  if (isT) payload.meeting = meeting;
12106
- sub = await fetch(pfx + "op=submit&t=" + encodeURIComponent(intake.token), {
12107
- method: "POST", headers: { "content-type": "application/json" },
12108
- body: JSON.stringify(payload),
12109
- }).then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12360
+ sub = await callIntake("submit", payload)
12361
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
12110
12362
  } catch (net3) { return fail("サーバーに接続できませんでした。もう一度お試しください。"); }
12111
12363
  if (!sub.ok) return fail((sub.j && sub.j.message) || "取り込みに失敗しました。時間をおいて再度お試しください。");
12112
12364
  status.className = "modal-status";
@@ -12464,15 +12716,10 @@ ${deriveCapTile.toString()}
12464
12716
  var endBusy = beginBusy(btn, "削除中…");
12465
12717
  msg.className = "record-delete-msg";
12466
12718
  msg.textContent = "";
12467
- var pfx = intake.url + (intake.url.indexOf("?") === -1 ? "?" : "&");
12468
12719
  var res, j;
12469
12720
  try {
12470
- res = await fetch(pfx + "op=delete-record&t=" + encodeURIComponent(intake.token), {
12471
- method: "POST",
12472
- headers: { "content-type": "application/json" },
12473
- // 名乗りは確認済み昇格で覚えたものを流用する(削除のためだけに名前を聞かない)
12474
- body: JSON.stringify({ projectKey: intake.projectKey, path: path, submitter: rememberedVerifier() }),
12475
- });
12721
+ // 名乗りは確認済み昇格で覚えたものを流用する(削除のためだけに名前を聞かない)
12722
+ res = await callIntake("delete-record", { projectKey: intake.projectKey, path: path, submitter: rememberedVerifier() });
12476
12723
  j = await res.json().catch(function () { return {}; });
12477
12724
  } catch (e) {
12478
12725
  endBusy();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.21",
3
+ "version": "0.12.23",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,