@takagaki/cortex-decisions-viewer 0.12.30 → 0.12.32
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/google-chat-guide-images.js +33 -0
- package/dist/meet-guide-images.js +6 -1
- package/dist/pending-ops.js +140 -17
- package/dist/render.js +488 -83
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UNSETTLED_LIMITS = exports.LIVE_CONFIRM_LIMITS = void 0;
|
|
3
|
+
exports.UNSETTLED_LIMITS = exports.ALREADY_REGISTERED = exports.LIVE_CONFIRM_LIMITS = exports.CLAUDE_CODE_LINK = void 0;
|
|
4
4
|
exports.renderSite = renderSite;
|
|
5
5
|
exports.renderSiteWithAssets = renderSiteWithAssets;
|
|
6
6
|
exports.viewerAssetSources = viewerAssetSources;
|
|
7
7
|
exports.viewerAssetNames = viewerAssetNames;
|
|
8
|
+
exports.isRepositorySlug = isRepositorySlug;
|
|
9
|
+
exports.devRepositoryRefs = devRepositoryRefs;
|
|
10
|
+
exports.claudeCodeRepositories = claudeCodeRepositories;
|
|
11
|
+
exports.claudeCodeUrl = claudeCodeUrl;
|
|
12
|
+
exports.isAlreadyRegistered = isAlreadyRegistered;
|
|
8
13
|
exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
|
|
9
14
|
exports.sanitizeStateNow = sanitizeStateNow;
|
|
10
15
|
exports.overlayStateOnFleet = overlayStateOnFleet;
|
|
@@ -32,6 +37,7 @@ exports.deriveCapTile = deriveCapTile;
|
|
|
32
37
|
exports.renderConnectionMap = renderConnectionMap;
|
|
33
38
|
const node_crypto_1 = require("node:crypto");
|
|
34
39
|
const meet_guide_images_1 = require("./meet-guide-images");
|
|
40
|
+
const google_chat_guide_images_1 = require("./google-chat-guide-images");
|
|
35
41
|
// 操作タブの仮置き(#388)。判定の実体はあちら——ここは流し込みと配線だけ
|
|
36
42
|
const pending_ops_1 = require("./pending-ops");
|
|
37
43
|
/**
|
|
@@ -52,21 +58,27 @@ function renderSite(site, assets) {
|
|
|
52
58
|
.replace(/^https?:\/\/github\.com\//, "")
|
|
53
59
|
.replace(/\.git$/, "")
|
|
54
60
|
: "";
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
// 組み立ての正本は claudeCodeUrl 1か所(#404)。ここは薄い包みに留める。
|
|
62
|
+
// `?? []` は必須——build.ts は fleet-status.json が無い案件で externalSources を持たない
|
|
63
|
+
const codeUrl = (prompt) => claudeCodeUrl(prompt, repoSlug, site.externalSources ?? []);
|
|
64
|
+
/** ヘッダー「AIに聞く」の指示文。data-ai-prompt に同じ値を残し、接続状況が届いたら href を組み直す */
|
|
65
|
+
const ASK_PROMPT = "このプロジェクトの概要について教えてください。";
|
|
66
|
+
const askUrl = codeUrl(ASK_PROMPT);
|
|
59
67
|
// ※ 投入フォーム設定 site.intake は build.ts が --intake-url/--intake-token/--project-key から付与する。
|
|
60
68
|
const dataJson = JSON.stringify(site).replace(/</g, "\\u003c");
|
|
61
69
|
const safeTitle = escapeHtml(site.title);
|
|
62
70
|
// 接続マップ(操作タブ最上部のSVG図)。ビルド時に生成し<template>で埋め込む。fleet情報が無ければ出さない
|
|
63
71
|
const connMap = renderConnectionMap(site);
|
|
64
|
-
// チュートリアル(Home
|
|
65
|
-
|
|
72
|
+
// チュートリアル(Homeタブ下部の「はじめに」セクション)。**URLは焼き込まず指示文だけ渡す**(#404)
|
|
73
|
+
// ——URLは画面側が codeUrl で組む(登録した開発リポジトリを当日から載せるため)。
|
|
74
|
+
// 🔴 **この .replace(/</g, "\\u003c") を消さないこと。** </script> によるブレイクアウトを
|
|
75
|
+
// 防いでいる。指示文には案件データ(決定のタイトル)が入り、#404 以降は
|
|
76
|
+
// encodeURIComponent を通らない生の値がそのまま JSON に載る
|
|
77
|
+
const tutorialJson = JSON.stringify(buildTutorialData(site, repoSlug)).replace(/</g, "\\u003c");
|
|
66
78
|
const githubBtn = site.repoBaseUrl
|
|
67
79
|
? `<a class="gh-btn" href="${escapeHtml(site.repoBaseUrl)}" target="_blank" rel="noopener" title="コンテキストリポジトリをGitHubで開く">${GITHUB_ICON}</a>`
|
|
68
80
|
: "";
|
|
69
|
-
const askBtn = `<a class="ask-ai-btn" href="${escapeHtml(askUrl)}" target="_blank" rel="noopener" title="Claude Code Web
|
|
81
|
+
const askBtn = `<a class="ask-ai-btn" href="${escapeHtml(askUrl)}" data-ai-prompt="${escapeHtml(ASK_PROMPT)}" target="_blank" rel="noopener" title="Claude Code Web でこの案件のリポジトリと、登録済みの開発リポジトリを開き、プロジェクトについて質問・調査できます">💬 プロジェクトについてAIに聞く</a>`;
|
|
70
82
|
// CSPでインラインが拒否される環境向けの外部参照形。省略時は従来どおり埋め込む
|
|
71
83
|
const styleTag = assets
|
|
72
84
|
? `<link rel="stylesheet" href="${escapeHtml(assets.css)}" />`
|
|
@@ -341,6 +353,111 @@ function sourceAdapter(type) {
|
|
|
341
353
|
return "github";
|
|
342
354
|
return "";
|
|
343
355
|
}
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// Claude Code への導線(#404)— **組み立てる場所は下の claudeCodeUrl 1か所だけ**
|
|
358
|
+
//
|
|
359
|
+
// 画面には Claude Code を開くボタンが7つある(ヘッダーの「AIに聞く」・Home の「挑戦する」・
|
|
360
|
+
// 各区画の「+AIで登録」「AIで編集」「AIで適用」・Home の「AIで編集」「AIで書く」)。
|
|
361
|
+
// かつては7か所が別々に URL を組み立て、案件のコンテキストリポジトリ1本だけを渡していた。
|
|
362
|
+
// **ボタンが増えるたびに写しが増える形**だったので、組み立てを1関数に寄せ、
|
|
363
|
+
// 「ここ以外に claude.ai/code と書いたら赤くなる」テスト(claude-code-link)を付けてある。
|
|
364
|
+
//
|
|
365
|
+
// **クロージャを持たない function 宣言に保つこと**(.toString() でクライアントへ渡す)。
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
/**
|
|
368
|
+
* 説明用の素のリンク(リポジトリ指定なし)。「この仕組みについて」の Claude Code と、
|
|
369
|
+
* 案件リポジトリが owner/repo の形に解決できないときのフォールバックが使う。
|
|
370
|
+
*
|
|
371
|
+
* ⚠ **定数そのものを export してはいけない。** `export const` は CommonJS へ落ちるときに
|
|
372
|
+
* 参照が `exports.X` へ書き換えられ、**toString() で流し込んだ先には exports が無いので
|
|
373
|
+
* クライアントJSが丸ごと落ちる**(LIVE_CONFIRM_LIMITS の項に経緯がある)。
|
|
374
|
+
*/
|
|
375
|
+
const CLAUDE_CODE_HOME = "https://claude.ai/code/";
|
|
376
|
+
/** 素のリンクを**テストから固定するための入れ物**(LIVE_CONFIRM_LIMITS と同じ作法) */
|
|
377
|
+
exports.CLAUDE_CODE_LINK = { home: CLAUDE_CODE_HOME };
|
|
378
|
+
/**
|
|
379
|
+
* owner/repo の形か(ポータル web/src/features/harness/harness-link.ts の REPOSITORY_SLUG と同じ形)。
|
|
380
|
+
*
|
|
381
|
+
* ⚠ **波括弧を含む量指定子({2,} など)を書かないこと。** 配信スクリプトから
|
|
382
|
+
* この関数のソースを切り出すテストが、波括弧の対応で本体の末尾を決めている。
|
|
383
|
+
*/
|
|
384
|
+
function isRepositorySlug(ref) {
|
|
385
|
+
if (typeof ref !== "string")
|
|
386
|
+
return false;
|
|
387
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(ref))
|
|
388
|
+
return false;
|
|
389
|
+
// 🔴 **パスに見える値を通さない**(`../evil` / `./x` / `a/..` / `.git/config`)。
|
|
390
|
+
// クエリの値なので実害は無い(受け取る側でパスとして解決されない)が、
|
|
391
|
+
// **「形の検査」だと言っている以上、形として通してはいけない**——通していると、
|
|
392
|
+
// この関数を別の用途(ファイルパスの組み立て等)に流用した人が静かに踏む。
|
|
393
|
+
// 所有者は「.」で始まれない(GitHub の規則)。リポジトリ名は `.github` のように
|
|
394
|
+
// 「.」で始まれるので、そちらは**点だけの区切り**だけを弾く。
|
|
395
|
+
var parts = ref.split("/");
|
|
396
|
+
if (parts[0].charAt(0) === ".")
|
|
397
|
+
return false;
|
|
398
|
+
return !/^\.+$/.test(parts[1]);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 接続済みの開発リポジトリの ref を**登録順**に取り出す。
|
|
402
|
+
*
|
|
403
|
+
* 🔴 **絞りは sourceAdapter(正本・#153)に任せる。** ここに独自の if 連鎖を書くと、
|
|
404
|
+
* 次のアダプタが増えたときに片方だけ直り、一覧に出ているリポジトリが AI 導線から消える。
|
|
405
|
+
*
|
|
406
|
+
* 🔴 **読み取り停止中(goldState === "excluded")も載せる。** 読み取りを止めているのは
|
|
407
|
+
* 夜間の取り込みの話で、人が AI に見せたいかどうかとは別(オーナー決定・ポータルの
|
|
408
|
+
* ハーネスの導線と同じ規則)。
|
|
409
|
+
*/
|
|
410
|
+
function devRepositoryRefs(externals) {
|
|
411
|
+
var out = [];
|
|
412
|
+
(externals || []).forEach(function (s) {
|
|
413
|
+
if (!s || sourceAdapter(s.type) !== "github")
|
|
414
|
+
return;
|
|
415
|
+
if (!isRepositorySlug(s.ref))
|
|
416
|
+
return;
|
|
417
|
+
out.push(s.ref);
|
|
418
|
+
});
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* repositories= に載せる並び。**案件リポ → 開発リポ(登録順)**。
|
|
423
|
+
*
|
|
424
|
+
* 🔴 **大小文字を無視して重複排除・先勝ち。** 案件リポ自身が開発リポとしても登録されて
|
|
425
|
+
* いる案件があり、そのまま並べると同じリポジトリが2回載る。
|
|
426
|
+
* 🔴 **形の壊れた開発リポは、その1件だけ落とす。** 登録は人が書く JSON なので、
|
|
427
|
+
* 1行の書き間違いでボタンごと死なせない。
|
|
428
|
+
* 🔴 **案件リポが owner/repo の形でないときは空を返す**(呼び出し側が素のリンクへ倒す)。
|
|
429
|
+
* ここで案件リポだけを落とすと「開発リポだけが開く」URL ができ、ヘッダーの
|
|
430
|
+
* 「このプロジェクトについてAIに聞く」が**別のリポジトリを開く**。
|
|
431
|
+
*/
|
|
432
|
+
function claudeCodeRepositories(projectRepo, devRefs) {
|
|
433
|
+
if (!isRepositorySlug(projectRepo))
|
|
434
|
+
return [];
|
|
435
|
+
var out = [String(projectRepo)];
|
|
436
|
+
var seen = [String(projectRepo).toLowerCase()];
|
|
437
|
+
(devRefs || []).forEach(function (ref) {
|
|
438
|
+
if (!isRepositorySlug(ref))
|
|
439
|
+
return;
|
|
440
|
+
var k = String(ref).toLowerCase();
|
|
441
|
+
if (seen.indexOf(k) >= 0)
|
|
442
|
+
return;
|
|
443
|
+
seen.push(k);
|
|
444
|
+
out.push(String(ref));
|
|
445
|
+
});
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* 🔴 **Claude Code への URL を組み立ててよい唯一の場所。**
|
|
450
|
+
* ここ以外に claude.ai/code の文字列を書かないこと(guard テストが見張っている)。
|
|
451
|
+
*
|
|
452
|
+
* repositories はカンマ区切りで**素のまま**載せる(公式の例がその形で、スラッシュも
|
|
453
|
+
* エンコードしない)。指示文だけ encodeURIComponent する。
|
|
454
|
+
*/
|
|
455
|
+
function claudeCodeUrl(prompt, projectRepo, externals) {
|
|
456
|
+
var repos = claudeCodeRepositories(projectRepo, devRepositoryRefs(externals));
|
|
457
|
+
if (!repos.length)
|
|
458
|
+
return CLAUDE_CODE_HOME;
|
|
459
|
+
return "https://claude.ai/code?repositories=" + repos.join(",") + "&prompt=" + encodeURIComponent(prompt);
|
|
460
|
+
}
|
|
344
461
|
/**
|
|
345
462
|
* 外部ソースの行が「この能力・このアダプタのタイル」に属するか。
|
|
346
463
|
*
|
|
@@ -563,6 +680,11 @@ const REGISTERED_FIELDS = {
|
|
|
563
680
|
chat: ["ref", "name", "workspace", "orgUnknown", "origin", "goldState", "notify"],
|
|
564
681
|
"google-chat": ["ref", "name"],
|
|
565
682
|
dev: ["ref", "type"],
|
|
683
|
+
// 🔴 **意図的に外した開発リポジトリ(#391)。** サーバは前から別キーで返していたが、
|
|
684
|
+
// ここに無いので篩が丸ごと落としていた。**これが読めないと「読み取り停止中」の行を
|
|
685
|
+
// 消せない**——`registeredDev` は exclude された repo を落とすので、`dev` だけを見ていると
|
|
686
|
+
// 「停止中の行」と「登録から消えた行」がどちらも「dev に無い」になり、区別が付かない
|
|
687
|
+
devExcluded: ["ref", "type"],
|
|
566
688
|
design: ["key", "name"],
|
|
567
689
|
materials: ["id", "name"],
|
|
568
690
|
issues: ["domain", "projectKey"],
|
|
@@ -572,12 +694,41 @@ const REGISTERED_REQUIRED = {
|
|
|
572
694
|
chat: ["ref"],
|
|
573
695
|
"google-chat": ["ref"],
|
|
574
696
|
dev: ["ref"],
|
|
697
|
+
devExcluded: ["ref"],
|
|
575
698
|
design: ["key"],
|
|
576
699
|
materials: ["id"],
|
|
577
700
|
issues: ["domain", "projectKey"],
|
|
578
701
|
};
|
|
579
702
|
/** 開発の種別に許す値。**知らない値は落とす**(画面のラベル・振り分けが決まらない) */
|
|
580
703
|
const REGISTERED_DEV_TYPES = ["github-issues", "github-discussions"];
|
|
704
|
+
/**
|
|
705
|
+
* 「もう登録されている」という断りの語尾。**正本は intake の `ALREADY_REGISTERED_SUFFIX`**
|
|
706
|
+
* (`infra/lambda/intake/logic.mjs`)で、ここはその写し。一致は
|
|
707
|
+
* `viewer/test/already-registered-parity.test.mjs` が見張っている。
|
|
708
|
+
*
|
|
709
|
+
* ⚠ **定数そのものを export しない**(CommonJS で `exports.X` に化け、流し込んだ先で落ちる)。
|
|
710
|
+
* テストから読む値は下の `ALREADY_REGISTERED` 経由。
|
|
711
|
+
*/
|
|
712
|
+
const ALREADY_REGISTERED_SUFFIX = "既に登録されています";
|
|
713
|
+
/** 語尾を**テストから固定するための入れ物**(LIVE_CONFIRM_LIMITS と同じ作法) */
|
|
714
|
+
exports.ALREADY_REGISTERED = { suffix: ALREADY_REGISTERED_SUFFIX };
|
|
715
|
+
/**
|
|
716
|
+
* サーバの断りが「もう登録されている」か。
|
|
717
|
+
*
|
|
718
|
+
* 🔴 **これは失敗の判定ではなく、事実の判定(#409)。** サーバがこう答えた時点で、
|
|
719
|
+
* その接続先は登録ファイルに確かに在る。画面はこれを受けて**一覧に行を出す**——
|
|
720
|
+
* 「一覧は空なのに、追加すると既に登録されていると断られる」という袋小路を作らない。
|
|
721
|
+
*
|
|
722
|
+
* 🔴 **語尾で見る(能力ごとの主語まで写さない)。** 「このチャンネルは」「このスペースは」
|
|
723
|
+
* まで写すと、主語を1つ増やした日に画面だけ古い写しを持ち、その能力だけ袋小路が残る。
|
|
724
|
+
*
|
|
725
|
+
* **クロージャを持たない純関数に保つこと**(`.toString()` でクライアントへ渡す)。
|
|
726
|
+
*/
|
|
727
|
+
function isAlreadyRegistered(message) {
|
|
728
|
+
if (typeof message !== "string")
|
|
729
|
+
return false;
|
|
730
|
+
return message.indexOf(ALREADY_REGISTERED_SUFFIX) >= 0;
|
|
731
|
+
}
|
|
581
732
|
/**
|
|
582
733
|
* 応答の registeredNow を、画面が扱ってよい形へ畳む。**通らなかったものは黙って落とす。**
|
|
583
734
|
*
|
|
@@ -622,7 +773,8 @@ function sanitizeRegisteredNow(v) {
|
|
|
622
773
|
continue; // 葉は文字列だけ
|
|
623
774
|
item[f] = fv;
|
|
624
775
|
}
|
|
625
|
-
|
|
776
|
+
// 種別の検査は dev と devExcluded の両方に掛ける(片側だけ緩いと篩の規則が割れる)
|
|
777
|
+
if ((cap === "dev" || cap === "devExcluded") && item.type && REGISTERED_DEV_TYPES.indexOf(item.type) < 0)
|
|
626
778
|
delete item.type;
|
|
627
779
|
let full = true;
|
|
628
780
|
for (const f of REGISTERED_REQUIRED[cap])
|
|
@@ -1018,7 +1170,44 @@ function subtractRemovedRegistered(view, reg) {
|
|
|
1018
1170
|
for (const r of reg.dev || [])
|
|
1019
1171
|
if (r && r.ref)
|
|
1020
1172
|
devRefs[String(r.ref)] = true;
|
|
1021
|
-
|
|
1173
|
+
// 意図的に外した開発リポジトリ(#391)。**「停止中」と「消えた」を分ける唯一の材料**
|
|
1174
|
+
//
|
|
1175
|
+
// 🔴 **照合は小文字で行う。** サーバ側の `devExcludeSet`(logic.mjs)は exclude と sources を
|
|
1176
|
+
// 小文字化して突き合わせるので、`dev-sources.json` の綴りが `Owner/Repo` と `owner/repo` で
|
|
1177
|
+
// ずれている案件では、大小を区別すると**停止中の行が表示から消える**(「登録から消えた」と
|
|
1178
|
+
// 誤読して引いてしまう)。
|
|
1179
|
+
// 注: 下の `devRefs`(停止中でない行の照合)は従来どおり大小を区別する。こちらを変えると
|
|
1180
|
+
// 既存の引き算の挙動が変わるので、本件では触らない
|
|
1181
|
+
const lc = (v) => String(v == null ? "" : v).toLowerCase();
|
|
1182
|
+
const excludedRefsLc = {};
|
|
1183
|
+
for (const r of reg.devExcluded || [])
|
|
1184
|
+
if (r && r.ref)
|
|
1185
|
+
excludedRefsLc[lc(r.ref)] = true;
|
|
1186
|
+
const devRefsLc = {};
|
|
1187
|
+
for (const r of reg.dev || [])
|
|
1188
|
+
if (r && r.ref)
|
|
1189
|
+
devRefsLc[lc(r.ref)] = true;
|
|
1190
|
+
// **読み取りを再開した行は、印だけ落として中身は残す(#391 の実装レビュー④)。**
|
|
1191
|
+
//
|
|
1192
|
+
// 再開すると repo は `dev` に戻り `devExcluded` から消える。下の引き算はこれを
|
|
1193
|
+
// 「登録から消えた」と読んで行ごと引き、`devRowsToAdd` が素の行を作り直す——
|
|
1194
|
+
// **表示名・ゲート・最終読み取りが次のビルドまで消える**。かといって行ごと残すと、
|
|
1195
|
+
// 読み取りが再開しているのに「読み取り停止中」の札が出たままになる(画面が嘘をつく)。
|
|
1196
|
+
// **印(goldState)だけを今の値に直して、行はそのまま使う**のが両方を満たす。
|
|
1197
|
+
const raw = v.externalSources || [];
|
|
1198
|
+
let reopened = false;
|
|
1199
|
+
const before = raw.map((s) => {
|
|
1200
|
+
if (!s || !s.ref || s.goldState !== "excluded")
|
|
1201
|
+
return s;
|
|
1202
|
+
if (String(s.type || "").indexOf("github") !== 0)
|
|
1203
|
+
return s;
|
|
1204
|
+
if (!Array.isArray(reg.dev) || !Array.isArray(reg.devExcluded))
|
|
1205
|
+
return s;
|
|
1206
|
+
if (excludedRefsLc[lc(s.ref)] === true || devRefsLc[lc(s.ref)] !== true)
|
|
1207
|
+
return s;
|
|
1208
|
+
reopened = true;
|
|
1209
|
+
return Object.assign({}, s, { goldState: "on" });
|
|
1210
|
+
});
|
|
1022
1211
|
const after = before.filter((s) => {
|
|
1023
1212
|
if (!s || !s.ref)
|
|
1024
1213
|
return true;
|
|
@@ -1035,14 +1224,20 @@ function subtractRemovedRegistered(view, reg) {
|
|
|
1035
1224
|
}
|
|
1036
1225
|
if (String(s.type || "").indexOf("github") === 0 && Array.isArray(reg.dev)) {
|
|
1037
1226
|
// 「読み取り停止中」(exclude)の行は登録簿の sources に居ないが、
|
|
1038
|
-
//
|
|
1039
|
-
|
|
1040
|
-
|
|
1227
|
+
// まとめ表はバッジを出すために保持している——引くと停止中の表示ごと消える。
|
|
1228
|
+
// 🔴 **停止中かどうかは devExcluded で判断する(#391)。** それが読めない古い応答では
|
|
1229
|
+
// 従来どおり保持する(判断できないので安全側)。読めるなら、そこに居なくなった行は
|
|
1230
|
+
// **本当に登録から消えた**ので引いてよい——引かないと、停止中の行を解除したとき
|
|
1231
|
+
// 「消したのに次のビルドまで戻り続ける」(仮置きが捨てられた後に復活する経路)
|
|
1232
|
+
if (s.goldState === "excluded") {
|
|
1233
|
+
return Array.isArray(reg.devExcluded) ? excludedRefsLc[lc(s.ref)] === true : true;
|
|
1234
|
+
}
|
|
1041
1235
|
return devRefs[String(s.ref)] === true;
|
|
1042
1236
|
}
|
|
1043
1237
|
return true;
|
|
1044
1238
|
});
|
|
1045
|
-
|
|
1239
|
+
// **件数が変わらなくても、印を直したなら書き戻す**(再開した行は引かれないので長さが同じ)
|
|
1240
|
+
if (after.length !== before.length || reopened)
|
|
1046
1241
|
set({ externalSources: after });
|
|
1047
1242
|
}
|
|
1048
1243
|
if (Array.isArray(reg.design) || Array.isArray(reg.materials)) {
|
|
@@ -1951,7 +2146,7 @@ function renderConnectionMap(site) {
|
|
|
1951
2146
|
* 各チャレンジのプロンプトは固定文(ビューアコードに実案件名は残さない)。
|
|
1952
2147
|
* チャレンジ4だけは site.decisions から根拠付きの最新決定を選び、そのtitle/idを埋め込む。
|
|
1953
2148
|
*/
|
|
1954
|
-
function buildTutorialData(site,
|
|
2149
|
+
function buildTutorialData(site, repoSlug) {
|
|
1955
2150
|
const c4 = tutorialChallenge4Prompt(site.decisions);
|
|
1956
2151
|
const specs = [
|
|
1957
2152
|
{
|
|
@@ -2036,7 +2231,7 @@ function buildTutorialData(site, codeUrl, repoSlug) {
|
|
|
2036
2231
|
n: i + 1,
|
|
2037
2232
|
title: s.title,
|
|
2038
2233
|
desc: s.desc,
|
|
2039
|
-
|
|
2234
|
+
prompt: s.prompt,
|
|
2040
2235
|
})),
|
|
2041
2236
|
};
|
|
2042
2237
|
}
|
|
@@ -3067,7 +3262,8 @@ a.ds-tile:hover { text-decoration: none; }
|
|
|
3067
3262
|
/* 取り込みの条件は箇条書きで出す(ANDであることを読み飛ばせないように) */
|
|
3068
3263
|
.sec-how-list { margin: 4px 0; padding-left: 1.3em; }
|
|
3069
3264
|
.sec-how-list li { margin: 2px 0; }
|
|
3070
|
-
/*
|
|
3265
|
+
/* 会議の主催者向けの選択肢(「Google Meet」フォルダの共有・2026-08-29 オーナー承認。
|
|
3266
|
+
共有対象のフォルダ名は 2026-09-03 に Meet Recordings から Google Meet へ改めた・#393)。
|
|
3071
3267
|
**折りたたまない。** 週次整理の節が「誰も見ない」で撤去された経緯があるので、
|
|
3072
3268
|
知ってほしい選択肢を <details> の中に隠さない。従来の bot 招待の説明の下に、
|
|
3073
3269
|
枠で囲った別の話として置く(差し替えではなく追加であることを見た目でも示す) */
|
|
@@ -3370,6 +3566,14 @@ var CAP_STATE_TEXT = ${JSON.stringify(CAP_STATE_TEXT)};
|
|
|
3370
3566
|
var CAP_STATE_MARK = ${JSON.stringify(CAP_STATE_MARK)};
|
|
3371
3567
|
${externalCapability.toString()}
|
|
3372
3568
|
${sourceAdapter.toString()}
|
|
3569
|
+
// Claude Code への導線(#404)。**組み立ての正本は上の claudeCodeUrl だけ。**
|
|
3570
|
+
// 🔴 この定数行を落とすと、流し込んだ関数が未定義の CLAUDE_CODE_HOME を参照して
|
|
3571
|
+
// 画面のスクリプトが丸ごと死ぬ(定数は export できないので、値はここで注入する)
|
|
3572
|
+
var CLAUDE_CODE_HOME = ${JSON.stringify(CLAUDE_CODE_HOME)};
|
|
3573
|
+
${isRepositorySlug.toString()}
|
|
3574
|
+
${devRepositoryRefs.toString()}
|
|
3575
|
+
${claudeCodeRepositories.toString()}
|
|
3576
|
+
${claudeCodeUrl.toString()}
|
|
3373
3577
|
${sourceMatchesAdapter.toString()}
|
|
3374
3578
|
${adapterList.toString()}
|
|
3375
3579
|
${adapterEntry.toString()}
|
|
@@ -3390,6 +3594,10 @@ var REGISTERED_CAP_NAMES = ${JSON.stringify(REGISTERED_CAP_NAMES)};
|
|
|
3390
3594
|
var REGISTERED_FIELDS = ${JSON.stringify(REGISTERED_FIELDS)};
|
|
3391
3595
|
var REGISTERED_REQUIRED = ${JSON.stringify(REGISTERED_REQUIRED)};
|
|
3392
3596
|
var REGISTERED_DEV_TYPES = ${JSON.stringify(REGISTERED_DEV_TYPES)};
|
|
3597
|
+
// 「もう登録されている」という断りの語尾(#409)。**この定数行を落とすと、
|
|
3598
|
+
// 流し込んだ isAlreadyRegistered が未定義を参照して画面のスクリプトが丸ごと死ぬ**
|
|
3599
|
+
var ALREADY_REGISTERED_SUFFIX = ${JSON.stringify(ALREADY_REGISTERED_SUFFIX)};
|
|
3600
|
+
${isAlreadyRegistered.toString()}
|
|
3393
3601
|
var UNSETTLED_TTL_MS = ${JSON.stringify(UNSETTLED_TTL_MS)};
|
|
3394
3602
|
var UNSETTLED_POLL_MS = ${JSON.stringify(UNSETTLED_POLL_MS)};
|
|
3395
3603
|
${sanitizeRegisteredNow.toString()}
|
|
@@ -3418,10 +3626,13 @@ ${pending_ops_1.pendingRepo.toString()}
|
|
|
3418
3626
|
${pending_ops_1.pendingFigmaKey.toString()}
|
|
3419
3627
|
${pending_ops_1.pendingDriveFolderId.toString()}
|
|
3420
3628
|
${pending_ops_1.pendingLabel.toString()}
|
|
3629
|
+
${pending_ops_1.pendingIsSpaceRef.toString()}
|
|
3630
|
+
${pending_ops_1.pendingIsSlackTeamId.toString()}
|
|
3421
3631
|
${pending_ops_1.pendingOpOf.toString()}
|
|
3422
3632
|
${pending_ops_1.sanitizePendingOps.toString()}
|
|
3423
3633
|
${pending_ops_1.rememberPendingOp.toString()}
|
|
3424
3634
|
${pending_ops_1.pendingSiteTimeMs.toString()}
|
|
3635
|
+
${pending_ops_1.pendingStale.toString()}
|
|
3425
3636
|
${pending_ops_1.pendingOpBuildCaughtUp.toString()}
|
|
3426
3637
|
${pending_ops_1.pendingLiveRowMatches.toString()}
|
|
3427
3638
|
${pending_ops_1.pendingOpLiveAgrees.toString()}
|
|
@@ -3570,19 +3781,31 @@ ${deriveCapTile.toString()}
|
|
|
3570
3781
|
*/
|
|
3571
3782
|
var DRIVE_INGEST_ACCOUNT = "cortex-notetaker@classmethod.jp";
|
|
3572
3783
|
/**
|
|
3573
|
-
* 自分の「Meet
|
|
3784
|
+
* 自分の「Google Meet」フォルダへの入口。
|
|
3574
3785
|
*
|
|
3575
3786
|
* **フォルダIDは人ごとに違うので、フォルダを直接指すURLは作れない。** 代わりに
|
|
3576
3787
|
* ドライブの検索URLを使う——開いた人のドライブの中で検索が走るので、**1本で全員に効く**。
|
|
3577
|
-
*
|
|
3788
|
+
* フォルダはマイドライブ直下にでき、**日本語環境でも名前は英語のまま**作られる
|
|
3789
|
+
*(オーナー1人の実測。ロケールによって違う可能性は残る)。
|
|
3790
|
+
*
|
|
3791
|
+
* 🔴 **検索結果には会議ごとのフォルダも並ぶ。** メモは「Google Meet」の中の
|
|
3792
|
+
*「<会議名> - 日付」「<会議名> (recurring)」というフォルダに入るので、名前で検索すると
|
|
3793
|
+
* それらも一緒に出てくる。**共有すべきなのは、それらを入れている親のほう**——
|
|
3794
|
+
* 絞り込みでは決着しないので、owner:me で他人の共有を落としたうえで、
|
|
3795
|
+
* **どれを選ぶかは手順の文で言う**(下の steps の1つ目)。
|
|
3578
3796
|
*
|
|
3579
3797
|
* **共有ダイアログを直接開くURLは無い。** リンクの先で「右クリック → 共有」の1手が要ることは、
|
|
3580
3798
|
* 押す前に画面へ書く(リンクだけ置くと、開いた先で止まる人が出る)。
|
|
3581
3799
|
*/
|
|
3582
|
-
var MEET_FOLDER_SEARCH_URL =
|
|
3800
|
+
var MEET_FOLDER_SEARCH_URL =
|
|
3801
|
+
"https://drive.google.com/drive/search?q=type:folder%20owner:me%20%22Google%20Meet%22";
|
|
3583
3802
|
// 手順の画像(data URI)。中身と埋め込み方の理由は src/meet-guide-images.ts に書いてある
|
|
3584
3803
|
var MEET_SHOT_STEP1 = "${meet_guide_images_1.MEET_SHARE_STEP1}";
|
|
3585
3804
|
var MEET_SHOT_STEP2 = "${meet_guide_images_1.MEET_SHARE_STEP2}";
|
|
3805
|
+
// Google Chat の「スペースにアプリを追加する」手順の画像(#389)。理由は
|
|
3806
|
+
// src/google-chat-guide-images.ts に書いてある(Meet と同じ埋め込み方)
|
|
3807
|
+
var GCHAT_SHOT_STEP1 = "${google_chat_guide_images_1.GCHAT_APP_STEP1}";
|
|
3808
|
+
var GCHAT_SHOT_STEP2 = "${google_chat_guide_images_1.GCHAT_APP_STEP2}";
|
|
3586
3809
|
|
|
3587
3810
|
function copyRow(text) {
|
|
3588
3811
|
// **input と button を使わない。** 追加フォームの中に置くので、input を足すと
|
|
@@ -3821,10 +4044,33 @@ ${deriveCapTile.toString()}
|
|
|
3821
4044
|
var REPO_SLUG = SITE.repoBaseUrl
|
|
3822
4045
|
? SITE.repoBaseUrl.replace(/^https?:\\/\\/github\\.com\\//, "").replace(/\\.git$/, "")
|
|
3823
4046
|
: "";
|
|
4047
|
+
// 組み立ての正本は claudeCodeUrl(上で流し込んだビルド時の関数・#404)。
|
|
4048
|
+
// 開発リポジトリは **まとめ表ではなく画面と同じ値**(まとめ表+登録の即時値+仮置き)から取る
|
|
4049
|
+
// ——焼き込むと、登録した開発リポジトリがその日いっぱい AI 導線に載らない。
|
|
4050
|
+
// 🔴 **var MERGE_KEYS の宣言より上に codeUrl を呼ぶトップレベル文を足さないこと。**
|
|
4051
|
+
// fleetVal は MERGE_KEYS を読むが、var の初期化は宣言行に達するまで走らない
|
|
4052
|
+
// (関数宣言の巻き上げでは undefined を読む)。呼び出しは全て描画(route)以降に置く
|
|
3824
4053
|
function codeUrl(prompt) {
|
|
3825
|
-
return REPO_SLUG
|
|
3826
|
-
|
|
3827
|
-
|
|
4054
|
+
return claudeCodeUrl(prompt, REPO_SLUG, fleetVal("externalSources") || []);
|
|
4055
|
+
}
|
|
4056
|
+
// AI 導線の a はここでだけ作る。href を決めてよいのは codeUrl だけ(#404)。
|
|
4057
|
+
// 指示文を属性に残すのは、接続状況の応答が届いたときに href を組み直すため(syncAiLinks)
|
|
4058
|
+
function aiLink(cls, prompt, text) {
|
|
4059
|
+
var a = el("a", { class: cls, href: codeUrl(prompt), target: "_blank", rel: "noopener", text: text });
|
|
4060
|
+
a.setAttribute("data-ai-prompt", prompt);
|
|
4061
|
+
return a;
|
|
4062
|
+
}
|
|
4063
|
+
// 接続状況が届いた・保存が効いたときに、画面に出ている AI 導線の href を組み直す。
|
|
4064
|
+
// 🔴 **pendingOpsNow の中(仮置きの失効)からは呼ばない**——codeUrl が fleetVal を
|
|
4065
|
+
// 呼ぶので再入する。仮置きが失効して行が減ったときの貼り替えは行わない
|
|
4066
|
+
// (15分後の話で、その頃には接続状況の応答が届いている)
|
|
4067
|
+
// 🔴 **document を素で引かない(app.ownerDocument を使う)。** ここは接続状況の応答が
|
|
4068
|
+
// 届いたときに走るので、**画面が畳まれた後に届く**ことがある(テストの window.close()・
|
|
4069
|
+
// 実機のタブを閉じた直後)。掴んだノード越しなら、その後でも例外を投げずに済む
|
|
4070
|
+
// ——投げると応答の後段(取り直しの停止など)が丸ごと落ちる
|
|
4071
|
+
function syncAiLinks() {
|
|
4072
|
+
var as = app.ownerDocument.querySelectorAll("a[data-ai-prompt]");
|
|
4073
|
+
for (var i = 0; i < as.length; i++) as[i].href = codeUrl(as[i].getAttribute("data-ai-prompt"));
|
|
3828
4074
|
}
|
|
3829
4075
|
// editUrl(.../edit/<branch>/<path>)からリポジトリ相対のファイルパスを取り出す(プロンプトで対象を指し示すため)
|
|
3830
4076
|
function repoPathOf(rec) {
|
|
@@ -3955,7 +4201,7 @@ ${deriveCapTile.toString()}
|
|
|
3955
4201
|
if (!g) return null;
|
|
3956
4202
|
var prompt = aiHead(g.skill) + g.add + AI_COMMON;
|
|
3957
4203
|
return el("div", { class: "ai-add-bar" }, [
|
|
3958
|
-
|
|
4204
|
+
aiLink("ai-add-btn", prompt, "+ AIで登録"),
|
|
3959
4205
|
]);
|
|
3960
4206
|
}
|
|
3961
4207
|
// 各Goldレコードの「AIで編集」ボタン(GitHubで編集の隣)。スキルの無い型では null
|
|
@@ -3967,7 +4213,7 @@ ${deriveCapTile.toString()}
|
|
|
3967
4213
|
var target = (pathRef ? pathRef + " と " : "") + g.dir + "/README.md";
|
|
3968
4214
|
var subject = "この" + g.noun + "「" + title + "」" + (pathRef ? "" : "(" + rec.id + ")");
|
|
3969
4215
|
var prompt = aiHead(g.skill) + g.edit(target, subject) + AI_COMMON;
|
|
3970
|
-
return
|
|
4216
|
+
return aiLink("ai-edit-btn", prompt, "AIで編集");
|
|
3971
4217
|
}
|
|
3972
4218
|
|
|
3973
4219
|
// ---------- AIからの更新提案(.cortex/gold-proposals.json 由来) ----------
|
|
@@ -4027,7 +4273,7 @@ ${deriveCapTile.toString()}
|
|
|
4027
4273
|
);
|
|
4028
4274
|
lines.push("書き込む前に、変更内容を提示して承認を得てください。");
|
|
4029
4275
|
var prompt = aiHead(g.skill) + lines.join("\\n") + AI_COMMON;
|
|
4030
|
-
return
|
|
4276
|
+
return aiLink("ai-apply-btn", prompt, "AIで適用");
|
|
4031
4277
|
}
|
|
4032
4278
|
|
|
4033
4279
|
/**
|
|
@@ -4275,7 +4521,7 @@ ${deriveCapTile.toString()}
|
|
|
4275
4521
|
challenges.forEach(function (c) {
|
|
4276
4522
|
var done = tutIsDone(c.n);
|
|
4277
4523
|
var card = el("div", { class: "tut-card" + (done ? " tut-done" : "") });
|
|
4278
|
-
var btn =
|
|
4524
|
+
var btn = aiLink("tut-btn", c.prompt, "🚀 挑戦する");
|
|
4279
4525
|
var cb = el("input", { type: "checkbox" });
|
|
4280
4526
|
cb.checked = done;
|
|
4281
4527
|
var check = el("label", { class: "tut-check" }, [cb, el("span", { text: "完了" })]);
|
|
@@ -4371,11 +4617,8 @@ ${deriveCapTile.toString()}
|
|
|
4371
4617
|
* 破棄の条件を2つ持つと、片方だけ直したときに挙動が割れる。
|
|
4372
4618
|
*/
|
|
4373
4619
|
function iconPendingStale(pend, nowMs) {
|
|
4374
|
-
if (!pend
|
|
4375
|
-
|
|
4376
|
-
var gen0 = siteTimeMs(pend.gen0);
|
|
4377
|
-
if (isNaN(gen0)) return byAt;
|
|
4378
|
-
return byAt && nowMs > gen0 + TOMBSTONE_MARGIN_MS;
|
|
4620
|
+
if (!pend) return false;
|
|
4621
|
+
return pendingStale(pend.at, pend.gen0, nowMs);
|
|
4379
4622
|
}
|
|
4380
4623
|
/** いま覚えている反映待ちを記録する(保存・解除に共通の形) */
|
|
4381
4624
|
function rememberIconPending(kind, dataUrl) {
|
|
@@ -4519,7 +4762,7 @@ ${deriveCapTile.toString()}
|
|
|
4519
4762
|
// **本物が追いついたら仮画像を捨てる。** 残したままだと、次に差し替えた人にも
|
|
4520
4763
|
// 古い仮画像が見え続ける(削除のトゥームストーンと同じ捨て方)
|
|
4521
4764
|
var pend = readIconPending();
|
|
4522
|
-
if (pend && iconPendingStale(pend,
|
|
4765
|
+
if (pend && iconPendingStale(pend, pendingSiteTimeMs(SITE.generatedAt))) clearIconPending();
|
|
4523
4766
|
var cur = projectIconCurrent();
|
|
4524
4767
|
slot.textContent = "";
|
|
4525
4768
|
|
|
@@ -4741,7 +4984,7 @@ ${deriveCapTile.toString()}
|
|
|
4741
4984
|
// (renderSiteIcon)——全タブから同じ場所で触れる形にした
|
|
4742
4985
|
head.appendChild(titleWrap);
|
|
4743
4986
|
var homeEditRow = el("div", { class: "detail-actions-row" }, [
|
|
4744
|
-
|
|
4987
|
+
aiLink("ai-edit-btn", HOME_EDIT_PROMPT, "AIで編集"),
|
|
4745
4988
|
]);
|
|
4746
4989
|
if (h.editUrl) {
|
|
4747
4990
|
homeEditRow.appendChild(el("a", { class: "edit-btn", href: h.editUrl, target: "_blank", rel: "noopener", text: "GitHubで編集" }));
|
|
@@ -4796,7 +5039,7 @@ ${deriveCapTile.toString()}
|
|
|
4796
5039
|
]),
|
|
4797
5040
|
]);
|
|
4798
5041
|
var actions = el("div", { class: "home-unfilled-actions" }, [
|
|
4799
|
-
|
|
5042
|
+
aiLink("ai-add-btn", HOME_WRITE_PROMPT, "AIで書く"),
|
|
4800
5043
|
]);
|
|
4801
5044
|
if (SITE.home && SITE.home.editUrl) {
|
|
4802
5045
|
actions.appendChild(el("a", { class: "edit-btn", href: SITE.home.editUrl, target: "_blank", rel: "noopener", text: "GitHubで編集" }));
|
|
@@ -5308,13 +5551,17 @@ ${deriveCapTile.toString()}
|
|
|
5308
5551
|
}
|
|
5309
5552
|
|
|
5310
5553
|
/**
|
|
5311
|
-
* 主催者向けの選択肢: 「Meet
|
|
5554
|
+
* 主催者向けの選択肢: 「Google Meet」フォルダを1回共有すると、以後は招待が要らない。
|
|
5312
5555
|
*
|
|
5313
5556
|
* **上の bot 招待の説明は残したまま足す(2026-08-29 オーナー承認)。** 招待方式は
|
|
5314
5557
|
* 参加するだけの会議・他社主催の会議で今も唯一の手段なので、置き換えではない。
|
|
5315
5558
|
*
|
|
5559
|
+
* **共有対象は 2026-09-03 に「Meet Recordings」から「Google Meet」へ改めた(#393)。**
|
|
5560
|
+
* Gemini の保存先が変わり、メモは「Google Meet」の中の会議ごとのフォルダに入るようになった。
|
|
5561
|
+
*
|
|
5316
5562
|
* **手順の細目は書かない。** 正本は apps-script/docs/meet-recordings-sharing.md で、
|
|
5317
|
-
*
|
|
5563
|
+
* 「共有したフォルダから1階層下までしか見ない」「メモを移すと対象外になる」
|
|
5564
|
+
* 「Google Meet が無い環境では Meet Recordings を共有する」等はそちらが持つ。
|
|
5318
5565
|
* 画面が担うのは**選択肢があることと、共有すると何が起きるかの1行**まで
|
|
5319
5566
|
*(同じ説明を2箇所で維持すると、必ず片方が古くなる)。
|
|
5320
5567
|
*
|
|
@@ -5326,11 +5573,14 @@ ${deriveCapTile.toString()}
|
|
|
5326
5573
|
function meetingFolderShare(signal, aliasCount) {
|
|
5327
5574
|
var box = el("div", { class: "mt-share" });
|
|
5328
5575
|
box.appendChild(el("div", { class: "mt-share-title", text: "毎回の招待をやめる方法(会議の主催者向け・任意)" }));
|
|
5329
|
-
box.appendChild(el("p", { text: "自分の Google ドライブにある「Meet Recordings
|
|
5576
|
+
box.appendChild(el("p", { text: "自分の Google ドライブにある「Google Meet」フォルダ(2026年7月より前から使っている環境では「Meet Recordings」)を、下のアカウントに「閲覧者」で1回だけ共有すると、以後あなたが主催する会議は招待が要らなくなります。会議名の目印はこれまでどおり必要です。「Google Meet」フォルダがまだ無い場合は、Gemini のメモを ON にした会議を1回主催すると作られます。" }));
|
|
5330
5577
|
box.appendChild(copyRow(DRIVE_INGEST_ACCOUNT));
|
|
5331
5578
|
|
|
5332
5579
|
/*
|
|
5333
|
-
* 🔴 **「全部見られてしまうのでは」に答える**(2026-08-29
|
|
5580
|
+
* 🔴 **「全部見られてしまうのでは」に答える**(2026-08-29 オーナー確定文)。
|
|
5581
|
+
* **2026-09-03 に1語だけ更新した(#393)**——「ファイル名だけを見ます」は、共有フォルダの
|
|
5582
|
+
* 1階層下まで見るようになって不正確になったので「フォルダ名とファイル名だけを見ます」に。
|
|
5583
|
+
* それ以外は表記も含めて変えない。
|
|
5334
5584
|
*
|
|
5335
5585
|
* すぐ下の注記(.mt-share-note)は「**取り込むか**」に答えていて、
|
|
5336
5586
|
* 「**見るのか**」に答えていない。共有をためらう理由はそちらなので、専用に1行置く。
|
|
@@ -5358,7 +5608,7 @@ ${deriveCapTile.toString()}
|
|
|
5358
5608
|
: mark + "に当てはまらない";
|
|
5359
5609
|
box.appendChild(el("p", {
|
|
5360
5610
|
class: "mt-privacy",
|
|
5361
|
-
text: "⚠ 共有を開始すると、bot
|
|
5611
|
+
text: "⚠ 共有を開始すると、bot はこのフォルダの中のフォルダ名とファイル名だけを見ます。" + scope +
|
|
5362
5612
|
"メモは、中身を開きません。読み取りも保存も行わないので、1on1や面談のメモがどこかに残ることはありません。",
|
|
5363
5613
|
}));
|
|
5364
5614
|
|
|
@@ -5368,20 +5618,22 @@ ${deriveCapTile.toString()}
|
|
|
5368
5618
|
// 押す前に書いておく(押した先で迷子にしない)
|
|
5369
5619
|
box.appendChild(el("a", {
|
|
5370
5620
|
class: "mt-share-open", href: MEET_FOLDER_SEARCH_URL, target: "_blank", rel: "noopener",
|
|
5371
|
-
text: "自分の「Meet
|
|
5621
|
+
text: "自分の「Google Meet」フォルダを開く →",
|
|
5372
5622
|
}));
|
|
5373
5623
|
var steps = el("ol", { class: "mt-share-steps" });
|
|
5374
|
-
|
|
5375
|
-
|
|
5624
|
+
// 🔴 **どれを選ぶかをここで言い切る。** 検索結果には会議ごとのフォルダも並ぶので、
|
|
5625
|
+
// 「検索されます」だけで終えると、末尾に日付の付いたフォルダを共有してしまう人が出る
|
|
5626
|
+
steps.appendChild(el("li", { text: "上のリンクを開くと、自分のドライブの中から「Google Meet」フォルダが検索されます。名前がちょうど「Google Meet」のものを選んでください。末尾に日付や (recurring) が付いたものは会議ごとのフォルダなので、それらを入れている親を選びます。" }));
|
|
5627
|
+
steps.appendChild(el("li", { text: "そのフォルダを右クリック(または右上の ⋮)して「共有」→「共有」を選びます(画像は旧名称「Meet Recordings」のときの画面ですが、操作は同じです)。" }, [
|
|
5376
5628
|
// **寸法を属性で書く。** 書かないと、モーダルを開いた瞬間に画像の高さのぶんだけ
|
|
5377
5629
|
// 下の手順がずれる。loading=lazy は付けない——**data: URI には取得が無いので
|
|
5378
5630
|
// 何もしない**のに、付いていると「遅延読み込みしている」という誤解が仕様として残る
|
|
5379
5631
|
el("img", { class: "mt-shot", src: MEET_SHOT_STEP1, width: "664", height: "425",
|
|
5380
|
-
alt: "Google
|
|
5632
|
+
alt: "Googleドライブの検索結果。フォルダのメニューで「共有」を選び、右に開いたサブメニューの「共有」を指している画面(旧名称「Meet Recordings」のときの例)。" }),
|
|
5381
5633
|
]));
|
|
5382
|
-
steps.appendChild(el("li", { text: "
|
|
5634
|
+
steps.appendChild(el("li", { text: "上のアカウントを貼り付け、権限を「閲覧者」にして「共有」を押します(通知のチェックは外して構いません。画像は旧名称「Meet Recordings」のときの画面ですが、操作は同じです)。" }, [
|
|
5383
5635
|
el("img", { class: "mt-shot", src: MEET_SHOT_STEP2, width: "425", height: "220",
|
|
5384
|
-
alt: "
|
|
5636
|
+
alt: "フォルダを共有するダイアログ。宛先に cortex-notetaker のアカウントが入り、権限が「閲覧者」に設定され、右下に「共有」ボタンがある画面(旧名称「Meet Recordings」のときの例)。" }),
|
|
5385
5637
|
]));
|
|
5386
5638
|
box.appendChild(steps);
|
|
5387
5639
|
|
|
@@ -6135,7 +6387,7 @@ ${deriveCapTile.toString()}
|
|
|
6135
6387
|
// 記録の追加でも進むので、それで捨てると下地の古い行が残ったまま仮置きだけ消える
|
|
6136
6388
|
var kept = prunePendingOps(PENDING_OPS, {
|
|
6137
6389
|
now: Date.now(),
|
|
6138
|
-
siteNowMs:
|
|
6390
|
+
siteNowMs: pendingSiteTimeMs(rawFleetVal("generatedAt")),
|
|
6139
6391
|
live: LIVE_REGISTERED,
|
|
6140
6392
|
liveAt: LIVE_REGISTERED_AT,
|
|
6141
6393
|
});
|
|
@@ -6151,7 +6403,7 @@ ${deriveCapTile.toString()}
|
|
|
6151
6403
|
* ——失敗した操作を「入っている」ように見せない(#184 の懸念その1)。
|
|
6152
6404
|
* 作れたエントリを返す(切り出せない入力では null=従来どおり live の読み直しに任せる)
|
|
6153
6405
|
*/
|
|
6154
|
-
function notePendingOp(payload, targets) {
|
|
6406
|
+
function notePendingOp(payload, targets, response) {
|
|
6155
6407
|
if (!SITE.intake) return null;
|
|
6156
6408
|
var entry = pendingOpOf(payload, {
|
|
6157
6409
|
projectKey: SITE.intake.projectKey,
|
|
@@ -6160,6 +6412,8 @@ ${deriveCapTile.toString()}
|
|
|
6160
6412
|
generatedAt: rawFleetVal("generatedAt") || "",
|
|
6161
6413
|
// Backlog の解除は送る payload にドメインが無いので、表示中の一覧から補う
|
|
6162
6414
|
targets: targets || null,
|
|
6415
|
+
// 保存の応答(#391)。Slack の追加で、サーバが実測した組織(slackOrg)を受け取る
|
|
6416
|
+
response: response || null,
|
|
6163
6417
|
});
|
|
6164
6418
|
if (!entry) return null;
|
|
6165
6419
|
// **書く前に読み直す。** メモリの一覧に足して書き戻すだけだと、別のタブが書いた仮置きを
|
|
@@ -7163,15 +7417,58 @@ ${deriveCapTile.toString()}
|
|
|
7163
7417
|
return sourceAdapter(s.type) === kind;
|
|
7164
7418
|
});
|
|
7165
7419
|
}
|
|
7166
|
-
/**
|
|
7420
|
+
/**
|
|
7421
|
+
* 行の見た目を決める値だけを並べる(時刻など毎回変わる値は入れない)。
|
|
7422
|
+
* 🔴 **「確かめられたか」も混ぜる(#409)。** 混ぜないと、応答が届いて
|
|
7423
|
+
* 注記の要否が変わっても署名が同じままで、**一覧が作り直されない**
|
|
7424
|
+
*(0件の一覧は行が増えないので、行だけを見ていると変化が現れない)
|
|
7425
|
+
*/
|
|
7167
7426
|
function externalsSig(kind, extra) {
|
|
7168
|
-
return JSON.stringify([liveExternals(kind), extra]);
|
|
7427
|
+
return JSON.stringify([liveExternals(kind), extra, registrationUnreadable()]);
|
|
7169
7428
|
}
|
|
7170
7429
|
/**
|
|
7171
7430
|
* **空の箱を置いてよいのは、そこへ足せる案件だけ。** 投入の口を持たない構成
|
|
7172
7431
|
* (素の Decisions ビュー)で行も無い能力に空の箱を積むと、押しても何も無いモーダルになる。
|
|
7173
7432
|
*/
|
|
7174
7433
|
function liveListWanted(kind) { return !!SITE.intake || liveExternals(kind).length > 0; }
|
|
7434
|
+
/**
|
|
7435
|
+
* 🔴 **「1件も登録されていない」と「確かめられなかった」を分ける(#409)。**
|
|
7436
|
+
*
|
|
7437
|
+
* サーバは登録ファイルを読めなかった能力を、**キーごと応答から落とす**
|
|
7438
|
+
*(「読めなかった」を「0件」と混ぜないため。intake の buildRegisteredNow)。
|
|
7439
|
+
* ところが画面はそれを「足す行が無い」としか受け取っていなかったので、
|
|
7440
|
+
* **確かめられなかった結果が「登録されていません」として出ていた**。
|
|
7441
|
+
* オーナーはそれを見て未登録だと読み、追加して「既に登録されています」と断られた。
|
|
7442
|
+
*
|
|
7443
|
+
* 🔴 **能力ごとのキーの有無で判定してはいけない(実装レビューの指摘)。** サーバは
|
|
7444
|
+
* 「読めなかった」ときだけでなく **「そのファイルがまだ無い」ときにも同じくキーを落とす**
|
|
7445
|
+
* (読んだ本文が null でも undefined でも、文字列でなければ同じ枝に落ちる)。
|
|
7446
|
+
* キーの有無で見ると、**その能力をまだ使っていない案件**——つまり立ち上げ直後の、
|
|
7447
|
+
* この画面が一番よく見られる状態——に**永久に消えない誤警報**が出る
|
|
7448
|
+
* (「再読み込みしてください」と言われても、設定ファイルは再読み込みでは生えない)。
|
|
7449
|
+
* しかも製品としては、その状態は「登録されていません」と言い切ると決めてある
|
|
7450
|
+
* (engine の capabilities: ファイルの有無は実装の都合で、利用者にはどちらも「まだ登録していない」)。
|
|
7451
|
+
*
|
|
7452
|
+
* **だから応答まるごとで見る。** 登録ファイルの一覧(registeredNow)と
|
|
7453
|
+
* 能力の状態(capabilitiesNow)は**サーバの同じ return から出る**ので、
|
|
7454
|
+
* **両方とも無いときだけ**が「そもそも読みに行けなかった」——今回の症状の
|
|
7455
|
+
* 3経路(ツリー取得の失敗・ツリーの打ち切り・応答そのものの失敗)は全部これに当たる。
|
|
7456
|
+
*
|
|
7457
|
+
* ⚠ この粒度では、**ファイル1本だけが読めなかった回**(8秒の打ち切り・単発の失敗)は
|
|
7458
|
+
* 拾えない。そちらはサーバがログを出す経路なので、まず記録から追えるようにしてある。
|
|
7459
|
+
*/
|
|
7460
|
+
function registrationUnreadable() {
|
|
7461
|
+
return liveAnswered && LIVE_REGISTERED === null && LIVE_CAPS === null;
|
|
7462
|
+
}
|
|
7463
|
+
/**
|
|
7464
|
+
* 空の一覧の代わりに出す1行。**帯は症状・行は一手・繰り返さない。**
|
|
7465
|
+
* 空の一覧そのものを出さない——今回オーナーが踏んだのは「空の一覧を見て未登録だと読んだ」ことなので。
|
|
7466
|
+
* 応答がまだ届いていない間は出さない(確かめる前に「確かめられなかった」と言わない)。
|
|
7467
|
+
*/
|
|
7468
|
+
function unverifiedListNote(noun) {
|
|
7469
|
+
if (!registrationUnreadable()) return null;
|
|
7470
|
+
return el("div", { class: "ds-note", text: "登録されている" + noun + "を確認できませんでした。再読み込みしてください。" });
|
|
7471
|
+
}
|
|
7175
7472
|
// **箱にはアダプタのタグを付ける(#153)。** pushCap の無タグは「どのタイルにも出す」
|
|
7176
7473
|
// 既定なので、タグを忘れると Slack の一覧が Google Chat モーダルに丸ごと出る
|
|
7177
7474
|
if (liveListWanted("slack")) pushCap("チャット", liveList(
|
|
@@ -7179,7 +7476,11 @@ ${deriveCapTile.toString()}
|
|
|
7179
7476
|
function () { return externalsSig("slack", slackWorkspaces()); },
|
|
7180
7477
|
function (box) {
|
|
7181
7478
|
var items = liveExternals("slack");
|
|
7182
|
-
if (!items.length)
|
|
7479
|
+
if (!items.length) {
|
|
7480
|
+
var noteSlack = unverifiedListNote("チャンネル");
|
|
7481
|
+
if (noteSlack) box.appendChild(noteSlack);
|
|
7482
|
+
return;
|
|
7483
|
+
}
|
|
7183
7484
|
var meta = DS_TYPE["slack"] || { icon: "🔗", label: "Slack" };
|
|
7184
7485
|
slackRows(items, meta).forEach(function (n) { box.appendChild(n); });
|
|
7185
7486
|
},
|
|
@@ -7191,7 +7492,11 @@ ${deriveCapTile.toString()}
|
|
|
7191
7492
|
function () { return externalsSig("google-chat", null); },
|
|
7192
7493
|
function (box) {
|
|
7193
7494
|
var items = liveExternals("google-chat");
|
|
7194
|
-
if (!items.length)
|
|
7495
|
+
if (!items.length) {
|
|
7496
|
+
var noteGchat = unverifiedListNote("スペース");
|
|
7497
|
+
if (noteGchat) box.appendChild(noteGchat);
|
|
7498
|
+
return;
|
|
7499
|
+
}
|
|
7195
7500
|
var meta = DS_TYPE["google-chat"] || { icon: "💬", label: "Google Chat" };
|
|
7196
7501
|
items.forEach(function (s) { box.appendChild(extSourceCard(s, meta)); });
|
|
7197
7502
|
},
|
|
@@ -7201,7 +7506,11 @@ ${deriveCapTile.toString()}
|
|
|
7201
7506
|
function () { return externalsSig("github", githubOwnersView() || null); },
|
|
7202
7507
|
function (box) {
|
|
7203
7508
|
var items = liveExternals("github");
|
|
7204
|
-
if (!items.length)
|
|
7509
|
+
if (!items.length) {
|
|
7510
|
+
var noteDev = unverifiedListNote("リポジトリ");
|
|
7511
|
+
if (noteDev) box.appendChild(noteDev);
|
|
7512
|
+
return;
|
|
7513
|
+
}
|
|
7205
7514
|
// **meta は行ごとに引く**(issues と discussions でアイコンとラベルが違う)
|
|
7206
7515
|
githubRows(items).forEach(function (n) { box.appendChild(n); });
|
|
7207
7516
|
},
|
|
@@ -9553,22 +9862,43 @@ ${deriveCapTile.toString()}
|
|
|
9553
9862
|
// 案内(アプリ追加の操作列)は独立の箱にせず、この手順リストが持つ(#170/#174 の圧縮方針。
|
|
9554
9863
|
// fleet-status にデータが無い案件でもフォームは常設なので、案内も常に出る)
|
|
9555
9864
|
//
|
|
9556
|
-
// Google Chat アプリの表示名。**Marketplace
|
|
9865
|
+
// Google Chat アプリの表示名。**Marketplace 公開名(2026-09-03 オーナー決定で
|
|
9866
|
+
// 「Cortex Bot」から改称・#387。このモーダル分は #389 で済ませた)。**
|
|
9557
9867
|
// 検索ワードとして本文に組み込む(コピー行は置かない——コマンドではなく検索語のため)
|
|
9558
|
-
var GCHAT_APP_NAME = "Cortex
|
|
9868
|
+
var GCHAT_APP_NAME = "Cortex by Classmethod";
|
|
9559
9869
|
box.appendChild(addForm({
|
|
9560
9870
|
title: "+ Google Chat スペースを追加",
|
|
9561
|
-
|
|
9562
|
-
|
|
9871
|
+
// **いま画面に出ている形を例示する(#389)。** 旧UIの /room/ を例にしていたので、
|
|
9872
|
+
// 新UIの人は自分のアドレスバーと見比べて「別のものを求められている」と読む
|
|
9873
|
+
placeholder: "https://chat.google.com/app/chat/XXXXXXXXXXX",
|
|
9874
|
+
// 素の spaces/<ID> も受ける(#389)ので、URL しか受けないとは書かない
|
|
9875
|
+
requiredNote: "スペースの URL か spaces/… を入力してください。",
|
|
9563
9876
|
// 手順リスト(#170 の型)。アプリ追加の話が先——Slack の招待と同じで、入っていないと
|
|
9564
9877
|
// 1件も読めない。操作列は Google 公式ヘルプの現行手順に合わせる
|
|
9565
9878
|
// (support.google.com/chat/answer/7655820)。DM が対象外であることもここで言う——
|
|
9566
9879
|
// 貼って 400 になってから知るより先に
|
|
9880
|
+
//
|
|
9881
|
+
// **1行1手に詰める(2026-09-03 オーナー指示・#389)。** 手順の中に事情の説明が
|
|
9882
|
+
// 混ざると、読む人が「自分がいま何をすべきか」を自分で切り出すことになる。
|
|
9883
|
+
// 🔴 落とさない語(既存テストが固定している): 「アプリと統合」「Gold 昇格の対象になり」
|
|
9884
|
+
// 「登録を解除」「22時」——重さと止め方を伝える場所がこの手順しか無いため
|
|
9567
9885
|
hint: [
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9886
|
+
{
|
|
9887
|
+
text: "読み取りたいスペースに Cortex のアプリを追加します。スペース名 → 「アプリと統合」 → 「アプリを追加」 で、「" + GCHAT_APP_NAME + "」を検索して追加します。",
|
|
9888
|
+
// **画像は補助で、上の1文だけで手順が完結する**(読み上げ・画像が出ない環境)。
|
|
9889
|
+
// 寸法は属性で明示——書かないと、モーダルを開いた瞬間に画像の高さのぶん下の手順がずれる。
|
|
9890
|
+
// loading=lazy は付けない(data: URI には取得が無いので何もしない・Meet と同じ)。
|
|
9891
|
+
// 2枚を div でくるむのは、hintNode の口を広げないため(1手順=1ノードの約束を保つ)
|
|
9892
|
+
node: el("div", {}, [
|
|
9893
|
+
el("img", { class: "mt-shot", src: GCHAT_SHOT_STEP1, width: "900", height: "566",
|
|
9894
|
+
alt: "Google Chat のスペースの画面。スペース名の横のメニューが開いていて、その中の「アプリと統合」が赤い枠で示されている。" }),
|
|
9895
|
+
el("img", { class: "mt-shot", src: GCHAT_SHOT_STEP2, width: "900", height: "491",
|
|
9896
|
+
alt: "「アプリと統合」の画面。右上の「+ アプリを追加」が赤い枠で示され、その下に、このスペースに追加されているアプリの一覧が並んでいる。" }),
|
|
9897
|
+
]),
|
|
9898
|
+
},
|
|
9899
|
+
"検索に出ないときは、貴社の Google Workspace 管理者の承認がまだです(承認できるのは管理者だけ・反映まで最大24時間)。",
|
|
9900
|
+
"下の欄に、ブラウザでスペースを開いたときのURL(chat.google.com/app/chat/… または /room/…)を貼って「追加」を押します。DM(1対1のチャット)は対象外です。",
|
|
9901
|
+
"追加したスペースは毎晩の Gold 昇格の対象になり、Decision・用語集の材料になります。止めるときは、その行の「登録を解除」を押します(今夜のぶんは22時ごろまでに解除すれば取り込まれません)。",
|
|
9572
9902
|
],
|
|
9573
9903
|
build: function (url) {
|
|
9574
9904
|
return { op: "add-google-chat-space", url: url };
|
|
@@ -9762,6 +10092,37 @@ ${deriveCapTile.toString()}
|
|
|
9762
10092
|
msg.textContent = "✗ " + (j.message || "保存できませんでした。");
|
|
9763
10093
|
return;
|
|
9764
10094
|
}
|
|
10095
|
+
// 🔴 **「既に登録されています」は失敗ではない(#409)。**
|
|
10096
|
+
//
|
|
10097
|
+
// サーバがそう答えた時点で、その接続先は**登録ファイルに確かに在る**。
|
|
10098
|
+
// それまでは赤い「✗」を出して終わっていたので、**一覧が空のまま**の画面で
|
|
10099
|
+
// 追加した人は「登録されているらしいが、どこにも出ていない」という袋小路に落ちた
|
|
10100
|
+
// (オーナーが cortex-test で踏んだ症状そのもの)。
|
|
10101
|
+
// **サーバが教えてくれた事実を、そのまま一覧に反映する。**
|
|
10102
|
+
//
|
|
10103
|
+
// 仮置き(#388)に足すのは、保存が効いたときと同じ扱いでよい——
|
|
10104
|
+
// 「登録ファイルに在る」という主張はどちらも同じで、仮置きは本物が追いつけば消える。
|
|
10105
|
+
if (isAlreadyRegistered(j.message)) {
|
|
10106
|
+
var known = notePendingOp(payload, hooks && typeof hooks.pendingTargets === "function" ? hooks.pendingTargets() : null, j);
|
|
10107
|
+
if (known) {
|
|
10108
|
+
// 帯は症状ではなく**いまの事実**。行が横に出るので、繰り返さない
|
|
10109
|
+
msg.className = "ds-toggle-msg is-ok";
|
|
10110
|
+
msg.textContent = "✓ " + j.message + "(一覧に出しました)";
|
|
10111
|
+
repaintOpenLists();
|
|
10112
|
+
endBusy();
|
|
10113
|
+
// 🔴 **成功と同じ後始末をする(実装レビューの指摘)。** onDone は入力欄を空にする
|
|
10114
|
+
// ——空にしないと、行が出たと言いながら URL が欄に残り、**もう一度押させる形**になる
|
|
10115
|
+
// (そのうえ「触っている最中」と見なされて、節ごとの差し替えが以後ずっと見送られる)。
|
|
10116
|
+
// 仮置きの entry を渡すので、控えの二重表示にはならない。
|
|
10117
|
+
// ⚠ **節ごとの差し替えは呼ばない。** 入力欄と結果メッセージを抱えたこの節は
|
|
10118
|
+
// 「触っている最中」なので、あちらは必ず早期 return する(呼んでも何も起きない)。
|
|
10119
|
+
// 一覧を追いつかせているのは上の repaintOpenLists。
|
|
10120
|
+
if (onDone) onDone(j, { pending: known });
|
|
10121
|
+
// **登録の即時読み取りを取りに行く。** 仮置きは繋ぎで、本物はサーバが持っている
|
|
10122
|
+
loadLiveFleet(true);
|
|
10123
|
+
return;
|
|
10124
|
+
}
|
|
10125
|
+
}
|
|
9765
10126
|
// サーバの文言をそのまま出す(貼り方の案内・取り違えの警告を含んでいる)。
|
|
9766
10127
|
// **例外は「他の人が先に変えていた」だけ**——サーバは再読み込みを案内するが、
|
|
9767
10128
|
// 画面は既に追いついているので、済んでいることをやらせる案内になる(#127)
|
|
@@ -9775,7 +10136,7 @@ ${deriveCapTile.toString()}
|
|
|
9775
10136
|
// その場で「反映待ち」の行が出ている横では嘘(intake の文面は本便では触らない)。
|
|
9776
10137
|
// **changed:false では書かない**(何も変わっていないのに追加したと見せない)
|
|
9777
10138
|
var pendingEntry = j.changed !== false
|
|
9778
|
-
? notePendingOp(payload, hooks && typeof hooks.pendingTargets === "function" ? hooks.pendingTargets() : null)
|
|
10139
|
+
? notePendingOp(payload, hooks && typeof hooks.pendingTargets === "function" ? hooks.pendingTargets() : null, j)
|
|
9779
10140
|
: null;
|
|
9780
10141
|
msg.className = "ds-toggle-msg is-ok";
|
|
9781
10142
|
msg.textContent = "";
|
|
@@ -9819,6 +10180,7 @@ ${deriveCapTile.toString()}
|
|
|
9819
10180
|
}
|
|
9820
10181
|
repaintOpenLists();
|
|
9821
10182
|
refreshDataSources();
|
|
10183
|
+
syncAiLinks();
|
|
9822
10184
|
}
|
|
9823
10185
|
if (hooks && typeof hooks.onSaved === "function") hooks.onSaved(j);
|
|
9824
10186
|
// **保存が効いたら、いまの登録を読み直す(段階1)。** 一覧はまとめ表(夜間バッチ)から
|
|
@@ -10064,6 +10426,8 @@ ${deriveCapTile.toString()}
|
|
|
10064
10426
|
* まだ文章なので、そちらを壊さない。横展開でそれぞれ配列に変えていく。
|
|
10065
10427
|
*
|
|
10066
10428
|
* 各要素は文字列か、{ text, node } (手順の中に部品を挟むとき。招待コマンドなど)。
|
|
10429
|
+
* **1手順に2つ以上を挟むときは、呼び出し側が div でくるんで node に渡す**(#389 の手順画像
|
|
10430
|
+
* 2枚)——ここを「配列も受ける」に広げると、5つのフォームが通る共通部品の口が増える。
|
|
10067
10431
|
*/
|
|
10068
10432
|
function hintNode(hint) {
|
|
10069
10433
|
if (!hint) return null;
|
|
@@ -10253,9 +10617,10 @@ ${deriveCapTile.toString()}
|
|
|
10253
10617
|
// 折りたたみの見出しでまとめるのをやめた(オーナー指摘「複雑になっちゃってる」)ので、
|
|
10254
10618
|
// **どの組織かを言うのは行のラベルだけ**になった。社内に何も出さないと、
|
|
10255
10619
|
// ラベルの無い行が「社内」なのか「まだ分かっていない」のかを読み分けられない。
|
|
10256
|
-
// **仮置きの行(#388
|
|
10257
|
-
//
|
|
10258
|
-
|
|
10620
|
+
// **仮置きの行(#388/#391)は、サーバが実測した組織を応答で受け取れたときだけ札を出す。**
|
|
10621
|
+
// 受け取れない古い intake の窓では札ごと出さない——ここで既定に落とすと
|
|
10622
|
+
// 「クラスメソッド」と名乗り、測っていないことを断言してしまう
|
|
10623
|
+
if (s.type === "slack" && (s.pending !== true || s.pendingOrg === true)) {
|
|
10259
10624
|
// **順序は groupSlackByOrg と揃える(workspace が先)。**
|
|
10260
10625
|
// 逆にすると、判定が済んで workspace が入った行に古い印が残っているとき、
|
|
10261
10626
|
// **並びは「Hapio AI」なのにラベルだけ「組織が分かりません」**になる
|
|
@@ -10829,6 +11194,10 @@ ${deriveCapTile.toString()}
|
|
|
10829
11194
|
repaintOpenLists();
|
|
10830
11195
|
// まだ足りない行があるなら取り直しを続ける(無くなったら止める)
|
|
10831
11196
|
refreshStateTimer();
|
|
11197
|
+
// 開発リポジトリが増えていれば AI 導線の href を組み直す(#404)。
|
|
11198
|
+
// 🔴 **後段の仕事より後ろに置く。** ここで投げると、図の復帰・取り直しタイマーが
|
|
11199
|
+
// 丸ごと落ちる(href を貼り替えるだけの処理に、そこまで巻き込ませない)
|
|
11200
|
+
syncAiLinks();
|
|
10832
11201
|
// 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
|
|
10833
11202
|
if (freshWanted) loadLiveFleet();
|
|
10834
11203
|
})
|
|
@@ -10851,6 +11220,12 @@ ${deriveCapTile.toString()}
|
|
|
10851
11220
|
// **失敗しても取り直しは続ける**(期限が来れば止まる)。ここで止めると、
|
|
10852
11221
|
// 一時的な失敗のあと画面が二度と追いつかない
|
|
10853
11222
|
refreshStateTimer();
|
|
11223
|
+
// 🔴 **ここでも AI 導線を貼り替える(#404)。** 「失敗」に見えて、
|
|
11224
|
+
// **登録の即時値(registeredNow)は既に読めている**ことがある——まとめ表
|
|
11225
|
+
// (fleet-status.json)をまだ持たない案件は、上で fleet が無いと投げてここへ来る。
|
|
11226
|
+
// つまり**立ち上げ直後の案件=開発リポジトリを登録したばかりの案件**では、
|
|
11227
|
+
// この行だけがヘッダーの導線を追いつかせる。**後段の仕事より後ろに置く**(上と同じ理由)
|
|
11228
|
+
syncAiLinks();
|
|
10854
11229
|
// 取得中に保存が効いていたら、迂回付きでもう一度取りに行く
|
|
10855
11230
|
if (freshWanted) loadLiveFleet();
|
|
10856
11231
|
});
|
|
@@ -10904,7 +11279,7 @@ ${deriveCapTile.toString()}
|
|
|
10904
11279
|
el("span", { text: ": 精製された確定情報を読む" }),
|
|
10905
11280
|
]));
|
|
10906
11281
|
var codeLi = el("li", {}, [
|
|
10907
|
-
el("a", { href:
|
|
11282
|
+
el("a", { href: CLAUDE_CODE_HOME, target: "_blank", rel: "noopener", text: "Claude Code" }),
|
|
10908
11283
|
el("span", { text: ": 議事録・課題・資料の生データまで含めて、AIに質問・調査する" }),
|
|
10909
11284
|
]);
|
|
10910
11285
|
ways.appendChild(codeLi);
|
|
@@ -12569,8 +12944,8 @@ ${deriveCapTile.toString()}
|
|
|
12569
12944
|
* 「作ったはずのものが出てこない」状態が永久に続く(破棄の条件は applyDeleteTombstones)。
|
|
12570
12945
|
*/
|
|
12571
12946
|
function deletedStoreKey() { return "cortex-deleted:" + REPO_SLUG; }
|
|
12572
|
-
|
|
12573
|
-
|
|
12947
|
+
// 「削除より後に始まったビルドか」を判断するときの余裕は PENDING_OPS_MARGIN_MS(30分)。
|
|
12948
|
+
// **同じ性質の待ちなので定数も1つ**(#391)——別に持つと片方だけ直したときに割れる
|
|
12574
12949
|
|
|
12575
12950
|
function readTombstones() {
|
|
12576
12951
|
try {
|
|
@@ -12596,14 +12971,9 @@ ${deriveCapTile.toString()}
|
|
|
12596
12971
|
list.push({ path: path, id: (rec && rec.id) || "", at: Date.now(), gen0: SITE.generatedAt || "" });
|
|
12597
12972
|
writeTombstones(list);
|
|
12598
12973
|
}
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
*/
|
|
12603
|
-
function siteTimeMs(s) {
|
|
12604
|
-
if (typeof s !== "string" || !s) return NaN;
|
|
12605
|
-
return Date.parse(s.replace(" UTC", "Z").replace(" ", "T"));
|
|
12606
|
-
}
|
|
12974
|
+
// ビルド時刻の文字列(cli.ts が入れる "YYYY-MM-DD HH:mm UTC")→ ms は
|
|
12975
|
+
// pendingSiteTimeMs(pending-ops.ts の実体を流し込んである)を使う。**写しを持たない**
|
|
12976
|
+
//——同じ変換が2つあると、片方だけ直したときに「消したのに戻る」が片側でだけ起きる(#391)
|
|
12607
12977
|
/**
|
|
12608
12978
|
* このトゥームストーンはもう用済みか(=削除より後のビルドにも記録が残っている)。
|
|
12609
12979
|
*
|
|
@@ -12617,12 +12987,8 @@ ${deriveCapTile.toString()}
|
|
|
12617
12987
|
* 再表示が遅れるだけ)。
|
|
12618
12988
|
*/
|
|
12619
12989
|
function tombstoneStale(t, nowMs) {
|
|
12620
|
-
if (
|
|
12621
|
-
|
|
12622
|
-
var gen0 = siteTimeMs(t.gen0);
|
|
12623
|
-
// gen0 を持たない古い形式のエントリは at だけで見る(後方互換・安全側)
|
|
12624
|
-
if (isNaN(gen0)) return byAt;
|
|
12625
|
-
return byAt && nowMs > gen0 + TOMBSTONE_MARGIN_MS;
|
|
12990
|
+
if (!t) return false;
|
|
12991
|
+
return pendingStale(t.at, t.gen0, nowMs);
|
|
12626
12992
|
}
|
|
12627
12993
|
/** トゥームストーンの相手を SITE から探す(id で引き、無ければパスで照合) */
|
|
12628
12994
|
function findTombstoned(t) {
|
|
@@ -12647,7 +13013,7 @@ ${deriveCapTile.toString()}
|
|
|
12647
13013
|
function applyDeleteTombstones() {
|
|
12648
13014
|
var list = readTombstones();
|
|
12649
13015
|
if (!list.length) return;
|
|
12650
|
-
var nowMs =
|
|
13016
|
+
var nowMs = pendingSiteTimeMs(SITE.generatedAt);
|
|
12651
13017
|
var kept = [];
|
|
12652
13018
|
for (var i = 0; i < list.length; i++) {
|
|
12653
13019
|
var t = list[i];
|
|
@@ -13841,6 +14207,41 @@ ${deriveCapTile.toString()}
|
|
|
13841
14207
|
}
|
|
13842
14208
|
}
|
|
13843
14209
|
|
|
14210
|
+
/**
|
|
14211
|
+
* ポータル配下で index.html に着いたら、アドレスを末尾スラッシュの形へ書き戻す(#400)。
|
|
14212
|
+
*
|
|
14213
|
+
* 🔴 **なぜ要るか。** ポータルの「門」(CloudFront)は、署名付きCookieの検証が
|
|
14214
|
+
* CloudFront Function より先に走る制約のせいで、**入口(末尾スラッシュのURL)にしか
|
|
14215
|
+
* 置けない**。門は通行証を持っている人を index.html(署名必須の側)へ送り出すので、
|
|
14216
|
+
* 何もしないと**アドレス欄が index.html のまま**になる。そのURLは門に当たらないため、
|
|
14217
|
+
* ブックマーク・Slack共有されると翌日(通行証の期限切れ)にまた素の403になり、
|
|
14218
|
+
* メンバーでない人が共有リンクを開いても参加リクエストの導線が出ない。
|
|
14219
|
+
* ここで書き戻せば、人がコピーするURLが常に門の効く形になる。
|
|
14220
|
+
*
|
|
14221
|
+
* 🔴 **クエリとハッシュを持ち越す。** 書き戻したあとの共有URLは**必ず門を通る**ので、
|
|
14222
|
+
* クエリを落とすと門の302がそれを運べない限り復元できない(門は運ぶ作りになっている)。
|
|
14223
|
+
* ハッシュは302をまたいでブラウザが引き継ぐ。
|
|
14224
|
+
* ⚠ ただし**ビューアはハッシュを読んでいない**(本文はJSで後から描くので、ブラウザの
|
|
14225
|
+
* フラグメントスクロールも効かない)。ここで保つのは「アドレス欄に残す」ところまでで、
|
|
14226
|
+
* 見出しまでスクロールする機能ではない。タブを1回押すと pushState が落とす
|
|
14227
|
+
*
|
|
14228
|
+
* ⚠ 表示にも相対参照にも影響しない——index.html と末尾スラッシュは相対URLの基準
|
|
14229
|
+
* (ディレクトリ)が同じ。以降の pushState も相対なので、ここを直せば後続の遷移も
|
|
14230
|
+
* すべて末尾スラッシュ基準になる。
|
|
14231
|
+
* ⚠ 範囲は**案件と向けの2階層がそろったポータル配下**だけ(下の linkTitleToPortal は
|
|
14232
|
+
* 案件1階層でよいので、同じ形に見えて要件が違う。共通化しない)。Amplify配信や
|
|
14233
|
+
* ローカルの静的サーバで index.html を直接開いている人のアドレスは触らない
|
|
14234
|
+
*/
|
|
14235
|
+
(function rewritePortalIndexUrl() {
|
|
14236
|
+
if (!/^\\/p\\/[^/]+\\/[^/]+\\//.test(location.pathname)) return;
|
|
14237
|
+
if (!/\\/index\\.html$/.test(location.pathname)) return;
|
|
14238
|
+
try {
|
|
14239
|
+
history.replaceState(history.state, "", "./" + location.search + location.hash);
|
|
14240
|
+
} catch (e) {
|
|
14241
|
+
// file:// など replaceState が使えない環境では何もしない(表示には影響しない)
|
|
14242
|
+
}
|
|
14243
|
+
})();
|
|
14244
|
+
|
|
13844
14245
|
window.addEventListener("popstate", route);
|
|
13845
14246
|
/**
|
|
13846
14247
|
* ヘッダーの案件名(#379)。**ポータル配下(/p/<key>/ の下)で開いているときだけ**、
|
|
@@ -13891,6 +14292,10 @@ ${deriveCapTile.toString()}
|
|
|
13891
14292
|
// 保存直後の mirror とメニューの開閉が、タブを移るたびに畳まれる
|
|
13892
14293
|
renderSiteIcon();
|
|
13893
14294
|
route();
|
|
14295
|
+
// AI 導線の href を起動時に1回だけ組み直す(#404)。仮置き(localStorage)は
|
|
14296
|
+
// ビルド時には存在しないので、**焼いた href は起動の時点で既に古いことがある**。
|
|
14297
|
+
// 🔴 route() の後に置く——ヘッダー以外の導線は route() が描くまで DOM に無い
|
|
14298
|
+
syncAiLinks();
|
|
13894
14299
|
/**
|
|
13895
14300
|
* 接続状況の先読み(#264)。**開いたタブに関係なく、起動時に1回だけ取りに行く。**
|
|
13896
14301
|
*
|