@takagaki/cortex-decisions-viewer 0.12.26 → 0.12.28
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 +1 -72
- package/dist/render.js +358 -1229
- package/package.json +1 -1
package/dist/render.js
CHANGED
|
@@ -6,8 +6,6 @@ exports.renderSiteWithAssets = renderSiteWithAssets;
|
|
|
6
6
|
exports.viewerAssetSources = viewerAssetSources;
|
|
7
7
|
exports.viewerAssetNames = viewerAssetNames;
|
|
8
8
|
exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
|
|
9
|
-
exports.sanitizeHarnessSchedules = sanitizeHarnessSchedules;
|
|
10
|
-
exports.sanitizeHarnessShelf = sanitizeHarnessShelf;
|
|
11
9
|
exports.sanitizeStateNow = sanitizeStateNow;
|
|
12
10
|
exports.overlayStateOnFleet = overlayStateOnFleet;
|
|
13
11
|
exports.chatRowsToAdd = chatRowsToAdd;
|
|
@@ -185,9 +183,10 @@ const ADAPTER_CATALOG = {
|
|
|
185
183
|
// Slack を先頭に保つ(入れ替えると接続マップの飛び先が Google Chat 側へ変わる)
|
|
186
184
|
"チャット": ["slack", "google-chat"],
|
|
187
185
|
"開発": ["github"],
|
|
188
|
-
// **アダプタを持たない設定タイル。**
|
|
189
|
-
//
|
|
190
|
-
// renderAdapterTile
|
|
186
|
+
// **アダプタを持たない設定タイル。** 外部ツールとの接続ではなくポータルのハーネス画面への
|
|
187
|
+
// リンク(#369)なので、繋ぐ相手が無い。値を読む相手も居ない(CAP_ORDER のループで
|
|
188
|
+
// special-case が先に立ち、renderAdapterTile へは行かない)——ここに載せているのは
|
|
189
|
+
// 能力の並びを1箇所で決めるため
|
|
191
190
|
"ハーネス連携": [],
|
|
192
191
|
};
|
|
193
192
|
/**
|
|
@@ -652,100 +651,6 @@ function sanitizeRegisteredNow(v) {
|
|
|
652
651
|
}
|
|
653
652
|
return any ? out : null;
|
|
654
653
|
}
|
|
655
|
-
// ---------------------------------------------------------------------------
|
|
656
|
-
// ハーネス連携(決まった時間に自動で動かす機能)— オン/オフの正本を通す篩
|
|
657
|
-
//
|
|
658
|
-
// 通り道は2本あり、**どちらも同じ関数を通す**:
|
|
659
|
-
// 1. ビルド時 … build.ts が `ハーネス連携/schedules.json` を読む
|
|
660
|
-
// 2. 画面側 … 保存の応答に添えられた entry でミラーを当て直す
|
|
661
|
-
// 写経して2枚にすると、片方だけ語彙を増やしたときに「静的では出ないものが
|
|
662
|
-
// 保存直後だけ出る」という一番気づきにくい食い違いになる(capabilities で踏んだ形)。
|
|
663
|
-
// ---------------------------------------------------------------------------
|
|
664
|
-
/** 動かす時間帯。**知らない値はキーごと落とす**(画面のラベルと送る値が決まらない) */
|
|
665
|
-
const HARNESS_SLOTS = ["morning", "noon", "evening"];
|
|
666
|
-
/** 毎週のときの曜日。同じく知らない値は落とす */
|
|
667
|
-
const HARNESS_WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
668
|
-
/**
|
|
669
|
-
* 時間帯のラベル。**「ごろ」を必ず含める**——実行はキューの都合で前後するので、
|
|
670
|
-
* 「朝9時」と書くと時刻の約束に読める。12時間表記で統一(発注書の例と一致)
|
|
671
|
-
*/
|
|
672
|
-
const HARNESS_SLOT_LABEL = {
|
|
673
|
-
morning: "朝9時ごろ",
|
|
674
|
-
noon: "昼1時ごろ",
|
|
675
|
-
evening: "夜9時ごろ",
|
|
676
|
-
};
|
|
677
|
-
/** 曜日のラベル(「毎週」の後ろに置く) */
|
|
678
|
-
const HARNESS_WEEKDAY_LABEL = {
|
|
679
|
-
mon: "月曜", tue: "火曜", wed: "水曜", thu: "木曜", fri: "金曜", sat: "土曜", sun: "日曜",
|
|
680
|
-
};
|
|
681
|
-
/**
|
|
682
|
-
* オンになっている機能の一覧を、画面が扱ってよい形へ畳む。**通らなかったものは黙って落とす。**
|
|
683
|
-
*
|
|
684
|
-
* - `harness`(owner/repo)と `skill` は必須。欠ける行は落とす(どの機能の話か決まらない)
|
|
685
|
-
* - `slot` / `weekday` は語彙外なら**キーごと落として行は残す**。プランと同じ
|
|
686
|
-
* 「時間帯の無い行は実行されない」状態が、画面にも「時間帯未設定」として現れるのが正しい
|
|
687
|
-
* - `ref` などの余りは落とす。**葉は文字列だけ**(配列やオブジェクトを通すと
|
|
688
|
-
* 画面に [object Object] が出る)
|
|
689
|
-
*
|
|
690
|
-
* 🔴 **クロージャを持たない純関数に保つこと。** クライアントJSへ toString() で
|
|
691
|
-
* 流し込んでいるので、外の変数を掴むと直列化した先で undefined を参照して壊れる
|
|
692
|
-
*(参照してよいのは同じく流し込んである HARNESS_SLOTS / HARNESS_WEEKDAYS だけ)。
|
|
693
|
-
*/
|
|
694
|
-
function sanitizeHarnessSchedules(v) {
|
|
695
|
-
if (!Array.isArray(v))
|
|
696
|
-
return [];
|
|
697
|
-
const out = [];
|
|
698
|
-
for (const r of v) {
|
|
699
|
-
if (r == null || typeof r !== "object" || Array.isArray(r))
|
|
700
|
-
continue;
|
|
701
|
-
if (out.length >= 200)
|
|
702
|
-
break;
|
|
703
|
-
const harness = r.harness;
|
|
704
|
-
const skill = r.skill;
|
|
705
|
-
if (typeof harness !== "string" || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(harness))
|
|
706
|
-
continue;
|
|
707
|
-
if (typeof skill !== "string" || !/^[A-Za-z0-9_-]+$/.test(skill))
|
|
708
|
-
continue;
|
|
709
|
-
const row = {
|
|
710
|
-
harness: harness,
|
|
711
|
-
skill: skill,
|
|
712
|
-
};
|
|
713
|
-
if (typeof r.slot === "string" && HARNESS_SLOTS.indexOf(r.slot) >= 0)
|
|
714
|
-
row.slot = r.slot;
|
|
715
|
-
if (typeof r.weekday === "string" && HARNESS_WEEKDAYS.indexOf(r.weekday) >= 0)
|
|
716
|
-
row.weekday = r.weekday;
|
|
717
|
-
out.push(row);
|
|
718
|
-
}
|
|
719
|
-
return out;
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* 棚(この案件で使う機能の候補)の一覧を、画面が扱ってよい形へ畳む。
|
|
723
|
-
*
|
|
724
|
-
* 🔴 **名前の篩は `sanitizeHarnessSchedules` に相乗りする(写経しない)。** 棚とオンは
|
|
725
|
-
* 別の状態だが、**同じ鍵(harness + skill)で突き合わせる**ので、通る名前の集合が
|
|
726
|
-
* 割れると「棚には置けるのにオンにできない機能」ができる。上限(200件)も向こうが持つ。
|
|
727
|
-
*
|
|
728
|
-
* 棚は**集合**なので、ここで重複を畳む(手編集や過去の並列書き込みで増えうる)。
|
|
729
|
-
* 描画順は名簿/カタログ順で決めるので、配列の並びそのものには意味を持たせない。
|
|
730
|
-
* **時間帯・曜日は落とす**——棚に周期を持たせると、intake の解決した値と食い違う
|
|
731
|
-
* 二重帳簿になる(棚は「いつ動くか」を決める場所ではない)。
|
|
732
|
-
*
|
|
733
|
-
* 🔴 **クロージャを持たない純関数に保つこと。** クライアントJSへ toString() で
|
|
734
|
-
* 流し込んでいる(参照してよいのは同じく流し込んである sanitizeHarnessSchedules だけ)。
|
|
735
|
-
*/
|
|
736
|
-
function sanitizeHarnessShelf(v) {
|
|
737
|
-
const rows = sanitizeHarnessSchedules(v);
|
|
738
|
-
const out = [];
|
|
739
|
-
const seen = {};
|
|
740
|
-
for (const r of rows) {
|
|
741
|
-
const key = r.harness + "\t" + r.skill;
|
|
742
|
-
if (seen[key])
|
|
743
|
-
continue;
|
|
744
|
-
seen[key] = true;
|
|
745
|
-
out.push({ harness: r.harness, skill: r.skill });
|
|
746
|
-
}
|
|
747
|
-
return out;
|
|
748
|
-
}
|
|
749
654
|
/**
|
|
750
655
|
* 接続状態ストアの語彙(サーバ側 connection-state-keys.mjs の REACHABLE_STATUSES /
|
|
751
656
|
* INGESTED_STATUSES と同じ。**勝手に増やさない・減らさない**)。
|
|
@@ -2262,6 +2167,10 @@ a:hover { text-decoration: underline; }
|
|
|
2262
2167
|
}
|
|
2263
2168
|
.site-title { font-size: 18px; font-weight: 700; color: var(--text); }
|
|
2264
2169
|
.site-title:hover { text-decoration: none; color: var(--accent); }
|
|
2170
|
+
/* ポータル配下で開いているとき(#379)、案件名はポータルの案件一覧(同一オリジンの /)へ
|
|
2171
|
+
戻るリンクになる。**文言は増やさない**(案件名そのものがリンク)ので、押せることは
|
|
2172
|
+
ホバーの下線で示す。丸(アイコン操作)とは別の口 */
|
|
2173
|
+
.site-title.site-title-portal:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
2265
2174
|
/* ---- 案件アイコン(#348)----
|
|
2266
2175
|
**ヘッダーの案件名の左に丸を置く。** ヘッダーは全タブ共通なので、
|
|
2267
2176
|
どのタブを開いていても同じ場所に居る。0.12.24 で Home 本文に置いた
|
|
@@ -2314,6 +2223,12 @@ button.site-icon[aria-expanded="true"] .site-icon-hover { opacity: 1; }
|
|
|
2314
2223
|
——押せない丸に指差しカーソルやリングを出すのが、画面の嘘のうちで一番たちが悪い */
|
|
2315
2224
|
button.site-icon { cursor: pointer; }
|
|
2316
2225
|
button.site-icon:hover, button.site-icon[aria-expanded="true"] { box-shadow: 0 0 0 2px var(--accent); }
|
|
2226
|
+
/* **保存中(#378)は丸の上に .busy-overlay(共通部品)が載る。** その間はホバーの「変更」「+」
|
|
2227
|
+
もリングも出さない(スピナーと重なって読めない・押せるように見せない)。
|
|
2228
|
+
ホバー規則と同じ強さなので、**この行はホバー規則より後に置く** */
|
|
2229
|
+
button.site-icon[aria-busy="true"] { cursor: wait; box-shadow: none; }
|
|
2230
|
+
button.site-icon[aria-busy="true"] .site-icon-hover,
|
|
2231
|
+
button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
|
|
2317
2232
|
.site-icon-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
|
|
2318
2233
|
/* 丸を押したときだけ出るメニュー。スロットを基準に浮かせる(ヘッダーは sticky なので
|
|
2319
2234
|
ページを送っても位置がずれない) */
|
|
@@ -2952,8 +2867,16 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
2952
2867
|
クラスは付いているのに効かず、見た目は「少し窮屈」なだけなので気づきにくい */
|
|
2953
2868
|
.modal.modal-wide { max-width: min(1040px, 92vw); }
|
|
2954
2869
|
.modal-overlay:has(.modal-wide) { padding: 16px; }
|
|
2955
|
-
|
|
2870
|
+
/* **タイルには <button> と <a> の2種類がある**(ハーネス連携だけがポータルへの外部リンク)。
|
|
2871
|
+
字の大きさと色をここで明示するのは、UA 既定が要素ごとに違うため——<button> は 13.33px の
|
|
2872
|
+
#000、<a> は body 継承の 15px にリンク色。何も書かないと**同じ一覧の中でタイルごとに
|
|
2873
|
+
字の大きさと濃さが変わる**。色は本文と同じ var(--text) に寄せる(#000 ではなく)*/
|
|
2874
|
+
.ds-tile { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; cursor: pointer; background: var(--card, #fff); color: var(--text); border: 1px solid var(--line, #e5e7eb); border-radius: 12px; padding: 14px 16px; font-size: 13.5px; line-height: 1.5; }
|
|
2956
2875
|
.ds-tile:hover { border-color: #c7cdd6; }
|
|
2876
|
+
/* リンクのタイルは、リンク既定の色と下線を打ち消す(ボタンのタイルと見た目を割らない)。
|
|
2877
|
+
:hover の下線も明示して消す——上の a:hover が下線を戻すため */
|
|
2878
|
+
a.ds-tile { color: inherit; text-decoration: none; }
|
|
2879
|
+
a.ds-tile:hover { text-decoration: none; }
|
|
2957
2880
|
.ds-tile-body { flex: 1; min-width: 0; }
|
|
2958
2881
|
.ds-tile-name { font-weight: 700; }
|
|
2959
2882
|
/* ロゴはタイルの左に独立して置き、名前の高さに縛られないようにする。
|
|
@@ -2980,52 +2903,6 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
2980
2903
|
.ds-tile-sub { font-size: 12.5px; margin-top: 2px; }
|
|
2981
2904
|
.ds-tile-chev { color: var(--muted); flex: 0 0 auto; }
|
|
2982
2905
|
.ds-tile-note { font-size: 12.5px; color: #a15c00; margin-top: 2px; }
|
|
2983
|
-
/* 「オンにしたのに配信先が無い」(ハーネス連携)。**必ず .ds-tile-note より後ろに置く**
|
|
2984
|
-
——同じ詳細度なので、前に書くと後ろの #a15c00 に負けて色が変わらない。
|
|
2985
|
-
琥珀(設定の不足)より強い赤にするのは、この状態だと**動いているのに誰にも届かない**ため */
|
|
2986
|
-
.ds-tile-alert { color: #b91c1c; }
|
|
2987
|
-
/* ---- ハーネス連携のモーダル(機能ごとの1行)----
|
|
2988
|
-
枠は .ds-card に揃える(同じモーダルの中で行の見え方を割らない)。
|
|
2989
|
-
縦組みにするのは、1行に「名前+周期」「説明」「操作」の3段が入るため */
|
|
2990
|
-
.ds-harness-list { display: flex; flex-direction: column; gap: 10px; }
|
|
2991
|
-
.ds-harness-row {
|
|
2992
|
-
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
2993
|
-
padding: 14px 16px; box-shadow: var(--shadow);
|
|
2994
|
-
display: flex; flex-direction: column; gap: 4px;
|
|
2995
|
-
}
|
|
2996
|
-
.ds-harness-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; }
|
|
2997
|
-
.ds-harness-name { font-weight: 700; }
|
|
2998
|
-
.ds-harness-period { font-size: 12.5px; color: var(--muted); }
|
|
2999
|
-
.ds-harness-summary { font-size: 12.5px; color: var(--muted); }
|
|
3000
|
-
/* 時間帯・曜日の選択。トグルと同じ行に並べる(押した直後に調整できるのが要点) */
|
|
3001
|
-
.ds-harness-selects { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
3002
|
-
.ds-harness-selects select { font-size: 12px; padding: 3px 6px; border: 1px solid var(--border); border-radius: 6px; background: #fff; color: var(--text); }
|
|
3003
|
-
.ds-harness-selects select:disabled { opacity: .6; cursor: wait; }
|
|
3004
|
-
/* 一覧の代わりに出す1行(読み込み中・0件・読み込めなかった)。**3つを見分けられる色にする** */
|
|
3005
|
-
.ds-harness-state { font-size: 12.5px; color: var(--muted); margin: 4px 0 10px; }
|
|
3006
|
-
.ds-harness-state.is-error { color: #b42318; }
|
|
3007
|
-
/* 一部の提供元から読めなかったとき。**行は出したうえで**「並んでいないものがある」と断る */
|
|
3008
|
-
.ds-harness-warn { font-size: 12.5px; color: #a15c00; margin: 4px 0 10px; }
|
|
3009
|
-
/* ---- 棚ビュー / ピッカーのセクション(部署 → ハーネス)----
|
|
3010
|
-
見出しの階層を**大きさではなく余白で**作る。モーダルの中で h3/h2 相当の字を並べると、
|
|
3011
|
-
モーダルの見出し(機能名)と競り合って、どれが画面の主題か読めなくなる */
|
|
3012
|
-
.ds-harness-dep { margin: 0 0 18px; }
|
|
3013
|
-
.ds-harness-dep-title { font-size: 13px; font-weight: 700; color: var(--muted); margin: 0 0 8px; letter-spacing: .04em; }
|
|
3014
|
-
.ds-harness-prov-title { font-size: 13px; font-weight: 700; margin: 12px 0 6px; }
|
|
3015
|
-
.ds-harness-dep-body > .ds-harness-prov-title:first-child { margin-top: 0; }
|
|
3016
|
-
/* 提供元ごとの「いま読み込めませんでした」(一覧全体の断りとは別。どの提供元の話かが分かる) */
|
|
3017
|
-
.ds-harness-subnote { font-size: 12.5px; color: #a15c00; margin: 0 0 6px; }
|
|
3018
|
-
/* 「棚に無いのにオンになっている」行の注記。**行の中に置く**——一覧の上でまとめて
|
|
3019
|
-
断ると、どの行のことなのかが読めない */
|
|
3020
|
-
.ds-harness-note { font-size: 12.5px; color: #a15c00; }
|
|
3021
|
-
/* ピッカーで「これはもう動いている」ことを示す印。棚の操作と混ぜないよう小さく添える */
|
|
3022
|
-
.ds-harness-onmark {
|
|
3023
|
-
font-size: 11.5px; color: var(--accent); border: 1px solid var(--accent);
|
|
3024
|
-
border-radius: 999px; padding: 0 8px; flex: 0 0 auto;
|
|
3025
|
-
}
|
|
3026
|
-
/* ビューを移る導線(機能をえらぶ/戻る)。行の操作と同じ高さに並べない(別の階層の操作) */
|
|
3027
|
-
.ds-harness-actions { display: flex; margin: 12px 0 4px; }
|
|
3028
|
-
.ds-harness-pick-state { font-size: 12.5px; color: var(--muted); margin: 4px 0 10px; }
|
|
3029
2906
|
/* ---- 可変長の文字列は必ず折り返す(0.4.71) ----
|
|
3030
2907
|
実機の受け入れテストで、状態の文言がタイル幅を超えて溢れた(スシローのデザインタイル)。
|
|
3031
2908
|
ここに出るのは engine の detail・チェックの直し方・チャンネル名・ファイルパスなど
|
|
@@ -3039,10 +2916,7 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
3039
2916
|
.ds-check-label, .ds-check-action, .ds-check-detail,
|
|
3040
2917
|
.ds-pipe-label, .ds-pipe-todo,
|
|
3041
2918
|
.ds-empty, .sec-how, .modal-desc, .ds-toggle-msg,
|
|
3042
|
-
.ds-drive-note, .ds-inactive-note, .ds-legacy-chip, .ds-keys-code, .ds-gate-help
|
|
3043
|
-
/* ハーネス連携の行。機能の表示名・説明・周期の文はどれも提供側が決めるので長さが読めない */
|
|
3044
|
-
.ds-harness-name, .ds-harness-period, .ds-harness-summary, .ds-harness-state, .ds-harness-warn,
|
|
3045
|
-
.ds-harness-dep-title, .ds-harness-prov-title, .ds-harness-subnote, .ds-harness-note, .ds-harness-pick-state {
|
|
2919
|
+
.ds-drive-note, .ds-inactive-note, .ds-legacy-chip, .ds-keys-code, .ds-gate-help {
|
|
3046
2920
|
min-width: 0; white-space: normal; overflow-wrap: anywhere;
|
|
3047
2921
|
}
|
|
3048
2922
|
.ds-toggle { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
|
|
@@ -3331,9 +3205,8 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
|
|
|
3331
3205
|
/* 貼ったURLを1行で出すので、はみ出させない(箱は420px・モバイルは実質335px) */
|
|
3332
3206
|
.confirm-line { font-size: 14px; color: var(--text); line-height: 1.75; margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
3333
3207
|
.confirm-line:first-child { font-weight: 700; }
|
|
3334
|
-
/*
|
|
3335
|
-
.confirm-line
|
|
3336
|
-
選択欄そのものは行側の .ds-harness-selects を流用する */
|
|
3208
|
+
/* 確認の中に挿す本文(openConfirm の body オプション)。箱の gap は
|
|
3209
|
+
.confirm-line の間隔なので、入れ子にすると効かない——同じ間隔をここでも持つ */
|
|
3337
3210
|
.confirm-body { display: flex; flex-direction: column; gap: 10px; }
|
|
3338
3211
|
.confirm-body-label { font-size: 13px; color: var(--muted); }
|
|
3339
3212
|
.modal-ok { color: #047857; font-weight: 600; }
|
|
@@ -3392,6 +3265,40 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
|
|
|
3392
3265
|
.body th, .body td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
|
|
3393
3266
|
.body-loading { color: var(--muted); font-size: 13px; padding: 8px 2px; }
|
|
3394
3267
|
|
|
3268
|
+
/* 一行の結果表示(トースト)。#348 の案件アイコンで使う。
|
|
3269
|
+
保存・解除は**その場で丸が変わる**ので、結果の説明は要らない——
|
|
3270
|
+
「保存しました」を数秒だけ出して消す。画面の下に浮かせるのは、
|
|
3271
|
+
ヘッダーの中に結果の行を抱えると、押した直後だけヘッダーが伸びるため */
|
|
3272
|
+
.toast {
|
|
3273
|
+
position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
|
|
3274
|
+
background: #111827; color: #fff; padding: 8px 14px; border-radius: 999px;
|
|
3275
|
+
font-size: 13px; opacity: 0; pointer-events: none; transition: opacity .15s; z-index: 40;
|
|
3276
|
+
}
|
|
3277
|
+
.toast-show { opacity: 1; }
|
|
3278
|
+
.toast-error { background: #b91c1c; }
|
|
3279
|
+
|
|
3280
|
+
/* 反映待ちのオーバーレイ+スピナー(#378・共通部品。JS の beginOverlaySpinner が載せる)。
|
|
3281
|
+
押した瞬間から結果が返るまで、**押した場所そのものの上に**半透明の白(60%)と
|
|
3282
|
+
ぐるぐるを重ねる——押した反応が無いように見えて押し直す(二重送信)のを止める。
|
|
3283
|
+
載せ先の大きさは変えない(absolute で内側に重ね、角丸は載せ先から継ぐ)。
|
|
3284
|
+
いまは案件アイコンの丸にだけ使う。操作タブの保存にも同じ部品を使える */
|
|
3285
|
+
.busy-overlay {
|
|
3286
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: inherit;
|
|
3287
|
+
display: flex; align-items: center; justify-content: center;
|
|
3288
|
+
background: rgba(255, 255, 255, .6); cursor: wait; z-index: 2;
|
|
3289
|
+
}
|
|
3290
|
+
.busy-spinner {
|
|
3291
|
+
width: 18px; height: 18px; border-radius: 50%;
|
|
3292
|
+
border: 2px solid var(--accent-weak); border-top-color: var(--accent);
|
|
3293
|
+
animation: busy-spin .8s linear infinite;
|
|
3294
|
+
}
|
|
3295
|
+
@keyframes busy-spin { to { transform: rotate(360deg); } }
|
|
3296
|
+
/* 読み上げ用の語(「保存中…」)。目には見せない(文言を増やさない) */
|
|
3297
|
+
.busy-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
3298
|
+
/* 動きを減らす設定でも止めはしない——止めると「保存中」と「固まった」の区別が付かない。
|
|
3299
|
+
回転を半分の速さに落とすだけ */
|
|
3300
|
+
@media (prefers-reduced-motion: reduce) { .busy-spinner { animation-duration: 1.6s; } }
|
|
3301
|
+
|
|
3395
3302
|
.site-footer { max-width: 960px; margin: 0 auto; padding: 0 20px 40px; color: var(--muted); font-size: 12px; }
|
|
3396
3303
|
|
|
3397
3304
|
@media (max-width: 600px) {
|
|
@@ -3433,6 +3340,11 @@ const CLIENT_JS = `
|
|
|
3433
3340
|
* intake の op を1回呼ぶ。POST は JSON の body に名乗り券(あれば)を actor として足す。
|
|
3434
3341
|
* GET(opts.method === "GET" のとき)はカスタムヘッダも body も付けない(preflight を増やさないため・#198)。
|
|
3435
3342
|
* 戻り値は fetch の Promise そのもの(呼び出し側の .then / await / res.json() はそのまま使える)。
|
|
3343
|
+
*
|
|
3344
|
+
* ⚠ **いま GET で呼ぶ op は1つも無い**(唯一の呼び手だった機能一覧の取得は #369 で廃止)。
|
|
3345
|
+
* それでも分岐を残すのは、**GET の op を足すときの契約をここ1箇所に置いておくため**——
|
|
3346
|
+
* 券もカスタムヘッダも載せない、という #198 の約束が消えると、次に足した GET が
|
|
3347
|
+
* プリフライトで落ちる(本番のブラウザでだけ・CloudWatch には何も出ない形で)。
|
|
3436
3348
|
*/
|
|
3437
3349
|
function callIntake(op, body, opts) {
|
|
3438
3350
|
var intake = SITE.intake;
|
|
@@ -3494,14 +3406,6 @@ var REGISTERED_DEV_TYPES = ${JSON.stringify(REGISTERED_DEV_TYPES)};
|
|
|
3494
3406
|
var UNSETTLED_TTL_MS = ${JSON.stringify(UNSETTLED_TTL_MS)};
|
|
3495
3407
|
var UNSETTLED_POLL_MS = ${JSON.stringify(UNSETTLED_POLL_MS)};
|
|
3496
3408
|
${sanitizeRegisteredNow.toString()}
|
|
3497
|
-
// ハーネス連携(決まった時間に動かす機能)の語彙とラベル。**判定の実体はビルド時の定義**
|
|
3498
|
-
// ——画面側に書き写すと、許可リストが2枚になって語彙の追加漏れが静かに消える
|
|
3499
|
-
var HARNESS_SLOTS = ${JSON.stringify(HARNESS_SLOTS)};
|
|
3500
|
-
var HARNESS_WEEKDAYS = ${JSON.stringify(HARNESS_WEEKDAYS)};
|
|
3501
|
-
var HARNESS_SLOT_LABEL = ${JSON.stringify(HARNESS_SLOT_LABEL)};
|
|
3502
|
-
var HARNESS_WEEKDAY_LABEL = ${JSON.stringify(HARNESS_WEEKDAY_LABEL)};
|
|
3503
|
-
${sanitizeHarnessSchedules.toString()}
|
|
3504
|
-
${sanitizeHarnessShelf.toString()}
|
|
3505
3409
|
${chatRowsToAdd.toString()}
|
|
3506
3410
|
${gchatRowsToAdd.toString()}
|
|
3507
3411
|
${devRowsToAdd.toString()}
|
|
@@ -4386,26 +4290,101 @@ ${deriveCapTile.toString()}
|
|
|
4386
4290
|
// ヘッダーは全タブ共通なので、どのタブを開いていても同じ場所に居る。
|
|
4387
4291
|
// 押せる口はメニューの中に畳む——常時見えるボタンは、たまにしか使わない操作には重い。
|
|
4388
4292
|
//
|
|
4389
|
-
// 保存はビルドを待たずに画面へ反映する(mirror)。恒久化は次のビルドで
|
|
4390
|
-
// site-data ごと焼き直されるので、mirror はこのタブが開いているあいだだけ。
|
|
4391
4293
|
// 上限はサーバ側(logic.mjs の PROJECT_ICON_MAX_BYTES)と同じ値。画面の門は
|
|
4392
4294
|
// 「送る前に断る」ためのもので、正本はサーバ側
|
|
4393
4295
|
var PROJECT_ICON_MAX_BYTES = 1024 * 1024;
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4296
|
+
|
|
4297
|
+
/* ---- 反映待ち(pending)----------------------------------------------------
|
|
4298
|
+
* 静的サイトなので、保存の commit が入っても**次のビルドまで SiteData は古いまま**。
|
|
4299
|
+
* 何もしないと「入れたのにまだ頭文字のまま」「外したのにまだ絵が出る」と見える。
|
|
4300
|
+
* 0.12.26 まではその待ち時間を「数分後に反映されます」の文で説明していたが、
|
|
4301
|
+
* オーナー実機で却下された(待たされている事実は説明されても消えない)。
|
|
4302
|
+
*
|
|
4303
|
+
* そこで**削除のトゥームストーンと同じ型**で、保存できたことをこのブラウザに覚える。
|
|
4304
|
+
* 覚えるのは押した本人のブラウザだけで、他の端末・他人には次のビルドで届く。
|
|
4305
|
+
*
|
|
4306
|
+
* ⚠ **キーは案件キー**(リポジトリのスラグではない)。同じオリジンに配られる
|
|
4307
|
+
* ポータルの一覧カードが、同じキーでこの仮画像を読んで先に出すため。
|
|
4308
|
+
*
|
|
4309
|
+
* ⚠ **いつまでも仮画像を出し続けない。** 本物が焼き直された後も残していると、
|
|
4310
|
+
* 別の画像に差し替えたつもりの人に、いつまでも古い仮画像が見え続ける
|
|
4311
|
+
* (破棄の条件は iconPendingStale=トゥームストーンと同じ判定)。
|
|
4312
|
+
*
|
|
4313
|
+
* ⚠ 反映待ちの入れ物に p という変数名を使わないこと。fleet-passthrough の検査は
|
|
4314
|
+
* その名前のドット参照を「艦隊データの欄」として走査するので、
|
|
4315
|
+
* ここで使うと build.ts の受け渡し漏れに化けて赤くなる
|
|
4316
|
+
*/
|
|
4317
|
+
function iconPendingKey() {
|
|
4318
|
+
return SITE.intake ? "cortex-icon-pending:" + SITE.intake.projectKey : "";
|
|
4319
|
+
}
|
|
4320
|
+
/** 覚えている反映待ち(無い・壊れている・形が違うなら null) */
|
|
4321
|
+
function readIconPending() {
|
|
4322
|
+
var key = iconPendingKey();
|
|
4323
|
+
if (!key) return null;
|
|
4324
|
+
try {
|
|
4325
|
+
var raw = localStorage.getItem(key);
|
|
4326
|
+
if (!raw) return null;
|
|
4327
|
+
var pend = JSON.parse(raw);
|
|
4328
|
+
if (!pend || typeof pend !== "object") return null;
|
|
4329
|
+
// 形の版と案件キーは**ポータルの読み側(usePendingProjectIcon)と同じ検査**にする。
|
|
4330
|
+
// 将来 v:2 を出したとき、古い画面が新しい形を読み違えないため
|
|
4331
|
+
if (pend.v !== 1 || pend.projectKey !== SITE.intake.projectKey) return null;
|
|
4332
|
+
if (pend.kind === "remove") return pend;
|
|
4333
|
+
// set は仮画像そのものが要る。**data:image/ 以外は載せない**(外部URLを持ち込まない・#110。
|
|
4334
|
+
// ポータル側と同じ検査)
|
|
4335
|
+
if (pend.kind === "set" && typeof pend.dataUrl === "string" && pend.dataUrl.indexOf("data:image/") === 0) return pend;
|
|
4336
|
+
return null;
|
|
4337
|
+
} catch (e) { return null; }
|
|
4338
|
+
}
|
|
4339
|
+
function writeIconPending(entry) {
|
|
4340
|
+
var key = iconPendingKey();
|
|
4341
|
+
if (!key) return;
|
|
4342
|
+
// 1MB の画像を data: にすると 1.4MB 前後。localStorage の枠を超えることがあるので、
|
|
4343
|
+
// 失敗しても操作は成功したまま進める(次のビルドで本物が出る)
|
|
4344
|
+
try { localStorage.setItem(key, JSON.stringify(entry)); } catch (e) {}
|
|
4345
|
+
}
|
|
4346
|
+
function clearIconPending() {
|
|
4347
|
+
var key = iconPendingKey();
|
|
4348
|
+
if (!key) return;
|
|
4349
|
+
try { localStorage.removeItem(key); } catch (e) {}
|
|
4350
|
+
}
|
|
4351
|
+
/**
|
|
4352
|
+
* この反映待ちはもう用済みか(=保存より後のビルドが来ている)。
|
|
4353
|
+
* **判定はトゥームストーン(tombstoneStale)と同じ**——同じ性質の待ちなので、
|
|
4354
|
+
* 破棄の条件を2つ持つと、片方だけ直したときに挙動が割れる。
|
|
4355
|
+
*/
|
|
4356
|
+
function iconPendingStale(pend, nowMs) {
|
|
4357
|
+
if (!pend || isNaN(nowMs)) return false;
|
|
4358
|
+
var byAt = typeof pend.at === "number" && nowMs > pend.at + TOMBSTONE_MARGIN_MS;
|
|
4359
|
+
var gen0 = siteTimeMs(pend.gen0);
|
|
4360
|
+
if (isNaN(gen0)) return byAt;
|
|
4361
|
+
return byAt && nowMs > gen0 + TOMBSTONE_MARGIN_MS;
|
|
4362
|
+
}
|
|
4363
|
+
/** いま覚えている反映待ちを記録する(保存・解除に共通の形) */
|
|
4364
|
+
function rememberIconPending(kind, dataUrl) {
|
|
4365
|
+
var entry = { v: 1, projectKey: SITE.intake ? SITE.intake.projectKey : "", kind: kind, at: Date.now(), gen0: SITE.generatedAt || "" };
|
|
4366
|
+
if (dataUrl) entry.dataUrl = dataUrl;
|
|
4367
|
+
writeIconPending(entry);
|
|
4368
|
+
}
|
|
4369
|
+
/** 選んだ画像を data: にする。読めなければ空文字(仮画像は出さず、本物のビルドを待つ) */
|
|
4370
|
+
function fileToDataUrl(file) {
|
|
4371
|
+
return new Promise(function (resolve) {
|
|
4372
|
+
try {
|
|
4373
|
+
var r = new FileReader();
|
|
4374
|
+
r.onload = function () { resolve(typeof r.result === "string" ? r.result : ""); };
|
|
4375
|
+
r.onerror = function () { resolve(""); };
|
|
4376
|
+
r.readAsDataURL(file);
|
|
4377
|
+
} catch (e) { resolve(""); }
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
/** いま画面に出すべきアイコン(無ければ null)。**反映待ちがビルド時の値に優先する** */
|
|
4402
4382
|
function projectIconCurrent() {
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
var p = SITE.projectIcon;
|
|
4383
|
+
var pend = readIconPending();
|
|
4384
|
+
if (pend) return pend.kind === "remove" ? null : { file: "", src: pend.dataUrl };
|
|
4385
|
+
var q = SITE.projectIcon;
|
|
4407
4386
|
// ビルドが同梱した画像はサイト内の相対パス。**外部URLは載せない**
|
|
4408
|
-
return
|
|
4387
|
+
return q ? { file: q.file, src: q.file } : null;
|
|
4409
4388
|
}
|
|
4410
4389
|
|
|
4411
4390
|
/**
|
|
@@ -4421,6 +4400,30 @@ ${deriveCapTile.toString()}
|
|
|
4421
4400
|
return e === "png" || e === "jpg" || e === "webp" ? e : null;
|
|
4422
4401
|
}
|
|
4423
4402
|
|
|
4403
|
+
// いま出ているトーストを消すタイマ。**1本だけ持つ**——連続で出したときに
|
|
4404
|
+
// 古いタイマが残っていると、2つ目が出た 0.2 秒後に消える
|
|
4405
|
+
var toastTimer = null;
|
|
4406
|
+
/**
|
|
4407
|
+
* 画面の下に1行だけ出して、3秒で消す。
|
|
4408
|
+
*
|
|
4409
|
+
* **結果の行をヘッダーの中に抱えない。** 押した直後だけヘッダーが伸びると、
|
|
4410
|
+
* その下の本文が飛ぶ。丸そのものが即座に変わるので、説明は一言でよい。
|
|
4411
|
+
*/
|
|
4412
|
+
function showToast(text, isError) {
|
|
4413
|
+
var t = document.getElementById("toast");
|
|
4414
|
+
if (!t) {
|
|
4415
|
+
t = el("div", { class: "toast", id: "toast", role: "status", "aria-live": "polite" });
|
|
4416
|
+
document.body.appendChild(t);
|
|
4417
|
+
}
|
|
4418
|
+
t.textContent = text;
|
|
4419
|
+
t.className = "toast" + (isError ? " toast-error" : "") + " toast-show";
|
|
4420
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
4421
|
+
toastTimer = setTimeout(function () {
|
|
4422
|
+
toastTimer = null;
|
|
4423
|
+
t.className = "toast" + (isError ? " toast-error" : "");
|
|
4424
|
+
}, 3000);
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4424
4427
|
/** callIntake の戻り(fetch の Promise)を { ok, j } に畳む(submitIntake と同じ形) */
|
|
4425
4428
|
function intakeJson(req) {
|
|
4426
4429
|
return req.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, j: j }; }); });
|
|
@@ -4433,6 +4436,27 @@ ${deriveCapTile.toString()}
|
|
|
4433
4436
|
// document に付ける開閉のリスナーは1組だけ。描き直しのたびに足すと Escape 1回で
|
|
4434
4437
|
// 同じ処理が何度も走る(見た目は同じなので気づけない)
|
|
4435
4438
|
var siteIconGlobalBound = false;
|
|
4439
|
+
// 保存中か(画像を選ぶ/変える/外す の3段が走っている間)。**DOM ではなくこの変数が正本**
|
|
4440
|
+
// (#378)。描き直しで丸が作り直されても、renderSiteIcon がこれを見てオーバーレイを付け直す。
|
|
4441
|
+
// setProjectIcon / removeProjectIcon の入口で見て、走っている間の2回目を捨てる(二重送信防止)
|
|
4442
|
+
var siteIconSaving = false;
|
|
4443
|
+
// いま丸に載っているオーバーレイの外し方(無ければ null)
|
|
4444
|
+
var siteIconBusyEnd = null;
|
|
4445
|
+
|
|
4446
|
+
/**
|
|
4447
|
+
* 丸の保存中の見た目を付ける/外す。丸は disabled にして押せなくする
|
|
4448
|
+
* (メニューの開閉もここで止まる。丸のクリックは siteIconSaving でも見る)
|
|
4449
|
+
*/
|
|
4450
|
+
function setSiteIconBusy(circle, on) {
|
|
4451
|
+
if (siteIconBusyEnd) { siteIconBusyEnd(); siteIconBusyEnd = null; }
|
|
4452
|
+
if (!circle) return;
|
|
4453
|
+
if (on) {
|
|
4454
|
+
siteIconBusyEnd = beginOverlaySpinner(circle, "保存中…");
|
|
4455
|
+
circle.disabled = true;
|
|
4456
|
+
} else {
|
|
4457
|
+
circle.disabled = false;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4436
4460
|
|
|
4437
4461
|
/**
|
|
4438
4462
|
* 案件名の頭文字1字。**ポータルの ProjectIcon.tsx(initialOf)と同じ取り方**にする
|
|
@@ -4470,12 +4494,15 @@ ${deriveCapTile.toString()}
|
|
|
4470
4494
|
* **押せる形にするのは受け口(intake)がある案件だけ。** 無ければ span にして
|
|
4471
4495
|
* メニューもファイル選択も付けない——押して何も起きない口は「壊れている」と読まれる。
|
|
4472
4496
|
*
|
|
4473
|
-
* @param status 前の描画から引き継ぐ結果の行(省略時は新しく作る)
|
|
4474
4497
|
*/
|
|
4475
|
-
function renderSiteIcon(
|
|
4498
|
+
function renderSiteIcon() {
|
|
4476
4499
|
var slot = document.getElementById("site-icon-slot");
|
|
4477
4500
|
if (!slot) return;
|
|
4478
4501
|
var intake = SITE.intake;
|
|
4502
|
+
// **本物が追いついたら仮画像を捨てる。** 残したままだと、次に差し替えた人にも
|
|
4503
|
+
// 古い仮画像が見え続ける(削除のトゥームストーンと同じ捨て方)
|
|
4504
|
+
var pend = readIconPending();
|
|
4505
|
+
if (pend && iconPendingStale(pend, siteTimeMs(SITE.generatedAt))) clearIconPending();
|
|
4479
4506
|
var cur = projectIconCurrent();
|
|
4480
4507
|
slot.textContent = "";
|
|
4481
4508
|
|
|
@@ -4497,7 +4524,7 @@ ${deriveCapTile.toString()}
|
|
|
4497
4524
|
// **設定済みは画像だけ。** 頭文字も「+」も重ねない(選んだロゴが読めなくなる)
|
|
4498
4525
|
var img = el("img", { class: "site-icon-img", src: cur.src, alt: "" });
|
|
4499
4526
|
// **onerror 属性で書かない(CSP でインラインハンドラは動かない)。**
|
|
4500
|
-
//
|
|
4527
|
+
// 仮画像の data: が配信先の img-src で拒まれることがあるので、そのときは頭文字に倒す
|
|
4501
4528
|
img.addEventListener("error", function () {
|
|
4502
4529
|
if (img.parentNode) img.parentNode.replaceChild(siteIconInitialEl(), img);
|
|
4503
4530
|
});
|
|
@@ -4512,26 +4539,30 @@ ${deriveCapTile.toString()}
|
|
|
4512
4539
|
}
|
|
4513
4540
|
slot.appendChild(circle);
|
|
4514
4541
|
if (!intake) return;
|
|
4542
|
+
// 保存中に描き直されても、オーバーレイと押せない状態を引き継ぐ(正本は siteIconSaving)。
|
|
4543
|
+
// いまの呼び出し経路(起動時1回・ui.busy(false) の後の redraw)では保存中に来ないので
|
|
4544
|
+
// 防御。将来タブ切り替え等でヘッダーを描き直すようになっても、ぐるぐるが消えない
|
|
4545
|
+
if (siteIconSaving) setSiteIconBusy(circle, true);
|
|
4546
|
+
|
|
4547
|
+
circle.addEventListener("click", function () {
|
|
4548
|
+
// 保存中は丸が disabled なので click 自体が飛ばない(ブラウザの既定)。これは防御
|
|
4549
|
+
if (siteIconSaving) return;
|
|
4550
|
+
setSiteIconMenu(!siteIconMenuOpen);
|
|
4551
|
+
});
|
|
4515
4552
|
|
|
4516
|
-
circle.addEventListener("click", function () { setSiteIconMenu(!siteIconMenuOpen); });
|
|
4517
|
-
|
|
4518
|
-
// **status の要素は描き直しをまたいで持ち回る。** 描き直したあとに
|
|
4519
|
-
// 「✓ 保存しました」と保存先のリンクを足すので、入れ替わると書いた先が画面から外れる
|
|
4520
|
-
var st = status || el("div", { class: "site-icon-status", "aria-live": "polite" });
|
|
4521
4553
|
var menu = el("div", { class: "site-icon-menu", role: "menu" });
|
|
4522
4554
|
if (!siteIconMenuOpen) menu.setAttribute("hidden", "");
|
|
4523
4555
|
|
|
4524
|
-
function setStatus(msg, isError) {
|
|
4525
|
-
st.className = "site-icon-status" + (isError ? " site-icon-status-error" : "");
|
|
4526
|
-
st.textContent = msg || "";
|
|
4527
|
-
}
|
|
4528
4556
|
var ui = {
|
|
4529
|
-
status: st,
|
|
4530
|
-
setStatus: setStatus,
|
|
4531
4557
|
// 描き直すと押していたボタンが DOM から消えてフォーカスが body に落ちるので、
|
|
4532
4558
|
// 丸へ戻す(キーボードの人が位置を失わない。Escape は document 側なので効く)
|
|
4533
4559
|
redraw: function () {
|
|
4534
|
-
renderSiteIcon(
|
|
4560
|
+
renderSiteIcon();
|
|
4561
|
+
ui.refocus();
|
|
4562
|
+
},
|
|
4563
|
+
// 丸へフォーカスを戻す。失敗のあとも呼ぶ——押していた項目は disabled にした瞬間に
|
|
4564
|
+
// フォーカスを失って body に落ちるので、戻さないとキーボードの人がやり直す位置を失う
|
|
4565
|
+
refocus: function () {
|
|
4535
4566
|
var s = document.getElementById("site-icon-slot");
|
|
4536
4567
|
var c = s && s.querySelector("button.site-icon");
|
|
4537
4568
|
if (c && typeof c.focus === "function") c.focus();
|
|
@@ -4539,7 +4570,15 @@ ${deriveCapTile.toString()}
|
|
|
4539
4570
|
input: null,
|
|
4540
4571
|
buttons: [],
|
|
4541
4572
|
};
|
|
4542
|
-
|
|
4573
|
+
// 送信中は全部を止める(#378: 丸にオーバーレイ+スピナー・丸とメニューを押せなくする)。
|
|
4574
|
+
// **元から押せない項目は押せないまま戻す**——
|
|
4575
|
+
// 未設定の「画像を外す」が、失敗して戻ったときだけ押せるようになるのを防ぐ
|
|
4576
|
+
ui.busy = function (flag) {
|
|
4577
|
+
siteIconSaving = !!flag;
|
|
4578
|
+
ui.buttons.forEach(function (b) { b.disabled = flag ? true : !!b.lockedOff; });
|
|
4579
|
+
var s = document.getElementById("site-icon-slot");
|
|
4580
|
+
setSiteIconBusy(s && s.querySelector("button.site-icon"), siteIconSaving);
|
|
4581
|
+
};
|
|
4543
4582
|
|
|
4544
4583
|
var input = el("input", { type: "file", accept: "image/png,image/jpeg,image/webp", hidden: "" });
|
|
4545
4584
|
ui.input = input;
|
|
@@ -4550,18 +4589,21 @@ ${deriveCapTile.toString()}
|
|
|
4550
4589
|
try { input.value = ""; } catch (e) {}
|
|
4551
4590
|
if (f) setProjectIcon(f, ui);
|
|
4552
4591
|
});
|
|
4592
|
+
// **項目は常にこの2つ・この順。** 状態で数が変わると、押す場所が毎回動く
|
|
4593
|
+
// (未設定のときは「外す」を押せなくするだけで、消しはしない)
|
|
4553
4594
|
var choose = el("button", { class: "site-icon-choose", type: "button", role: "menuitem", text: cur ? "画像を変える" : "画像を選ぶ" });
|
|
4554
4595
|
choose.addEventListener("click", function () { input.click(); });
|
|
4555
4596
|
ui.buttons.push(choose);
|
|
4556
4597
|
menu.appendChild(choose);
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
remove.
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
}
|
|
4598
|
+
var remove = el("button", { class: "site-icon-remove", type: "button", role: "menuitem", text: "画像を外す" });
|
|
4599
|
+
if (!cur) {
|
|
4600
|
+
remove.disabled = true;
|
|
4601
|
+
remove.lockedOff = true;
|
|
4602
|
+
}
|
|
4603
|
+
remove.addEventListener("click", function () { removeProjectIcon(ui); });
|
|
4604
|
+
ui.buttons.push(remove);
|
|
4605
|
+
menu.appendChild(remove);
|
|
4563
4606
|
menu.appendChild(input);
|
|
4564
|
-
menu.appendChild(st);
|
|
4565
4607
|
slot.appendChild(menu);
|
|
4566
4608
|
|
|
4567
4609
|
if (!siteIconGlobalBound) {
|
|
@@ -4580,16 +4622,6 @@ ${deriveCapTile.toString()}
|
|
|
4580
4622
|
}
|
|
4581
4623
|
}
|
|
4582
4624
|
|
|
4583
|
-
/** 保存先(GitHub)の1行。submitIntake の modal-paths と同じ組み立て */
|
|
4584
|
-
function projectIconPathBox(repo, p) {
|
|
4585
|
-
var br = SITE.branch || "main";
|
|
4586
|
-
var href = "https://github.com/" + repo + "/blob/" + encodeURIComponent(br) + "/" + p.split("/").map(encodeURIComponent).join("/");
|
|
4587
|
-
return el("div", { class: "modal-paths" }, [
|
|
4588
|
-
el("div", { class: "modal-paths-label", text: "保存先(GitHub):" }),
|
|
4589
|
-
el("a", { class: "modal-path", href: href, target: "_blank", rel: "noopener", text: p }),
|
|
4590
|
-
]);
|
|
4591
|
-
}
|
|
4592
|
-
|
|
4593
4625
|
/**
|
|
4594
4626
|
* 画像を1枚選んで保存する。
|
|
4595
4627
|
*
|
|
@@ -4597,7 +4629,9 @@ ${deriveCapTile.toString()}
|
|
|
4597
4629
|
* ものを送ると、1MB を上げきってから「大きすぎます」と言うことになる
|
|
4598
4630
|
*/
|
|
4599
4631
|
async function setProjectIcon(file, ui) {
|
|
4600
|
-
|
|
4632
|
+
// 走っている間の2回目は捨てる(丸もメニューも止めてあるが、正本はこの変数)
|
|
4633
|
+
if (siteIconSaving) return;
|
|
4634
|
+
function fail(msg) { showToast("✗ " + msg, true); ui.busy(false); ui.refocus(); }
|
|
4601
4635
|
// 画面側の門。**サーバ側と同じ文言**で、送る前に断る(fail で「✗ 」を揃える)
|
|
4602
4636
|
if (!projectIconExtOfName(file.name)) return fail("PNG・JPEG・WebP の画像を選んでください");
|
|
4603
4637
|
if (file.size > PROJECT_ICON_MAX_BYTES) return fail("1MB までの画像を選んでください");
|
|
@@ -4605,8 +4639,8 @@ ${deriveCapTile.toString()}
|
|
|
4605
4639
|
|
|
4606
4640
|
var intake = SITE.intake;
|
|
4607
4641
|
var ctype = file.type || "application/octet-stream";
|
|
4608
|
-
|
|
4609
|
-
ui.
|
|
4642
|
+
// 押した瞬間からぐるぐる(#378)。成功なら redraw で新しい丸に、失敗なら fail で元の丸に戻る
|
|
4643
|
+
ui.busy(true);
|
|
4610
4644
|
|
|
4611
4645
|
var pres;
|
|
4612
4646
|
try {
|
|
@@ -4631,36 +4665,39 @@ ${deriveCapTile.toString()}
|
|
|
4631
4665
|
} catch (net3) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
|
|
4632
4666
|
if (!saved.ok) return fail((saved.j && saved.j.message) || "保存できませんでした");
|
|
4633
4667
|
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4668
|
+
// **選んだ画像そのものを反映待ちに置く。** blob: にしないのは、
|
|
4669
|
+
// ポータルの一覧カード(別のページ)が同じ画像を読むため——blob: は
|
|
4670
|
+
// 作ったページの中でしか開けない
|
|
4671
|
+
var dataUrl = await fileToDataUrl(file);
|
|
4672
|
+
// **読めなかったら反映待ちを書かない**(サーバ側の保存は済んでいるので、仮画像だけ
|
|
4673
|
+
// 諦めて次のビルドを待つ)。空の set を書くと、直前の有効な反映待ちを上書きして消す
|
|
4674
|
+
if (dataUrl) rememberIconPending("set", dataUrl);
|
|
4638
4675
|
// **同じファイルをもう一度選べるようにする。** value を戻さないと、選び直しても
|
|
4639
4676
|
// change が飛ばない(形式で断られた直後に選び直す経路がここで死ぬ)
|
|
4640
4677
|
if (ui.input) ui.input.value = "";
|
|
4678
|
+
ui.busy(false);
|
|
4679
|
+
// 丸がその場で変わるので、メニューは畳んでよい(結果はトーストが伝える)
|
|
4680
|
+
setSiteIconMenu(false);
|
|
4641
4681
|
ui.redraw();
|
|
4642
|
-
|
|
4643
|
-
ui.status.appendChild(el("div", { text: "✓ 保存しました。数分後にポータルの一覧に反映されます。" }));
|
|
4644
|
-
if (repoPath && saved.j.repo) ui.status.appendChild(projectIconPathBox(saved.j.repo, repoPath));
|
|
4682
|
+
showToast("保存しました", false);
|
|
4645
4683
|
}
|
|
4646
4684
|
|
|
4647
4685
|
/** 画像を外す。**確認は出さない**——押し間違えても、もう一度選び直せば戻る操作 */
|
|
4648
4686
|
async function removeProjectIcon(ui) {
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4687
|
+
// 保存中はメニューの項目が disabled なので普通は来ない(setProjectIcon と同じ防御)
|
|
4688
|
+
if (siteIconSaving) return;
|
|
4689
|
+
function fail(msg) { showToast("✗ " + msg, true); ui.busy(false); ui.refocus(); }
|
|
4690
|
+
ui.busy(true);
|
|
4652
4691
|
var res;
|
|
4653
4692
|
try {
|
|
4654
4693
|
res = await intakeJson(callIntake("project-icon-remove", { projectKey: SITE.intake.projectKey }));
|
|
4655
4694
|
} catch (net) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
|
|
4656
4695
|
if (!res.ok) return fail((res.j && res.j.message) || "外せませんでした");
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
}
|
|
4661
|
-
projectIconMirror = { removed: true };
|
|
4696
|
+
rememberIconPending("remove", "");
|
|
4697
|
+
ui.busy(false);
|
|
4698
|
+
setSiteIconMenu(false);
|
|
4662
4699
|
ui.redraw();
|
|
4663
|
-
|
|
4700
|
+
showToast("外しました", false);
|
|
4664
4701
|
}
|
|
4665
4702
|
|
|
4666
4703
|
// ---------- ホーム(着地画面: Markdownをそのまま表示) ----------
|
|
@@ -6003,32 +6040,6 @@ ${deriveCapTile.toString()}
|
|
|
6003
6040
|
*/
|
|
6004
6041
|
var LIVE_FLEET = null;
|
|
6005
6042
|
var LIVE_FLEET_FAILED = false;
|
|
6006
|
-
/**
|
|
6007
|
-
* いまオンになっているハーネスの機能(ページ生存中の正本)。
|
|
6008
|
-
*
|
|
6009
|
-
* 🔴 **ここ(CLIENT_JS のトップレベル)に置くのが仕様。** buildDataSources の中に
|
|
6010
|
-
* 置くと、モーダルを閉じるたびに焼き値へ巻き戻る——close() は refreshDataSources() を
|
|
6011
|
-
* 呼んで節を作り直すので、**保存してモーダルを閉じた瞬間に、タイルの2行目と
|
|
6012
|
-
* 赤バッジが保存前の姿へ戻る**。loadLiveFleet の応答・失敗の経路でも再初期化しない
|
|
6013
|
-
*(op=state はこのキーを運ばないので、上書きできる材料がそもそも無い)。
|
|
6014
|
-
*
|
|
6015
|
-
* **寿命はこのページ内だけ。** 恒久化は保存コミット→次回ビルドの焼き直しで、
|
|
6016
|
-
* ここは「押した結果がその場で見える」ためのミラー(notifyToggle の s.notify と同型)。
|
|
6017
|
-
*/
|
|
6018
|
-
var HARNESS_SCHEDULES = sanitizeHarnessSchedules(SITE.harnessSchedules);
|
|
6019
|
-
/**
|
|
6020
|
-
* この案件で使う機能の棚(ページ生存中の正本)。**HARNESS_SCHEDULES と同じ置き場所**
|
|
6021
|
-
*(buildDataSources の中に置くと、モーダルを閉じるたびに焼き値へ巻き戻る)。
|
|
6022
|
-
*
|
|
6023
|
-
* 🔴 **null は「まだ棚を作っていない」で、空配列とは別の状態(設計 B-1)。**
|
|
6024
|
-
* null のときは棚の絞り込みそのものを効かせず、オン中の行を注記なしで並べる
|
|
6025
|
-
*——導入直後の既存案件が、いきなり「棚に無いがオン」の注記だらけになるのを避ける。
|
|
6026
|
-
* 空配列は「意図して空」で、空状態の文言に倒す。**null を [] へ丸めないこと。**
|
|
6027
|
-
*
|
|
6028
|
-
* **棚に置いても何も動かない。** 実行の正本は HARNESS_SCHEDULES のままで、
|
|
6029
|
-
* この配列は「この案件で使う候補」でしかない。
|
|
6030
|
-
*/
|
|
6031
|
-
var HARNESS_SHELF = SITE.harnessShelf == null ? null : sanitizeHarnessShelf(SITE.harnessShelf);
|
|
6032
6043
|
/**
|
|
6033
6044
|
* リアルタイム同期の可否(op=state の realtime。#113)。取得前・取得失敗時は null。
|
|
6034
6045
|
*
|
|
@@ -6690,942 +6701,6 @@ ${deriveCapTile.toString()}
|
|
|
6690
6701
|
showModal(m.overlay, m.modal);
|
|
6691
6702
|
}
|
|
6692
6703
|
|
|
6693
|
-
/* ---------------------------------------------------------------------------
|
|
6694
|
-
* ハーネス連携(7枚目の能力タイル)
|
|
6695
|
-
* ---------------------------------------------------------------------------
|
|
6696
|
-
* **既存のタイル(renderAdapterTile)には乗せない。** あちらは capabilityState を
|
|
6697
|
-
* 引くが、この能力は fleet-status.json にも capabilities にも無いので未知の能力の
|
|
6698
|
-
* 既定(off)へ落ち、2行目が「未設定」になる——タイルの状態表示が嘘になる。
|
|
6699
|
-
* 意味論も違う: ここで語るのは接続状態ではなく「何件オンか」と「配信先が無いか」。
|
|
6700
|
-
*
|
|
6701
|
-
* 一覧(何を選べるか)は提供側のリポジトリが持つので**ビルド時には焼けない**。
|
|
6702
|
-
* モーダルを開いたときに op=harness-catalog で取りに行く。
|
|
6703
|
-
* ------------------------------------------------------------------------- */
|
|
6704
|
-
|
|
6705
|
-
/**
|
|
6706
|
-
* 通知先になっているチャット行。**0件のままオンにすると、動いても誰にも届かない。**
|
|
6707
|
-
*
|
|
6708
|
-
* 読むのは fleetVal(登録の即時値がマージ済みの見え方)。写経すると
|
|
6709
|
-
* 「一覧には通知先の札が出ているのに、確認文は配信先なしと言う」が起きる。
|
|
6710
|
-
* **workspace を持つ行は除く**(読み取り専用の接続には通知が届かない・notifyToggle と同じ判定)。
|
|
6711
|
-
*/
|
|
6712
|
-
function harnessNotifyTargets() {
|
|
6713
|
-
var srcs = fleetVal("externalSources") || [];
|
|
6714
|
-
return srcs.filter(function (s) { return s && s.notify === true && !s.workspace; });
|
|
6715
|
-
}
|
|
6716
|
-
|
|
6717
|
-
/** 配信先の呼び名。**# を付けるのは Slack だけ**(付け方の正本は slackDisplayName) */
|
|
6718
|
-
function harnessTargetLabel(s) {
|
|
6719
|
-
var name = s.name || s.ref || "";
|
|
6720
|
-
return s.type === "slack" ? slackDisplayName(name) : name;
|
|
6721
|
-
}
|
|
6722
|
-
|
|
6723
|
-
/**
|
|
6724
|
-
* 周期の説明(「毎日・朝9時ごろ」「毎週金曜・夜9時ごろ」)。
|
|
6725
|
-
*
|
|
6726
|
-
* **「ごろ」はラベル側が持つ**(ここで足すと二重になる)。時間帯が分からない行は
|
|
6727
|
-
* 既定へ倒さず「時間帯未設定」と言う——語彙外の値は落ちており、その行は実行されない
|
|
6728
|
-
* ので、「朝9時ごろに動きます」と書くと画面が嘘をつく。
|
|
6729
|
-
*/
|
|
6730
|
-
function harnessPeriodText(slot, weekday) {
|
|
6731
|
-
var sl = HARNESS_SLOT_LABEL[slot];
|
|
6732
|
-
if (!sl) return "時間帯未設定";
|
|
6733
|
-
if (weekday && HARNESS_WEEKDAY_LABEL[weekday]) {
|
|
6734
|
-
return "毎週" + HARNESS_WEEKDAY_LABEL[weekday] + "・" + sl;
|
|
6735
|
-
}
|
|
6736
|
-
return "毎日・" + sl;
|
|
6737
|
-
}
|
|
6738
|
-
|
|
6739
|
-
/**
|
|
6740
|
-
* カタログの既定から言える周期(オフの行の表示と、オンにする確認文に使う)。
|
|
6741
|
-
*
|
|
6742
|
-
* 🔴 **既定が欠けていたら空文字=周期を言わない。** 既定の解決は intake が正本で、
|
|
6743
|
-
* ここで morning / fri を発明して「毎日・朝9時ごろ届き始めます」と言い切ると、
|
|
6744
|
-
* intake が別の値に解決したとき**確認文が嘘だったことになる**(契約裁定・レビュー Minor 5)。
|
|
6745
|
-
* H1 のカタログ CI は必須フィールドとして検証しているので、通常ここは空にならない。
|
|
6746
|
-
*/
|
|
6747
|
-
function harnessDefaultPeriod(meta) {
|
|
6748
|
-
if (!meta || !meta.default_slot) return "";
|
|
6749
|
-
if (meta.cadence === "weekly" && !meta.default_weekday) return "";
|
|
6750
|
-
return harnessPeriodText(meta.default_slot, meta.cadence === "weekly" ? meta.default_weekday : null);
|
|
6751
|
-
}
|
|
6752
|
-
|
|
6753
|
-
/** いまオンになっているエントリ(無ければ null)。**一致鍵は harness + skill の2つだけ** */
|
|
6754
|
-
function harnessEntryOf(harness, skill) {
|
|
6755
|
-
for (var i = 0; i < HARNESS_SCHEDULES.length; i++) {
|
|
6756
|
-
var e = HARNESS_SCHEDULES[i];
|
|
6757
|
-
if (e.harness === harness && e.skill === skill) return e;
|
|
6758
|
-
}
|
|
6759
|
-
return null;
|
|
6760
|
-
}
|
|
6761
|
-
|
|
6762
|
-
function harnessMirrorRemove(harness, skill) {
|
|
6763
|
-
for (var i = HARNESS_SCHEDULES.length - 1; i >= 0; i--) {
|
|
6764
|
-
var e = HARNESS_SCHEDULES[i];
|
|
6765
|
-
if (e.harness === harness && e.skill === skill) HARNESS_SCHEDULES.splice(i, 1);
|
|
6766
|
-
}
|
|
6767
|
-
}
|
|
6768
|
-
|
|
6769
|
-
/**
|
|
6770
|
-
* 保存できたことをミラーへ反映する。
|
|
6771
|
-
*
|
|
6772
|
-
* 🔴 **正は応答の entry。** 既定(時間帯・曜日)を解決するのは intake なので、
|
|
6773
|
-
* UI が既定を写経して覚えると、既定を変えたときに二重帳簿になる。
|
|
6774
|
-
* entry を持たない応答(古い Lambda)のときだけ fallback を使う。
|
|
6775
|
-
*
|
|
6776
|
-
* **どちらも sanitizeHarnessSchedules を通す。** 壊れた entry(slot が配列など)を
|
|
6777
|
-
* そのまま持つと、周期の文に [object Object] が出る。
|
|
6778
|
-
*/
|
|
6779
|
-
function harnessMirrorSet(harness, skill, entry, fallback) {
|
|
6780
|
-
var got = sanitizeHarnessSchedules([entry]);
|
|
6781
|
-
var row = got.length && got[0].harness === harness && got[0].skill === skill
|
|
6782
|
-
? got[0]
|
|
6783
|
-
: sanitizeHarnessSchedules([fallback])[0] || { harness: harness, skill: skill };
|
|
6784
|
-
harnessMirrorRemove(harness, skill);
|
|
6785
|
-
HARNESS_SCHEDULES.push(row);
|
|
6786
|
-
}
|
|
6787
|
-
|
|
6788
|
-
/** 棚に置いてあるか。**一致鍵は harness + skill**(オンの判定と同じ) */
|
|
6789
|
-
function harnessShelfHas(harness, skill) {
|
|
6790
|
-
if (!HARNESS_SHELF) return false;
|
|
6791
|
-
for (var i = 0; i < HARNESS_SHELF.length; i++) {
|
|
6792
|
-
var it = HARNESS_SHELF[i];
|
|
6793
|
-
if (it.harness === harness && it.skill === skill) return true;
|
|
6794
|
-
}
|
|
6795
|
-
return false;
|
|
6796
|
-
}
|
|
6797
|
-
|
|
6798
|
-
/**
|
|
6799
|
-
* 棚のミラーへ足す。**最初の1件で null から配列へ変わる**——「まだ棚を作っていない」
|
|
6800
|
-
* 案件でピッカーから1件置くと、そこから先は「棚が在る」案件として振る舞う
|
|
6801
|
-
*(サーバ側でも shelf.json がこの1件目で作られる)。
|
|
6802
|
-
*/
|
|
6803
|
-
function harnessShelfMirrorAdd(harness, skill) {
|
|
6804
|
-
if (!HARNESS_SHELF) HARNESS_SHELF = [];
|
|
6805
|
-
if (harnessShelfHas(harness, skill)) return;
|
|
6806
|
-
HARNESS_SHELF.push({ harness: harness, skill: skill });
|
|
6807
|
-
}
|
|
6808
|
-
|
|
6809
|
-
function harnessShelfMirrorRemove(harness, skill) {
|
|
6810
|
-
if (!HARNESS_SHELF) return;
|
|
6811
|
-
for (var i = HARNESS_SHELF.length - 1; i >= 0; i--) {
|
|
6812
|
-
var it = HARNESS_SHELF[i];
|
|
6813
|
-
if (it.harness === harness && it.skill === skill) HARNESS_SHELF.splice(i, 1);
|
|
6814
|
-
}
|
|
6815
|
-
}
|
|
6816
|
-
|
|
6817
|
-
/**
|
|
6818
|
-
* 棚の保存を**1件ずつ直列に**送るための鎖。
|
|
6819
|
-
*
|
|
6820
|
-
* 🔴 **並列に投げてはいけない。** 棚の操作は1件=1コミットで、同じファイルへの
|
|
6821
|
-
* 同時書き込みはサーバ側の commitWithRetry の衝突になる(ピッカーは連打される場所
|
|
6822
|
-
* ——「あれもこれも置く」が自然な操作なので、二重送信は正常系として起きる)。
|
|
6823
|
-
*
|
|
6824
|
-
* **失敗しても鎖を切らない**(then の第2引数にも同じ実行を渡す)——1件の失敗で
|
|
6825
|
-
* 以後の操作が永久に待たされる状態を作らない。sendConfig 自身は例外を投げないが、
|
|
6826
|
-
* 鎖の前段が何であっても次が走ることを、ここで保証しておく。
|
|
6827
|
-
*/
|
|
6828
|
-
var harnessShelfChain = null;
|
|
6829
|
-
function harnessShelfSend(payload, msg, btn, onOk) {
|
|
6830
|
-
// 🔴 **onDone ではなく hooks.onSaved で受ける。** 棚の追加は冪等で、
|
|
6831
|
-
// 「既に在る」は {ok:true, changed:false} の**正常な成功**。onDone は
|
|
6832
|
-
// changed !== false のときしか流れないので、そこに置くと
|
|
6833
|
-
// 「サーバには在るのに画面は棚が空」で取り残される(次のビルドまで直らない)
|
|
6834
|
-
var run = function () { return sendConfig(payload, msg, btn, null, { onSaved: onOk }); };
|
|
6835
|
-
var next = (harnessShelfChain || Promise.resolve()).then(run, run);
|
|
6836
|
-
harnessShelfChain = next;
|
|
6837
|
-
return next;
|
|
6838
|
-
}
|
|
6839
|
-
|
|
6840
|
-
/**
|
|
6841
|
-
* セクションの骨組み(部署 → ハーネス)。**応答のトップレベルの departments が正本で、
|
|
6842
|
-
* 画面は名簿の写しを持たない。** 読めなければ null=この応答は骨組みを持たない
|
|
6843
|
-
*(古い Lambda)ので、従来どおりのフラット表示へ落とす。
|
|
6844
|
-
*
|
|
6845
|
-
* 🔴 **提供元の識別子(owner/repo)も部署の語彙キーも画面には出さない**(#124 の原則)。
|
|
6846
|
-
* 表示名が解決できていない(=サーバが識別子やキーをそのまま返した)ときは、
|
|
6847
|
-
* **名前を出さない**方に倒す——部署は「その他」、ハーネスは見出しごと出さない。
|
|
6848
|
-
*/
|
|
6849
|
-
function harnessDepartmentsOf(j) {
|
|
6850
|
-
if (!j || typeof j !== "object" || Array.isArray(j)) return null;
|
|
6851
|
-
if (!Array.isArray(j.departments)) return null;
|
|
6852
|
-
var out = [];
|
|
6853
|
-
for (var i = 0; i < j.departments.length; i++) {
|
|
6854
|
-
var d = j.departments[i];
|
|
6855
|
-
if (d == null || typeof d !== "object" || Array.isArray(d)) continue;
|
|
6856
|
-
if (!Array.isArray(d.harnesses)) continue;
|
|
6857
|
-
var key = typeof d.key === "string" ? d.key : "";
|
|
6858
|
-
var list = [];
|
|
6859
|
-
for (var k = 0; k < d.harnesses.length; k++) {
|
|
6860
|
-
var hn = d.harnesses[k];
|
|
6861
|
-
if (hn == null || typeof hn !== "object" || Array.isArray(hn)) continue;
|
|
6862
|
-
// 名前の篩は行の篩と同じものを通す(描ける提供元だけを骨組みに載せる)
|
|
6863
|
-
if (sanitizeHarnessSchedules([{ harness: hn.harness, skill: "x" }]).length !== 1) continue;
|
|
6864
|
-
var hlabel = typeof hn.label === "string" ? hn.label : "";
|
|
6865
|
-
list.push({
|
|
6866
|
-
harness: hn.harness,
|
|
6867
|
-
label: hlabel && hlabel !== hn.harness ? hlabel : "",
|
|
6868
|
-
});
|
|
6869
|
-
}
|
|
6870
|
-
var dlabel = typeof d.label === "string" ? d.label : "";
|
|
6871
|
-
out.push({ key: key, label: dlabel && dlabel !== key ? dlabel : "", harnesses: list });
|
|
6872
|
-
}
|
|
6873
|
-
return out;
|
|
6874
|
-
}
|
|
6875
|
-
|
|
6876
|
-
/**
|
|
6877
|
-
* カタログの応答を、画面が扱ってよい形へ畳む。**読めなければ null(=異常表示)。**
|
|
6878
|
-
*
|
|
6879
|
-
* 🔴 **入口の判定は Array.isArray(j.harnesses)。** j.harnesses || [] と書くと、
|
|
6880
|
-
* harnesses の欄を持たない応答(古い Lambda・別 op のスタブ)が「正常の空」に化け、
|
|
6881
|
-
* 画面が「いま利用できる機能はありません」と、無いことを断言してしまう。
|
|
6882
|
-
*
|
|
6883
|
-
* **提供元の名前(owner/repo)も理由(HTTP コード等)も画面には出さない**(#124 の原則)。
|
|
6884
|
-
* errors はここでは件数としてしか使わない。
|
|
6885
|
-
*/
|
|
6886
|
-
function harnessCatalogOf(j) {
|
|
6887
|
-
if (!j || typeof j !== "object" || Array.isArray(j)) return null;
|
|
6888
|
-
if (!Array.isArray(j.harnesses)) return null;
|
|
6889
|
-
var errors = Array.isArray(j.errors) ? j.errors : [];
|
|
6890
|
-
var providers = [];
|
|
6891
|
-
for (var i = 0; i < j.harnesses.length; i++) {
|
|
6892
|
-
var h = j.harnesses[i];
|
|
6893
|
-
if (h == null || typeof h !== "object" || Array.isArray(h)) continue;
|
|
6894
|
-
if (typeof h.harness !== "string" || !h.harness) continue;
|
|
6895
|
-
if (!Array.isArray(h.skills)) continue; // 形が違う提供元は行を出さない(0件と混ぜない)
|
|
6896
|
-
var skills = [];
|
|
6897
|
-
// 変数名を s にしない(fleet-passthrough.test.mjs の走査対象。ここはカタログ応答で
|
|
6898
|
-
// fleet-status 由来ではないため、番人に fleet の欄として数えさせない)
|
|
6899
|
-
for (var k = 0; k < h.skills.length; k++) {
|
|
6900
|
-
var sk = h.skills[k];
|
|
6901
|
-
if (sk == null || typeof sk !== "object" || Array.isArray(sk)) continue;
|
|
6902
|
-
// **表示名が無い行は落とす。** 代わりに識別子(内部語)を出すと、#124 と同じ
|
|
6903
|
-
// 「読み手に意味の分からない名前が並ぶ」画面になる
|
|
6904
|
-
if (typeof sk.display_name !== "string" || !sk.display_name) continue;
|
|
6905
|
-
// 🔴 **描ける行は必ずミラーにも通ることを、ここで保証する。** 名前の篩を写経せず
|
|
6906
|
-
// sanitizeHarnessSchedules へ通す——通らない名前の行を描くと、オンにした直後に
|
|
6907
|
-
// ミラーが落として行が「オフ」へ戻る(押しても何も起きないように見える)
|
|
6908
|
-
if (sanitizeHarnessSchedules([{ harness: h.harness, skill: sk.skill }]).length !== 1) continue;
|
|
6909
|
-
var weekly = sk.cadence === "weekly";
|
|
6910
|
-
skills.push({
|
|
6911
|
-
harness: h.harness,
|
|
6912
|
-
skill: sk.skill,
|
|
6913
|
-
display_name: sk.display_name,
|
|
6914
|
-
summary: typeof sk.summary === "string" ? sk.summary : "",
|
|
6915
|
-
cadence: weekly ? "weekly" : "daily",
|
|
6916
|
-
// 🔴 **既定が欠けていても発明しない(空文字=不明)。** 既定の解決は intake が
|
|
6917
|
-
// 正本で、ここで morning / fri を作ると確認文が「毎日・朝9時ごろ」と言い切り、
|
|
6918
|
-
// intake が別に解決したとき嘘になる。不明の扱いは harnessDefaultPeriod が持つ
|
|
6919
|
-
default_slot: HARNESS_SLOTS.indexOf(sk.default_slot) >= 0 ? sk.default_slot : "",
|
|
6920
|
-
default_weekday: weekly
|
|
6921
|
-
? (HARNESS_WEEKDAYS.indexOf(sk.default_weekday) >= 0 ? sk.default_weekday : "")
|
|
6922
|
-
: null,
|
|
6923
|
-
});
|
|
6924
|
-
}
|
|
6925
|
-
providers.push({ harness: h.harness, skills: skills });
|
|
6926
|
-
}
|
|
6927
|
-
// **読めた提供元が1つも無く、読めなかった提供元がある → 異常。** ここで「0件」と
|
|
6928
|
-
// 言うと、実際には在るかもしれないものを「無い」と断言することになる
|
|
6929
|
-
if (!providers.length && errors.length) return null;
|
|
6930
|
-
return {
|
|
6931
|
-
providers: providers,
|
|
6932
|
-
errors: errors,
|
|
6933
|
-
partial: providers.length > 0 && errors.length > 0,
|
|
6934
|
-
// 骨組みを持たない応答(古い Lambda)では null。**セクション表示ごと落とす**
|
|
6935
|
-
// ——名簿を画面側に写経して補うと、名簿を1行足した日に画面だけが古いまま残る
|
|
6936
|
-
departments: harnessDepartmentsOf(j),
|
|
6937
|
-
};
|
|
6938
|
-
}
|
|
6939
|
-
|
|
6940
|
-
/** カタログの中の1機能(無ければ null) */
|
|
6941
|
-
function harnessMetaOf(cat, harness, skill) {
|
|
6942
|
-
for (var i = 0; i < cat.providers.length; i++) {
|
|
6943
|
-
var prov = cat.providers[i];
|
|
6944
|
-
if (prov.harness !== harness) continue;
|
|
6945
|
-
for (var k = 0; k < prov.skills.length; k++) {
|
|
6946
|
-
if (prov.skills[k].skill === skill) return prov.skills[k];
|
|
6947
|
-
}
|
|
6948
|
-
}
|
|
6949
|
-
return null;
|
|
6950
|
-
}
|
|
6951
|
-
|
|
6952
|
-
/** 一覧を読み込めなかった提供元(harness → true) */
|
|
6953
|
-
function harnessErroredMap(cat) {
|
|
6954
|
-
var errored = {};
|
|
6955
|
-
for (var i = 0; i < cat.errors.length; i++) {
|
|
6956
|
-
var er = cat.errors[i];
|
|
6957
|
-
if (er && typeof er === "object" && typeof er.harness === "string") errored[er.harness] = true;
|
|
6958
|
-
}
|
|
6959
|
-
return errored;
|
|
6960
|
-
}
|
|
6961
|
-
|
|
6962
|
-
/**
|
|
6963
|
-
* 語彙から select を作る。**現在値が語彙外のときだけ**先頭に空の選択肢を足す
|
|
6964
|
-
* ——既定へ勝手に倒すと、実際には実行されない行を「朝9時ごろ」と説明することになる。
|
|
6965
|
-
*/
|
|
6966
|
-
function harnessSelect(vocab, labels, current, blankLabel, ariaLabel) {
|
|
6967
|
-
var known = vocab.indexOf(current) >= 0;
|
|
6968
|
-
var sel = el("select", { "aria-label": ariaLabel });
|
|
6969
|
-
if (!known) sel.appendChild(el("option", { value: "", text: blankLabel }));
|
|
6970
|
-
for (var i = 0; i < vocab.length; i++) {
|
|
6971
|
-
sel.appendChild(el("option", { value: vocab[i], text: labels[vocab[i]] }));
|
|
6972
|
-
}
|
|
6973
|
-
sel.value = known ? current : "";
|
|
6974
|
-
return sel;
|
|
6975
|
-
}
|
|
6976
|
-
|
|
6977
|
-
/**
|
|
6978
|
-
* オンにする前の確認。**何が・どこに届き始めるのかを書く。**
|
|
6979
|
-
*
|
|
6980
|
-
* 🔴 **「いつ」は文で言わない。** 確認の中に時間帯の select を出すようになったので
|
|
6981
|
-
*(オンにするのと同じ流れで決められる)、括弧書きで周期も書くと、
|
|
6982
|
-
* **利用者が select を変えた瞬間に文のほうが嘘になる**。いつ届くかは select が示す。
|
|
6983
|
-
*
|
|
6984
|
-
* 「よろしいですか?」もここには置かない——select より後に問うため、
|
|
6985
|
-
* 呼び出し側が body の末尾へ積む(問いのあとに答えを左右する操作を出さない)。
|
|
6986
|
-
*/
|
|
6987
|
-
function harnessOnConfirmLines(meta) {
|
|
6988
|
-
var lines = [
|
|
6989
|
-
"オンにすると、この案件の通知先チャンネルに「" + meta.display_name + "」が届き始めます。",
|
|
6990
|
-
];
|
|
6991
|
-
var targets = harnessNotifyTargets();
|
|
6992
|
-
if (targets.length) {
|
|
6993
|
-
lines.push("配信先: " + harnessTargetLabel(targets[0])
|
|
6994
|
-
+ (targets.length > 1 ? " ほか" + (targets.length - 1) + "件" : "")
|
|
6995
|
-
+ "(「チャット」の通知先設定に従います)。");
|
|
6996
|
-
} else {
|
|
6997
|
-
// **ここで止めない。** 先にオンにして、あとで通知先を決める順番もありうる。
|
|
6998
|
-
// 届かないことだけを、はっきり伝える
|
|
6999
|
-
lines.push("⚠ この案件にはまだ配信先(通知先チャンネル)が設定されていません。オンにしても、「チャット」の通知先を設定するまで届きません。");
|
|
7000
|
-
}
|
|
7001
|
-
return lines;
|
|
7002
|
-
}
|
|
7003
|
-
|
|
7004
|
-
/**
|
|
7005
|
-
* 棚に置く / 棚から外す を送る(棚ビューとピッカーの共通処理)。
|
|
7006
|
-
*
|
|
7007
|
-
* 🔴 **オン中の機能を外すときは、止まらないことを操作の瞬間に言う(設計 B-2)。**
|
|
7008
|
-
* 「棚から外す」と「配信が止まる」は別のことで、取り違えられると
|
|
7009
|
-
* 「外したのに毎朝届く」になる。**オフの機能では確認を出さない**——即時に戻せる
|
|
7010
|
-
* 操作なので、確認を出すのは notifyToggle と同じ判断で過剰。
|
|
7011
|
-
*
|
|
7012
|
-
* 送信は harnessShelfSend を通す(1件ずつ直列。並列は同じファイルへの同時コミット)。
|
|
7013
|
-
*
|
|
7014
|
-
* @param label 画面に出ている名前(確認文に使う。識別子は使わない)
|
|
7015
|
-
*/
|
|
7016
|
-
async function harnessShelfToggle(harness, skill, label, msg, btn, after) {
|
|
7017
|
-
var onShelf = harnessShelfHas(harness, skill);
|
|
7018
|
-
if (onShelf && harnessEntryOf(harness, skill)) {
|
|
7019
|
-
var lines = [
|
|
7020
|
-
"「" + label + "」を、この案件で使う機能から外します。",
|
|
7021
|
-
"この機能はいまオンなので、外しても配信は止まりません。止めるには「オフにする」を押してください。",
|
|
7022
|
-
"よろしいですか?",
|
|
7023
|
-
];
|
|
7024
|
-
// **赤で出さない。** 何かが消える操作ではなく、候補の一覧から外すだけ
|
|
7025
|
-
if (!(await openConfirm(lines, { okLabel: "使う機能から外す", returnTo: btn, okDanger: false }))) {
|
|
7026
|
-
msg.textContent = "";
|
|
7027
|
-
return;
|
|
7028
|
-
}
|
|
7029
|
-
}
|
|
7030
|
-
// 🔴 **順番が回ってくる前に、押したボタンを止める。** 送信は直列なので、
|
|
7031
|
-
// beginBusy(sendConfig の中)が効くのは自分の番が来てから——それまでは
|
|
7032
|
-
// 押せる見た目のまま残り、①連打で同じ追加が2本飛ぶ ②押した感触が無いので
|
|
7033
|
-
// 何度も押す、の両方が起きる。**戻すのは sendConfig の endBusy**
|
|
7034
|
-
btn.disabled = true;
|
|
7035
|
-
await harnessShelfSend(
|
|
7036
|
-
{
|
|
7037
|
-
op: onShelf ? "harness-shelf-remove" : "harness-shelf-add",
|
|
7038
|
-
harness: harness,
|
|
7039
|
-
skill: skill,
|
|
7040
|
-
},
|
|
7041
|
-
msg,
|
|
7042
|
-
btn,
|
|
7043
|
-
function () {
|
|
7044
|
-
// 応答は棚の中身を運ばないので、送った向きでミラーを当てる
|
|
7045
|
-
//(add は冪等・remove は無い行でも成功なので、向きだけで確定する)
|
|
7046
|
-
if (onShelf) harnessShelfMirrorRemove(harness, skill);
|
|
7047
|
-
else harnessShelfMirrorAdd(harness, skill);
|
|
7048
|
-
if (after) after();
|
|
7049
|
-
},
|
|
7050
|
-
);
|
|
7051
|
-
}
|
|
7052
|
-
|
|
7053
|
-
/**
|
|
7054
|
-
* 機能1件の行。
|
|
7055
|
-
*
|
|
7056
|
-
* @param meta カタログの中身。**null はゴースト行**(提供元から消えた機能。出せる
|
|
7057
|
-
* 表示名が無いので、オフにする手段だけを残す——出さないと画面から止められなくなる)
|
|
7058
|
-
* @param opts 棚ビュー専用の追加(骨組みを持たない応答では渡さない=従来の見た目)
|
|
7059
|
-
* - shelf … 棚の導線(置く/外す)を出すか
|
|
7060
|
-
* - unreadable … 一覧を読めなかった提供元の行か。**「もう無い」と言わない**(B-4)
|
|
7061
|
-
* ——次に開けば普通に並ぶので、消えたと伝えるのは嘘になる
|
|
7062
|
-
*/
|
|
7063
|
-
function harnessRow(harness, skill, meta, opts) {
|
|
7064
|
-
opts = opts || {};
|
|
7065
|
-
var row = el("div", { class: "ds-harness-row" });
|
|
7066
|
-
var label = meta
|
|
7067
|
-
? meta.display_name
|
|
7068
|
-
: (opts.unreadable ? "(名前を読み込めなかった機能)" : "(提供元にもう無い機能)");
|
|
7069
|
-
var name = el("div", { class: "ds-harness-name", text: label });
|
|
7070
|
-
var period = el("div", { class: "ds-harness-period" });
|
|
7071
|
-
row.appendChild(el("div", { class: "ds-harness-head" }, [name, period]));
|
|
7072
|
-
if (meta && meta.summary) row.appendChild(el("div", { class: "ds-harness-summary", text: meta.summary }));
|
|
7073
|
-
// 「棚に無いのにオンになっている」ことの注記。**棚が在るときだけ出す**(B-1)
|
|
7074
|
-
// ——棚を作っていない案件で出すと、既存案件が導入直後に注記だらけになる
|
|
7075
|
-
var note = el("div", { class: "ds-harness-note" });
|
|
7076
|
-
note.hidden = true;
|
|
7077
|
-
row.appendChild(note);
|
|
7078
|
-
var btn = el("button", { class: "ds-toggle-btn", type: "button" });
|
|
7079
|
-
var selects = el("span", { class: "ds-harness-selects" });
|
|
7080
|
-
var msg = el("div", { class: "ds-toggle-msg" });
|
|
7081
|
-
var shelfBtn = opts.shelf
|
|
7082
|
-
? el("button", { class: "ds-toggle-btn ds-harness-shelf-btn", type: "button" })
|
|
7083
|
-
: null;
|
|
7084
|
-
// **トグルは必ず先頭に置く**(行の先頭のボタン=オン/オフ、が既存の検査の前提)
|
|
7085
|
-
row.appendChild(el("div", { class: "ds-toggle" }, [btn, selects, shelfBtn, msg]));
|
|
7086
|
-
if (shelfBtn) {
|
|
7087
|
-
shelfBtn.addEventListener("click", function () {
|
|
7088
|
-
harnessShelfToggle(harness, skill, label, msg, shelfBtn, paint);
|
|
7089
|
-
});
|
|
7090
|
-
}
|
|
7091
|
-
|
|
7092
|
-
/**
|
|
7093
|
-
* 時間帯(毎週なら曜日も)の調整。**確認モーダルは出さない**——同じ画面から
|
|
7094
|
-
* 即時に戻せる操作で、notifyToggle と同じ判断。
|
|
7095
|
-
*/
|
|
7096
|
-
function buildSelects(e) {
|
|
7097
|
-
var weekly = meta.cadence === "weekly";
|
|
7098
|
-
var slotSel = harnessSelect(HARNESS_SLOTS, HARNESS_SLOT_LABEL, e.slot, "時間帯未設定", "時間帯");
|
|
7099
|
-
var daySel = weekly
|
|
7100
|
-
? harnessSelect(HARNESS_WEEKDAYS, HARNESS_WEEKDAY_LABEL, e.weekday, "曜日未設定", "曜日")
|
|
7101
|
-
: null;
|
|
7102
|
-
var prevSlot = slotSel.value;
|
|
7103
|
-
var prevDay = daySel ? daySel.value : "";
|
|
7104
|
-
var onChange = async function () {
|
|
7105
|
-
var payload = { op: "harness-schedule-set", harness: harness, skill: skill };
|
|
7106
|
-
// 🔴 **時間帯と曜日は両方送る。** set は同じ harness+skill の置き換えで、
|
|
7107
|
-
// 省略したキーは既定へ解決されるので、片方だけ送るともう片方が既定へ巻き戻る。
|
|
7108
|
-
// 値が分からない(語彙外で落ちていた)ぶんだけは送らず、既定の解決に委ねる
|
|
7109
|
-
if (slotSel.value) payload.slot = slotSel.value;
|
|
7110
|
-
if (daySel && daySel.value) payload.weekday = daySel.value;
|
|
7111
|
-
slotSel.disabled = true;
|
|
7112
|
-
if (daySel) daySel.disabled = true;
|
|
7113
|
-
// sendConfig は例外を投げず戻り値も持たないので、成否は onDone で受ける
|
|
7114
|
-
var applied = false;
|
|
7115
|
-
await sendConfig(payload, msg, null, function (j) {
|
|
7116
|
-
applied = true;
|
|
7117
|
-
harnessMirrorSet(harness, skill, j && j.entry, {
|
|
7118
|
-
harness: harness, skill: skill, slot: payload.slot, weekday: payload.weekday,
|
|
7119
|
-
});
|
|
7120
|
-
var now = harnessEntryOf(harness, skill);
|
|
7121
|
-
if (now) {
|
|
7122
|
-
period.textContent = harnessPeriodText(now.slot, now.weekday);
|
|
7123
|
-
if (now.slot) slotSel.value = now.slot;
|
|
7124
|
-
if (daySel && now.weekday) daySel.value = now.weekday;
|
|
7125
|
-
}
|
|
7126
|
-
prevSlot = slotSel.value;
|
|
7127
|
-
prevDay = daySel ? daySel.value : "";
|
|
7128
|
-
});
|
|
7129
|
-
slotSel.disabled = false;
|
|
7130
|
-
if (daySel) daySel.disabled = false;
|
|
7131
|
-
// **失敗したら元の値へ戻す。** 画面だけが変わった状態を残すと、次に開いたとき
|
|
7132
|
-
//(焼き直しの後)に理由もなく値が戻る
|
|
7133
|
-
if (!applied) {
|
|
7134
|
-
slotSel.value = prevSlot;
|
|
7135
|
-
if (daySel) daySel.value = prevDay;
|
|
7136
|
-
}
|
|
7137
|
-
};
|
|
7138
|
-
slotSel.addEventListener("change", onChange);
|
|
7139
|
-
if (daySel) daySel.addEventListener("change", onChange);
|
|
7140
|
-
selects.appendChild(slotSel);
|
|
7141
|
-
if (daySel) selects.appendChild(daySel);
|
|
7142
|
-
}
|
|
7143
|
-
|
|
7144
|
-
/**
|
|
7145
|
-
* いまの向きに合わせて当て直す。**行ごと作り直さない**——作り直すと
|
|
7146
|
-
* sendConfig が書いた「✓ 保存しました。」ごと捨てることになる。
|
|
7147
|
-
*/
|
|
7148
|
-
function paint() {
|
|
7149
|
-
var e = harnessEntryOf(harness, skill);
|
|
7150
|
-
btn.textContent = e ? "オフにする" : "オンにする";
|
|
7151
|
-
// **オフにしたゴースト行はボタンごと止める。** カタログに中身が無いのでオンには
|
|
7152
|
-
// 戻せない——押せる見た目のまま黙って何もしないボタンを残さない
|
|
7153
|
-
btn.disabled = !e && !meta;
|
|
7154
|
-
if (opts.shelf) {
|
|
7155
|
-
var onShelf = harnessShelfHas(harness, skill);
|
|
7156
|
-
// 🔴 **画面に「棚」の語を出さない(分類B・リーダー承認)。** 内部では棚と呼ぶが、
|
|
7157
|
-
// 利用者に見えるのは「この案件で使う機能」という1つの言い方だけにする
|
|
7158
|
-
//(動詞もピッカーと揃える——同じ操作が2つの名前で呼ばれない)
|
|
7159
|
-
shelfBtn.textContent = onShelf ? "使う機能から外す" : "使う機能に追加";
|
|
7160
|
-
// 🔴 **棚にあるが提供元から消えた機能は「外す」だけの行にする(B-5)。**
|
|
7161
|
-
// オンにも戻せないボタンを並べると、腐った行を誰も片付けられないように見える
|
|
7162
|
-
btn.hidden = !meta && !e;
|
|
7163
|
-
// 棚にも無く提供元にも無い行(外した直後・オフにした直後)には、
|
|
7164
|
-
// 置き直す口を出さない——実在しない機能を棚へ戻せてしまう
|
|
7165
|
-
shelfBtn.hidden = !meta && !onShelf;
|
|
7166
|
-
// **「棚から外してもオン中は表示に残る」の実体**(B-2 の約束が守られていることが、
|
|
7167
|
-
// 外した直後の画面そのもので見える)。
|
|
7168
|
-
// 🔴 **棚を作っていない案件(HARNESS_SHELF が null)では出さない(B-1)。**
|
|
7169
|
-
// 出すと、この機能の導入直後に既存案件のオン中の行が全部この注記を持つ
|
|
7170
|
-
var stray = !!e && !onShelf && !!HARNESS_SHELF;
|
|
7171
|
-
note.hidden = !stray;
|
|
7172
|
-
if (stray) note.textContent = "この案件で使う機能には入っていませんが、オンになっています。";
|
|
7173
|
-
}
|
|
7174
|
-
// **オフの行はカタログの既定で周期を出す**(オンの行は定義ファイルの具体値)。
|
|
7175
|
-
// 既定が欠けている行では空=周期を言わない(harnessDefaultPeriod の項。
|
|
7176
|
-
// 「時間帯未設定」はオンの行の語——オフの行に出すと、設定の抜けがあるように読める)
|
|
7177
|
-
period.textContent = e
|
|
7178
|
-
? harnessPeriodText(e.slot, e.weekday)
|
|
7179
|
-
: harnessDefaultPeriod(meta);
|
|
7180
|
-
while (selects.firstChild) selects.removeChild(selects.firstChild);
|
|
7181
|
-
// ゴースト行には select を出さない(カタログが無いので周期の語彙が決まらない)
|
|
7182
|
-
if (e && meta) buildSelects(e);
|
|
7183
|
-
}
|
|
7184
|
-
|
|
7185
|
-
btn.addEventListener("click", async function () {
|
|
7186
|
-
if (harnessEntryOf(harness, skill)) {
|
|
7187
|
-
// **オフは確認を出さない。** 配信が止まる向きだが、同じ画面から即時に戻せる
|
|
7188
|
-
//(取り消せない「登録を解除」とは扱いを分ける・notifyToggle と同じ判断)
|
|
7189
|
-
await sendConfig({ op: "harness-schedule-remove", harness: harness, skill: skill }, msg, btn, function () {
|
|
7190
|
-
harnessMirrorRemove(harness, skill);
|
|
7191
|
-
paint();
|
|
7192
|
-
});
|
|
7193
|
-
return;
|
|
7194
|
-
}
|
|
7195
|
-
if (!meta) return; // ゴースト行はオンにできない(カタログに中身が無い)
|
|
7196
|
-
var weekly = meta.cadence === "weekly";
|
|
7197
|
-
// **確認の中で時間帯(毎週なら曜日も)まで決める。** オンにしてから調整させると、
|
|
7198
|
-
// 「何を・いつ受け取るか」を決めるのに2手かかる(オーナー実機フィードバック)。
|
|
7199
|
-
// **開くたびに組み直す。** 使い回すと、キャンセルした選択が次に開いたときも残り、
|
|
7200
|
-
// 「既定でオンにする」つもりの人が前回いじった値でオンにしてしまう
|
|
7201
|
-
var slotSel = harnessSelect(HARNESS_SLOTS, HARNESS_SLOT_LABEL, meta.default_slot, "時間帯未設定", "時間帯");
|
|
7202
|
-
var daySel = weekly
|
|
7203
|
-
? harnessSelect(HARNESS_WEEKDAYS, HARNESS_WEEKDAY_LABEL, meta.default_weekday, "曜日未設定", "曜日")
|
|
7204
|
-
: null;
|
|
7205
|
-
var picks = el("span", { class: "ds-harness-selects" }, [
|
|
7206
|
-
el("span", { class: "confirm-body-label", text: "届く時間帯:" }),
|
|
7207
|
-
slotSel,
|
|
7208
|
-
daySel,
|
|
7209
|
-
]);
|
|
7210
|
-
var body = el("div", { class: "confirm-body" }, [picks]);
|
|
7211
|
-
// 🔴 **既定が欠けている(=選択が空の)ときだけ、保存時に決まることを言う。**
|
|
7212
|
-
// 空のまま OK すると intake が既定を**決定として**書くので、「未設定」の見た目の
|
|
7213
|
-
// まま黙っていると、画面と保存結果が食い違う。具体値が見えているときは言わない
|
|
7214
|
-
//(下の select が示していることを、文でもう一度言うことになる)
|
|
7215
|
-
if (!slotSel.value || (daySel && !daySel.value)) {
|
|
7216
|
-
body.appendChild(el("p", { class: "confirm-line", text: "時間帯は保存時に決まり、あとでこの画面で調整できます。" }));
|
|
7217
|
-
}
|
|
7218
|
-
// **問いは選択の後ろ。** lines に混ぜると「よろしいですか?→[選択]→[ボタン]」と
|
|
7219
|
-
// なり、問うたあとに答えを左右する操作が出る
|
|
7220
|
-
body.appendChild(el("p", { class: "confirm-line", text: "よろしいですか?" }));
|
|
7221
|
-
// **OK は赤くしない。** 危険な向きではなく「届き始める」向きの操作なので、
|
|
7222
|
-
// 削除と同じ赤で出すと、止めるべき操作に見える
|
|
7223
|
-
if (!(await openConfirm(harnessOnConfirmLines(meta), { okLabel: "オンにする", returnTo: btn, okDanger: false, body: body }))) {
|
|
7224
|
-
msg.textContent = "";
|
|
7225
|
-
return;
|
|
7226
|
-
}
|
|
7227
|
-
// 🔴 **既定と同じ値は送らない。** 既定の解決は intake が正本で、UI が写経して送ると
|
|
7228
|
-
// 既定を変えたときに二重帳簿になる。送るのは利用者が既定から**変えた**ぶんだけ
|
|
7229
|
-
//(set は置き換えだが、省いたキーは intake が既定へ解決する=見えていた初期選択に
|
|
7230
|
-
// なるので、画面と食い違わない)。
|
|
7231
|
-
// **select はもう DOM から外れている**(確認が閉じた)が、.value は読める
|
|
7232
|
-
var payload = { op: "harness-schedule-set", harness: harness, skill: skill };
|
|
7233
|
-
if (slotSel.value && slotSel.value !== (meta.default_slot || "")) payload.slot = slotSel.value;
|
|
7234
|
-
if (daySel && daySel.value && daySel.value !== (meta.default_weekday || "")) payload.weekday = daySel.value;
|
|
7235
|
-
await sendConfig(payload, msg, btn, function (j) {
|
|
7236
|
-
harnessMirrorSet(harness, skill, j && j.entry, {
|
|
7237
|
-
harness: harness,
|
|
7238
|
-
skill: skill,
|
|
7239
|
-
// 応答が entry を持たないときの当て先。**選んだ値で当てる**(既定固定にすると、
|
|
7240
|
-
// 変えてオンにした直後の行が既定の周期を出す)
|
|
7241
|
-
slot: payload.slot || meta.default_slot,
|
|
7242
|
-
weekday: weekly ? (payload.weekday || meta.default_weekday) : undefined,
|
|
7243
|
-
});
|
|
7244
|
-
paint();
|
|
7245
|
-
});
|
|
7246
|
-
});
|
|
7247
|
-
|
|
7248
|
-
paint();
|
|
7249
|
-
return row;
|
|
7250
|
-
}
|
|
7251
|
-
|
|
7252
|
-
/**
|
|
7253
|
-
* 骨組みを持たない応答(古い Lambda)のときの中身。**従来どおりのフラット表示。**
|
|
7254
|
-
* 行が1つも無ければ「0件」の文言に倒す。
|
|
7255
|
-
*
|
|
7256
|
-
* **ここに棚の絞り込みを持ち込まない。** 骨組みが無い=部署もハーネスの表示名も
|
|
7257
|
-
* 無い状態で棚だけ効かせると、「選んだはずの機能がどこにも見当たらない」画面になる。
|
|
7258
|
-
*/
|
|
7259
|
-
function harnessFillList(list, setState, warn, cat) {
|
|
7260
|
-
var errored = harnessErroredMap(cat);
|
|
7261
|
-
var seen = {};
|
|
7262
|
-
var rows = 0;
|
|
7263
|
-
for (var p = 0; p < cat.providers.length; p++) {
|
|
7264
|
-
var prov = cat.providers[p];
|
|
7265
|
-
for (var k = 0; k < prov.skills.length; k++) {
|
|
7266
|
-
var meta = prov.skills[k];
|
|
7267
|
-
seen[meta.harness + "\\t" + meta.skill] = true;
|
|
7268
|
-
list.appendChild(harnessRow(meta.harness, meta.skill, meta));
|
|
7269
|
-
rows += 1;
|
|
7270
|
-
}
|
|
7271
|
-
}
|
|
7272
|
-
// **カタログに無いのにオンになっている行も出す(ゴースト行)。** 出さないと、
|
|
7273
|
-
// 提供側が機能を外したあとに画面からオフにする手段が消える(remove はカタログ検証不要)
|
|
7274
|
-
for (var s = 0; s < HARNESS_SCHEDULES.length; s++) {
|
|
7275
|
-
var ent = HARNESS_SCHEDULES[s];
|
|
7276
|
-
if (seen[ent.harness + "\\t" + ent.skill]) continue;
|
|
7277
|
-
// **一覧が読めなかっただけの提供元の行に「もう無い」と言わない。** 次に開けば
|
|
7278
|
-
// 普通の行として並ぶので、消えたと伝えるのは嘘になる(タイルの件数には残す)
|
|
7279
|
-
if (errored[ent.harness]) continue;
|
|
7280
|
-
list.appendChild(harnessRow(ent.harness, ent.skill, null));
|
|
7281
|
-
rows += 1;
|
|
7282
|
-
}
|
|
7283
|
-
// 行が並んだ/一部だけ読めた(警告が事情を説明する)ときは、状態の1行を伏せる
|
|
7284
|
-
if (rows || cat.partial) setState("");
|
|
7285
|
-
else setState("いま利用できる機能はありません。機能が追加されると、ここに並びます。");
|
|
7286
|
-
warn.hidden = !cat.partial;
|
|
7287
|
-
}
|
|
7288
|
-
|
|
7289
|
-
/**
|
|
7290
|
-
* 描く順番(部署 → ハーネス)を決める。
|
|
7291
|
-
*
|
|
7292
|
-
* 🔴 **骨組みに無い提供元を落とさない。** 名簿から外れた提供元の行が棚やオンに
|
|
7293
|
-
* 残っていることはある(名簿は艦隊側の都合で変わる)。骨組みだけで描くと、
|
|
7294
|
-
* その行は**画面から消えて誰も外せなくなる**——ゴースト行と同じ理由で「その他」へ回す。
|
|
7295
|
-
*
|
|
7296
|
-
* 同じ見出しは1つに畳む(部署の表示名が解決できない行と「その他」が2つに割れない)。
|
|
7297
|
-
*/
|
|
7298
|
-
function harnessSectionPlan(cat) {
|
|
7299
|
-
var plan = [];
|
|
7300
|
-
var placed = {};
|
|
7301
|
-
var groupFor = function (label) {
|
|
7302
|
-
for (var g = 0; g < plan.length; g++) if (plan[g].label === label) return plan[g];
|
|
7303
|
-
var made = { label: label, harnesses: [] };
|
|
7304
|
-
plan.push(made);
|
|
7305
|
-
return made;
|
|
7306
|
-
};
|
|
7307
|
-
for (var i = 0; i < cat.departments.length; i++) {
|
|
7308
|
-
var dep = cat.departments[i];
|
|
7309
|
-
var group = groupFor(dep.label || "その他");
|
|
7310
|
-
for (var k = 0; k < dep.harnesses.length; k++) {
|
|
7311
|
-
var hn = dep.harnesses[k];
|
|
7312
|
-
if (placed[hn.harness]) continue;
|
|
7313
|
-
placed[hn.harness] = true;
|
|
7314
|
-
group.harnesses.push(hn);
|
|
7315
|
-
}
|
|
7316
|
-
}
|
|
7317
|
-
var addExtra = function (harness) {
|
|
7318
|
-
if (placed[harness]) return;
|
|
7319
|
-
placed[harness] = true;
|
|
7320
|
-
// 表示名が分からない提供元。**識別子は見出しに出さない**(小見出しごと出さない)
|
|
7321
|
-
groupFor("その他").harnesses.push({ harness: harness, label: "" });
|
|
7322
|
-
};
|
|
7323
|
-
for (var p = 0; p < cat.providers.length; p++) addExtra(cat.providers[p].harness);
|
|
7324
|
-
if (HARNESS_SHELF) {
|
|
7325
|
-
for (var s = 0; s < HARNESS_SHELF.length; s++) addExtra(HARNESS_SHELF[s].harness);
|
|
7326
|
-
}
|
|
7327
|
-
for (var e = 0; e < HARNESS_SCHEDULES.length; e++) addExtra(HARNESS_SCHEDULES[e].harness);
|
|
7328
|
-
return plan;
|
|
7329
|
-
}
|
|
7330
|
-
|
|
7331
|
-
/**
|
|
7332
|
-
* 1つの提供元について、棚ビューに出す機能を並べ順つきで決める。
|
|
7333
|
-
*
|
|
7334
|
-
* 出すのは **棚にあるもの ∪ オンになっているもの**。
|
|
7335
|
-
* 🔴 **オンの機能は棚に無くても必ず出す。** 棚で隠すと、その機能を画面からオフに
|
|
7336
|
-
* できなくなる(ゴースト行と同じ理由)。
|
|
7337
|
-
*
|
|
7338
|
-
* 並びは**カタログ順**(提供側が決めた順)→ カタログに無いもの。
|
|
7339
|
-
* **棚の配列順は使わない**——棚は集合で、置いた順に意味は無い。
|
|
7340
|
-
*/
|
|
7341
|
-
function harnessShelfRowsFor(cat, harness) {
|
|
7342
|
-
var out = [];
|
|
7343
|
-
var seen = {};
|
|
7344
|
-
var take = function (skill) {
|
|
7345
|
-
if (seen[skill]) return;
|
|
7346
|
-
seen[skill] = true;
|
|
7347
|
-
out.push(skill);
|
|
7348
|
-
};
|
|
7349
|
-
for (var i = 0; i < cat.providers.length; i++) {
|
|
7350
|
-
var prov = cat.providers[i];
|
|
7351
|
-
if (prov.harness !== harness) continue;
|
|
7352
|
-
for (var k = 0; k < prov.skills.length; k++) {
|
|
7353
|
-
var sk = prov.skills[k].skill;
|
|
7354
|
-
if (harnessShelfHas(harness, sk) || harnessEntryOf(harness, sk)) take(sk);
|
|
7355
|
-
}
|
|
7356
|
-
}
|
|
7357
|
-
// カタログに無いもの(提供元から消えた・一覧を読めなかった)を後ろへ
|
|
7358
|
-
if (HARNESS_SHELF) {
|
|
7359
|
-
for (var s = 0; s < HARNESS_SHELF.length; s++) {
|
|
7360
|
-
if (HARNESS_SHELF[s].harness === harness) take(HARNESS_SHELF[s].skill);
|
|
7361
|
-
}
|
|
7362
|
-
}
|
|
7363
|
-
for (var e = 0; e < HARNESS_SCHEDULES.length; e++) {
|
|
7364
|
-
if (HARNESS_SCHEDULES[e].harness === harness) take(HARNESS_SCHEDULES[e].skill);
|
|
7365
|
-
}
|
|
7366
|
-
return out;
|
|
7367
|
-
}
|
|
7368
|
-
|
|
7369
|
-
/**
|
|
7370
|
-
* 棚ビュー(既定)を描く。**部署セクション → ハーネス小見出し → 機能の行。**
|
|
7371
|
-
*
|
|
7372
|
-
* **開き直しではなく描き直しで呼ばれる**(ピッカーから戻ったとき)ので、
|
|
7373
|
-
* 先に中身を空にする。中身が無いセクション・小見出しは立てない
|
|
7374
|
-
*(「中身の無い見出しを立てない」は能力タイル側と同じ約束)。
|
|
7375
|
-
*/
|
|
7376
|
-
function harnessRenderShelf(list, setState, cat) {
|
|
7377
|
-
while (list.firstChild) list.removeChild(list.firstChild);
|
|
7378
|
-
var errored = harnessErroredMap(cat);
|
|
7379
|
-
var plan = harnessSectionPlan(cat);
|
|
7380
|
-
var rows = 0;
|
|
7381
|
-
for (var i = 0; i < plan.length; i++) {
|
|
7382
|
-
var group = plan[i];
|
|
7383
|
-
var inner = el("div", { class: "ds-harness-dep-body" });
|
|
7384
|
-
var groupRows = 0;
|
|
7385
|
-
for (var k = 0; k < group.harnesses.length; k++) {
|
|
7386
|
-
var hn = group.harnesses[k];
|
|
7387
|
-
var skills = harnessShelfRowsFor(cat, hn.harness);
|
|
7388
|
-
var box = el("div", { class: "ds-harness-list" });
|
|
7389
|
-
var boxRows = 0;
|
|
7390
|
-
for (var s = 0; s < skills.length; s++) {
|
|
7391
|
-
var meta = harnessMetaOf(cat, hn.harness, skills[s]);
|
|
7392
|
-
var on = !!harnessEntryOf(hn.harness, skills[s]);
|
|
7393
|
-
var bad = !!errored[hn.harness];
|
|
7394
|
-
// **読めなかった提供元の「棚にあるだけ」の行は出さない。** 消えたのか
|
|
7395
|
-
// 読めなかったのかを言えないので、どう書いても嘘になる(次に開けば並ぶ)
|
|
7396
|
-
if (!meta && bad && !on) continue;
|
|
7397
|
-
box.appendChild(harnessRow(hn.harness, skills[s], meta, {
|
|
7398
|
-
// 🔴 一覧を読めなかった提供元のオン中の行は、棚の導線を出さない(B-4)。
|
|
7399
|
-
// 棚に在るかどうかは分かるが、「もう無い」のか「読めなかった」のかが
|
|
7400
|
-
// 分からないまま外す口だけ出すと、直せる状態を誤って片付けさせる
|
|
7401
|
-
shelf: !meta && bad ? false : true,
|
|
7402
|
-
unreadable: !meta && bad,
|
|
7403
|
-
}));
|
|
7404
|
-
boxRows += 1;
|
|
7405
|
-
}
|
|
7406
|
-
if (!boxRows) continue;
|
|
7407
|
-
// 表示名の無い提供元では小見出しごと出さない(識別子は画面に出さない)
|
|
7408
|
-
if (hn.label) inner.appendChild(el("h4", { class: "ds-harness-prov-title", text: hn.label }));
|
|
7409
|
-
if (errored[hn.harness]) {
|
|
7410
|
-
inner.appendChild(el("p", {
|
|
7411
|
-
class: "ds-harness-subnote",
|
|
7412
|
-
text: "この提供元の一覧を、いま読み込めませんでした。ここに並んでいない機能があるかもしれません。",
|
|
7413
|
-
}));
|
|
7414
|
-
}
|
|
7415
|
-
inner.appendChild(box);
|
|
7416
|
-
groupRows += boxRows;
|
|
7417
|
-
}
|
|
7418
|
-
if (!groupRows) continue;
|
|
7419
|
-
list.appendChild(el("div", { class: "ds-harness-dep" }, [
|
|
7420
|
-
el("h3", { class: "ds-harness-dep-title", text: group.label }),
|
|
7421
|
-
inner,
|
|
7422
|
-
]));
|
|
7423
|
-
rows += groupRows;
|
|
7424
|
-
}
|
|
7425
|
-
if (rows || cat.partial) { setState(""); return; }
|
|
7426
|
-
// 🔴 **「棚が無い」と「棚が空」を言い分ける(B-1)。** 前者はまだ何も選んでいない
|
|
7427
|
-
// 案件、後者は選んだうえで全部外した案件で、次にすることが違う
|
|
7428
|
-
setState(HARNESS_SHELF
|
|
7429
|
-
// **主語を先に置く**(何の話かを言ってから、その状態を言う)
|
|
7430
|
-
? "この案件で使う機能は、いま1件もありません。「機能をえらぶ」から追加できます。"
|
|
7431
|
-
: "この案件で使う機能は、まだ選んでいません。「機能をえらぶ」から選べます。");
|
|
7432
|
-
}
|
|
7433
|
-
|
|
7434
|
-
/**
|
|
7435
|
-
* ピッカー(機能をえらぶ)を描く。**全カタログを部署 → ハーネスで並べ、
|
|
7436
|
-
* 各機能に「棚に置く / 置いてある(外す)」を出す。**
|
|
7437
|
-
*
|
|
7438
|
-
* 一覧を読めなかった提供元・カタログを持たない提供元は並べない(選べるものが無い)。
|
|
7439
|
-
* **オン中の機能には印を付ける**(B-2: 外しても止まらないことが読めるように)。
|
|
7440
|
-
*/
|
|
7441
|
-
function harnessRenderPicker(box, setPickState, cat) {
|
|
7442
|
-
while (box.firstChild) box.removeChild(box.firstChild);
|
|
7443
|
-
var plan = harnessSectionPlan(cat);
|
|
7444
|
-
var rows = 0;
|
|
7445
|
-
for (var i = 0; i < plan.length; i++) {
|
|
7446
|
-
var group = plan[i];
|
|
7447
|
-
var inner = el("div", { class: "ds-harness-dep-body" });
|
|
7448
|
-
var groupRows = 0;
|
|
7449
|
-
for (var k = 0; k < group.harnesses.length; k++) {
|
|
7450
|
-
var hn = group.harnesses[k];
|
|
7451
|
-
var list = el("div", { class: "ds-harness-list" });
|
|
7452
|
-
var listRows = 0;
|
|
7453
|
-
for (var p = 0; p < cat.providers.length; p++) {
|
|
7454
|
-
var prov = cat.providers[p];
|
|
7455
|
-
if (prov.harness !== hn.harness) continue;
|
|
7456
|
-
for (var s = 0; s < prov.skills.length; s++) {
|
|
7457
|
-
list.appendChild(harnessPickRow(prov.skills[s]));
|
|
7458
|
-
listRows += 1;
|
|
7459
|
-
}
|
|
7460
|
-
}
|
|
7461
|
-
if (!listRows) continue;
|
|
7462
|
-
if (hn.label) inner.appendChild(el("h4", { class: "ds-harness-prov-title", text: hn.label }));
|
|
7463
|
-
inner.appendChild(list);
|
|
7464
|
-
groupRows += listRows;
|
|
7465
|
-
}
|
|
7466
|
-
if (!groupRows) continue;
|
|
7467
|
-
box.appendChild(el("div", { class: "ds-harness-dep" }, [
|
|
7468
|
-
el("h3", { class: "ds-harness-dep-title", text: group.label }),
|
|
7469
|
-
inner,
|
|
7470
|
-
]));
|
|
7471
|
-
rows += groupRows;
|
|
7472
|
-
}
|
|
7473
|
-
setPickState(rows ? "" : "いま選べる機能はありません。機能が追加されると、ここに並びます。");
|
|
7474
|
-
}
|
|
7475
|
-
|
|
7476
|
-
/** ピッカーの1行。**オン/オフは触らせない**(ここで決めるのは棚に置くかどうかだけ) */
|
|
7477
|
-
function harnessPickRow(meta) {
|
|
7478
|
-
var row = el("div", { class: "ds-harness-row" });
|
|
7479
|
-
var head = el("div", { class: "ds-harness-head" }, [
|
|
7480
|
-
el("div", { class: "ds-harness-name", text: meta.display_name }),
|
|
7481
|
-
el("div", { class: "ds-harness-period", text: harnessDefaultPeriod(meta) }),
|
|
7482
|
-
]);
|
|
7483
|
-
// **オン中であることは、この画面でも見えていないといけない。** 見えないまま
|
|
7484
|
-
// 「置いてある(外す)」を押すと、止まると受け取られる(B-2)
|
|
7485
|
-
if (harnessEntryOf(meta.harness, meta.skill)) {
|
|
7486
|
-
head.appendChild(el("span", { class: "ds-harness-onmark", text: "オン中" }));
|
|
7487
|
-
}
|
|
7488
|
-
row.appendChild(head);
|
|
7489
|
-
if (meta.summary) row.appendChild(el("div", { class: "ds-harness-summary", text: meta.summary }));
|
|
7490
|
-
var btn = el("button", { class: "ds-toggle-btn ds-harness-shelf-btn", type: "button" });
|
|
7491
|
-
var msg = el("div", { class: "ds-toggle-msg" });
|
|
7492
|
-
row.appendChild(el("div", { class: "ds-toggle" }, [btn, msg]));
|
|
7493
|
-
var paint = function () {
|
|
7494
|
-
// 動詞は棚ビューと揃える(追加/外す)。印は選択中であることを示すためだけに付ける
|
|
7495
|
-
btn.textContent = harnessShelfHas(meta.harness, meta.skill) ? "✓ 追加済み(外す)" : "+ 使う機能に追加";
|
|
7496
|
-
};
|
|
7497
|
-
btn.addEventListener("click", function () {
|
|
7498
|
-
harnessShelfToggle(meta.harness, meta.skill, meta.display_name, msg, btn, paint);
|
|
7499
|
-
});
|
|
7500
|
-
paint();
|
|
7501
|
-
return row;
|
|
7502
|
-
}
|
|
7503
|
-
|
|
7504
|
-
/**
|
|
7505
|
-
* ハーネス連携のモーダル。**開いたときに一覧を取りに行く**(カタログは提供側の
|
|
7506
|
-
* リポジトリが持つので、ビルド時には焼けない)。
|
|
7507
|
-
*
|
|
7508
|
-
* openCapabilityModal は使わない——items 機構にも noteOpenCap(能力ごとの登録の
|
|
7509
|
-
* 読み直し)にも乗らないため。閉じるボタンと showModal の作法だけ同じ形にしてある。
|
|
7510
|
-
*/
|
|
7511
|
-
var harnessGen = 0;
|
|
7512
|
-
function openHarnessModal() {
|
|
7513
|
-
var intake = SITE.intake;
|
|
7514
|
-
if (!intake) return;
|
|
7515
|
-
// 世代トークン。**応答が届いたときに、まだこの一覧が生きているかを判定する**
|
|
7516
|
-
//(押し替え・閉じたあとに親前提の DOM 操作をしない)
|
|
7517
|
-
harnessGen += 1;
|
|
7518
|
-
var gen = harnessGen;
|
|
7519
|
-
var m = makeModal("ハーネス連携", true);
|
|
7520
|
-
m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並ぶ機能をオンにすると、決まった時間に自動で動き、結果がこの案件の通知先チャンネル(「チャット」の通知先設定)に届きます。オンにするまで、何も動きません。" }));
|
|
7521
|
-
// 一覧の代わりに出す1行(読み込み中 → 0件/読み込めなかった)。**3状態を必ず区別する**
|
|
7522
|
-
var stateEl = el("p", { class: "ds-harness-state", text: "読み込み中…" });
|
|
7523
|
-
var warn = el("p", { class: "ds-harness-warn", text: "一部の提供元から一覧を読み込めませんでした。ここに並んでいない機能があるかもしれません。時間をおいて開き直してください。" });
|
|
7524
|
-
warn.hidden = true;
|
|
7525
|
-
var list = el("div", { class: "ds-harness-list" });
|
|
7526
|
-
// 「機能をえらぶ」(ピッカーへの入口)。**骨組みが読めたときだけ出す**
|
|
7527
|
-
// ——部署もハーネスの表示名も無いままピッカーを出すと、ただの長い一覧になる
|
|
7528
|
-
var choose = el("button", { class: "ds-toggle-btn ds-harness-choose", type: "button", text: "機能をえらぶ" });
|
|
7529
|
-
choose.hidden = true;
|
|
7530
|
-
// 棚ビューは1つの箱にまとめる(ピッカーと**丸ごと**入れ替えるため)
|
|
7531
|
-
var shelfView = el("div", { class: "ds-harness-shelf" }, [
|
|
7532
|
-
stateEl,
|
|
7533
|
-
warn,
|
|
7534
|
-
list,
|
|
7535
|
-
el("div", { class: "ds-harness-actions" }, [choose]),
|
|
7536
|
-
]);
|
|
7537
|
-
m.modal.appendChild(shelfView);
|
|
7538
|
-
// ピッカーは**同じモーダルの中のもう1つのビュー**(重ねない)。
|
|
7539
|
-
// 🔴 **hidden で切り替える。** 一覧を DOM から外すと alive() が false になり、
|
|
7540
|
-
// 遅れて届いた応答が捨てられて「読み込み中…」のまま固まる(設計 B-3)。
|
|
7541
|
-
var pickState = el("p", { class: "ds-harness-pick-state" });
|
|
7542
|
-
pickState.hidden = true;
|
|
7543
|
-
var pickBody = el("div", { class: "ds-harness-pick-list" });
|
|
7544
|
-
var back = el("button", { class: "ds-toggle-btn ds-harness-back", type: "button", text: "← 戻る" });
|
|
7545
|
-
var picker = el("div", { class: "ds-harness-picker" }, [
|
|
7546
|
-
el("div", { class: "ds-harness-actions" }, [back]),
|
|
7547
|
-
el("p", { class: "modal-desc", text: "この案件で使う機能を選びます。追加しただけでは動きません(動かすには、戻ってオンにしてください)。" }),
|
|
7548
|
-
pickState,
|
|
7549
|
-
pickBody,
|
|
7550
|
-
]);
|
|
7551
|
-
picker.hidden = true;
|
|
7552
|
-
m.modal.appendChild(picker);
|
|
7553
|
-
m.modal.appendChild(el("div", { class: "modal-actions" }, [
|
|
7554
|
-
el("button", { class: "ds-toggle-btn", type: "button", text: "閉じる" }),
|
|
7555
|
-
]));
|
|
7556
|
-
m.modal.querySelector(".modal-actions button").addEventListener("click", function () { m.close(); });
|
|
7557
|
-
m.overlay.appendChild(m.modal);
|
|
7558
|
-
showModal(m.overlay, m.modal);
|
|
7559
|
-
|
|
7560
|
-
/** 生きている一覧か(閉じた・押し替えた後は何もしない) */
|
|
7561
|
-
var alive = function () { return gen === harnessGen && app.contains(list); };
|
|
7562
|
-
/**
|
|
7563
|
-
* 状態の1行を差し替える。**節点は残して文言を空にする**(消すと、この一文を
|
|
7564
|
-
* 探している検査が「文言が変わった」と「箱ごと無くなった」を区別できない)
|
|
7565
|
-
*/
|
|
7566
|
-
var setState = function (text, isError) {
|
|
7567
|
-
stateEl.className = "ds-harness-state" + (isError ? " is-error" : "");
|
|
7568
|
-
stateEl.textContent = text;
|
|
7569
|
-
stateEl.hidden = text === "";
|
|
7570
|
-
};
|
|
7571
|
-
var fail = function () {
|
|
7572
|
-
if (!alive()) return;
|
|
7573
|
-
setState("利用できる機能の一覧を読み込めませんでした。時間をおいて開き直してください。", true);
|
|
7574
|
-
};
|
|
7575
|
-
// **fetch を持たない環境(JSDOM 素)では取りに行かない。** 素で呼ぶと
|
|
7576
|
-
// ReferenceError になり、タイルを押しただけで画面が落ちる(liveConfirmPossible と同形)
|
|
7577
|
-
if (typeof fetch !== "function") { fail(); return; }
|
|
7578
|
-
var ctrl = typeof AbortController === "function" ? new AbortController() : null;
|
|
7579
|
-
// 見切りは op=state と同じ10秒。**新しいリテラルを作らない**——値が割れると
|
|
7580
|
-
// 「10秒で戻る」検査が通ったまま実機では戻らなくなる
|
|
7581
|
-
var giveUp = ctrl ? setTimeout(function () { ctrl.abort(); }, LIVE_CONFIRM_TIMEOUT_MS) : null;
|
|
7582
|
-
// **成功・失敗の両方で外す。** 残すと、window を閉じないテストで node --test が終わらない
|
|
7583
|
-
var clearGiveUp = function () { if (giveUp != null) { clearTimeout(giveUp); giveUp = null; } };
|
|
7584
|
-
// GET で取る。**カスタムヘッダは付けない**(preflight を1往復増やさないため)。
|
|
7585
|
-
// 名乗り券は body に載せる仕組みなので、GET のこの op には付かない(#349)
|
|
7586
|
-
callIntake("harness-catalog", null, { method: "GET", signal: ctrl ? ctrl.signal : undefined })
|
|
7587
|
-
.then(function (res) {
|
|
7588
|
-
clearGiveUp();
|
|
7589
|
-
if (!res.ok) throw new Error("http " + res.status);
|
|
7590
|
-
return res.json();
|
|
7591
|
-
})
|
|
7592
|
-
.then(function (j) {
|
|
7593
|
-
if (!alive()) return;
|
|
7594
|
-
var cat = harnessCatalogOf(j);
|
|
7595
|
-
if (!cat) { fail(); return; }
|
|
7596
|
-
// 骨組みを持たない応答(古い Lambda)では、従来どおりのフラット表示に落とす。
|
|
7597
|
-
// **ピッカーの入口も出さない**——選んでも並べ替える先が無い
|
|
7598
|
-
if (!cat.departments) { harnessFillList(list, setState, warn, cat); return; }
|
|
7599
|
-
warn.hidden = !cat.partial;
|
|
7600
|
-
choose.hidden = false;
|
|
7601
|
-
var setPickState = function (text) {
|
|
7602
|
-
pickState.textContent = text;
|
|
7603
|
-
pickState.hidden = text === "";
|
|
7604
|
-
};
|
|
7605
|
-
/**
|
|
7606
|
-
* 棚ビューへ戻る。**そのつど描き直す**——ピッカーで置いた/外したぶんを
|
|
7607
|
-
* 反映するため(行ごと作り直すので、棚ビューに出ていた保存の印は消える。
|
|
7608
|
-
* ピッカーを一往復した後なので、消えて困る情報はもう画面に無い)
|
|
7609
|
-
*/
|
|
7610
|
-
var showShelf = function () {
|
|
7611
|
-
harnessRenderShelf(list, setState, cat);
|
|
7612
|
-
picker.hidden = true;
|
|
7613
|
-
shelfView.hidden = false;
|
|
7614
|
-
};
|
|
7615
|
-
var showPicker = function () {
|
|
7616
|
-
harnessRenderPicker(pickBody, setPickState, cat);
|
|
7617
|
-
shelfView.hidden = true;
|
|
7618
|
-
picker.hidden = false;
|
|
7619
|
-
};
|
|
7620
|
-
choose.addEventListener("click", showPicker);
|
|
7621
|
-
back.addEventListener("click", showShelf);
|
|
7622
|
-
showShelf();
|
|
7623
|
-
})
|
|
7624
|
-
// **必ず末尾に置く。** 上の .then の中で投げた例外まで拾う——未処理の rejection は
|
|
7625
|
-
// node --test をテストごと落とす
|
|
7626
|
-
.catch(function () { clearGiveUp(); fail(); });
|
|
7627
|
-
}
|
|
7628
|
-
|
|
7629
6704
|
/**
|
|
7630
6705
|
* 鍵の欄の置き場(2026-08-21・オーナー指摘「APIキーのセクションが下にあって、
|
|
7631
6706
|
* 変わってないように見えます」)。
|
|
@@ -7722,7 +6797,8 @@ ${deriveCapTile.toString()}
|
|
|
7722
6797
|
// 「これから繋げる入口」も接続マップのアンカーの受け皿も無くなる。
|
|
7723
6798
|
// 何を繋げるかは製品が決めていて、案件データでは変わらない(#125②)。
|
|
7724
6799
|
// **ハーネス連携は末尾。** 前6つは「どこから情報が入ってくるか」で、これだけが
|
|
7725
|
-
// 「Cortex
|
|
6800
|
+
// 「Cortex に何をさせられるか」(ポータルのスキルカタログへの導線)——
|
|
6801
|
+
// 読む順として、入口を見た後に出口が来る
|
|
7726
6802
|
var CAP_ORDER = ["課題管理", "会議", "共有資料", "チャット", "デザイン", "開発", "ハーネス連携"];
|
|
7727
6803
|
// internalSources に行が来る能力(チャットと開発は externalSources 側にしか来ない)。
|
|
7728
6804
|
// **fleet-status.json がまだ無い案件のために、行が無くても設定を出す**判定に使う(#138)
|
|
@@ -8202,7 +7278,7 @@ ${deriveCapTile.toString()}
|
|
|
8202
7278
|
"チャット": { icon: "💬", desc: "チャンネルの会話をGoldの材料に" },
|
|
8203
7279
|
"デザイン": { icon: "🎨", desc: "画面インベントリの同期" },
|
|
8204
7280
|
"開発": { icon: "🐙", desc: "IssuesをGoldの材料に" },
|
|
8205
|
-
"ハーネス連携": { icon: "
|
|
7281
|
+
"ハーネス連携": { icon: "🧰", desc: "スキルカタログを開く(Claude Code で使う/定期実行にする)" },
|
|
8206
7282
|
};
|
|
8207
7283
|
// 説明文がアダプタで割れるものだけ上書きする(#153)。チャットの単位は
|
|
8208
7284
|
// Slack=チャンネル / Google Chat=スペース で、能力共通の一文だと後者に嘘が出る
|
|
@@ -8399,11 +7475,11 @@ ${deriveCapTile.toString()}
|
|
|
8399
7475
|
var extraCaps = Object.keys(capBox).filter(function (c) { return CAP_ORDER.indexOf(c) === -1; });
|
|
8400
7476
|
var anyCap = false;
|
|
8401
7477
|
CAP_ORDER.concat(extraCaps).forEach(function (cap) {
|
|
8402
|
-
// **ハーネス連携だけは専用の描画。**
|
|
8403
|
-
//
|
|
7478
|
+
// **ハーネス連携だけは専用の描画。** 接続ではなくポータルのハーネス画面への
|
|
7479
|
+
// 外部リンクなので、状態(capabilityState)・アダプタ・fleet の行のどれも持たない
|
|
8404
7480
|
if (cap === "ハーネス連携") {
|
|
8405
|
-
//
|
|
8406
|
-
//
|
|
7481
|
+
// **案件キーの正本は SITE.intake.projectKey だけ。** 無い構成ではリンクが
|
|
7482
|
+
// 組めないので出さない(renderExternalAdders と同じゲート)
|
|
8407
7483
|
if (!SITE.intake) return;
|
|
8408
7484
|
sec.appendChild(el("div", { class: "ds-cap", text: cap }));
|
|
8409
7485
|
// ここで anyCap を立てても「ハーネス連携だけで節が立つ」ことは無い——
|
|
@@ -8607,44 +7683,42 @@ ${deriveCapTile.toString()}
|
|
|
8607
7683
|
}
|
|
8608
7684
|
|
|
8609
7685
|
/**
|
|
8610
|
-
*
|
|
8611
|
-
*
|
|
8612
|
-
*
|
|
8613
|
-
*
|
|
8614
|
-
*
|
|
7686
|
+
* ハーネス連携のタイル。**押すとポータルのハーネス画面を別タブで開くリンク。**
|
|
7687
|
+
*
|
|
7688
|
+
* ビューアの中に棚(案件ごとに使うスキルの一覧)と定期実行のオン/オフを持っていたが、
|
|
7689
|
+
* スキルの一覧はポータルが正本になり、定期実行は claude.ai の Routines に
|
|
7690
|
+
* 一本化した(#369)。ここに残すのは「どこへ行けば見られるか」の1本の導線だけで、
|
|
7691
|
+
* 件数も状態も語らない——**語れる正本がビューア側に無くなった**ため。
|
|
7692
|
+
*
|
|
7693
|
+
* 🔴 **<button> ではなく <a target="_blank"> で作る。** 行き先が同じ画面の
|
|
7694
|
+
* モーダルではなく外部のページなので、リンクの形にしないと「新しいタブで開く」
|
|
7695
|
+
* 「URL をコピーする」が読み手の側からできない。
|
|
8615
7696
|
*/
|
|
8616
7697
|
function renderHarnessTile() {
|
|
8617
7698
|
var meta = CAP_META["ハーネス連携"];
|
|
8618
|
-
//
|
|
8619
|
-
//
|
|
8620
|
-
//
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
}
|
|
7699
|
+
// 🔴 **案件キーは必ず encodeURIComponent を通す。** ポータルは ?project= の値で
|
|
7700
|
+
// 案件を引くので、記号を含む鍵をそのまま繋ぐと壊れ方が2通り出る——
|
|
7701
|
+
// アンパサンドはそこから先が**別のクエリ引数**になって鍵が途中で切れ、
|
|
7702
|
+
// スラッシュや空白は**鍵の文字列そのものが変わる**(別の案件を開く/案件が見つからない)
|
|
7703
|
+
var href = "https://cortex.classmethod.info/harness?project="
|
|
7704
|
+
+ encodeURIComponent(SITE.intake.projectKey);
|
|
8625
7705
|
var body = el("div", { class: "ds-tile-body" }, [
|
|
8626
7706
|
el("div", { class: "ds-tile-name", text: "ハーネス連携" }),
|
|
8627
7707
|
el("div", { class: "ds-tile-desc", text: meta.desc }),
|
|
8628
|
-
el("div", {
|
|
8629
|
-
class: "ds-tile-sub " + (n ? "ds-gate-ok" : "ds-gate-muted"),
|
|
8630
|
-
text: n ? "● " + n + "件をオンにしています" : "すべてオフ",
|
|
8631
|
-
}),
|
|
7708
|
+
el("div", { class: "ds-tile-sub ds-gate-muted", text: "ポータルで開く(別タブ)" }),
|
|
8632
7709
|
]);
|
|
8633
|
-
//
|
|
8634
|
-
//
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
text: "⚠ オンになっていますが、配信先が未設定です",
|
|
8640
|
-
}));
|
|
8641
|
-
}
|
|
8642
|
-
var tile = el("button", { class: "ds-tile", type: "button", id: "ds-ハーネス連携" }, [
|
|
7710
|
+
// **id(ds-ハーネス連携)は残す。** 能力の見出し(.ds-cap)を立てたなら
|
|
7711
|
+
// 対応する ds-<能力> の要素があること、を capability-modal の守りが確かめている
|
|
7712
|
+
var tile = el("a", {
|
|
7713
|
+
class: "ds-tile", id: "ds-ハーネス連携",
|
|
7714
|
+
href: href, target: "_blank", rel: "noopener",
|
|
7715
|
+
}, [
|
|
8643
7716
|
el("span", { class: "ds-tile-logo", text: meta.icon }),
|
|
8644
7717
|
body,
|
|
8645
|
-
|
|
7718
|
+
// **矢印は ↗。** 他のタイルの › は「同じ画面でモーダルが開く」印なので、
|
|
7719
|
+
// 同じ記号のままだと、行き先が変わったことが押す前に分からない
|
|
7720
|
+
el("span", { class: "ds-tile-chev", text: "↗" }),
|
|
8646
7721
|
]);
|
|
8647
|
-
tile.addEventListener("click", function () { openHarnessModal(); });
|
|
8648
7722
|
sec.appendChild(tile);
|
|
8649
7723
|
}
|
|
8650
7724
|
|
|
@@ -10383,6 +9457,48 @@ ${deriveCapTile.toString()}
|
|
|
10383
9457
|
};
|
|
10384
9458
|
}
|
|
10385
9459
|
|
|
9460
|
+
/**
|
|
9461
|
+
* 反映待ちのオーバーレイ+スピナー(#378・共通部品)。
|
|
9462
|
+
*
|
|
9463
|
+
* host の**上に**半透明の白(60%)とぐるぐるを重ねる。host の大きさは変えない。
|
|
9464
|
+
* 角丸は host から継ぐので、丸(案件アイコン)でも四角(操作タブのカード・ボタン)でも
|
|
9465
|
+
* 使える。beginBusy が「語を差し替える」形なのに対し、こちらは**語を置けない場所
|
|
9466
|
+
* (画像だけの丸など)**向け。
|
|
9467
|
+
*
|
|
9468
|
+
* **載せ先が static でも壊れない。** オーバーレイは absolute なので、載せ先に
|
|
9469
|
+
* position が無いと**別の祖先いっぱいに広がる**(黙って壊れる)。その場合だけ
|
|
9470
|
+
* relative を入れ、外すときに戻す(案件アイコンの丸は元から relative なので触らない)。
|
|
9471
|
+
*
|
|
9472
|
+
* 支援技術には host の aria-busy で「処理中」を伝える。label は目に見えない語として
|
|
9473
|
+
* 置くだけ(挿入と同時に中身を入れたライブリージョンは読まれないことが多い)。
|
|
9474
|
+
*
|
|
9475
|
+
* @returns 外す関数。beginBusy と同じく**何度呼んでも1回しか効かない**
|
|
9476
|
+
*/
|
|
9477
|
+
function beginOverlaySpinner(host, label) {
|
|
9478
|
+
if (!host) return function () {};
|
|
9479
|
+
var madeRelative = false;
|
|
9480
|
+
try {
|
|
9481
|
+
if (window.getComputedStyle(host).position === "static") {
|
|
9482
|
+
host.style.position = "relative";
|
|
9483
|
+
madeRelative = true;
|
|
9484
|
+
}
|
|
9485
|
+
} catch (e) {}
|
|
9486
|
+
var ov = el("span", { class: "busy-overlay" }, [
|
|
9487
|
+
el("span", { class: "busy-spinner", "aria-hidden": "true" }),
|
|
9488
|
+
el("span", { class: "busy-label", text: label || "処理中…" }),
|
|
9489
|
+
]);
|
|
9490
|
+
host.appendChild(ov);
|
|
9491
|
+
host.setAttribute("aria-busy", "true");
|
|
9492
|
+
var used = false;
|
|
9493
|
+
return function endOverlaySpinner() {
|
|
9494
|
+
if (used) return;
|
|
9495
|
+
used = true;
|
|
9496
|
+
if (ov.parentNode) ov.parentNode.removeChild(ov);
|
|
9497
|
+
host.removeAttribute("aria-busy");
|
|
9498
|
+
if (madeRelative) host.style.position = "";
|
|
9499
|
+
};
|
|
9500
|
+
}
|
|
9501
|
+
|
|
10386
9502
|
/**
|
|
10387
9503
|
* サーバへ設定変更を送る。**一覧は書き換えない**
|
|
10388
9504
|
* (接続状況は夜間バッチのスナップショットなので、「もう効いている」と誤解させないため)。
|
|
@@ -10501,8 +9617,7 @@ ${deriveCapTile.toString()}
|
|
|
10501
9617
|
// **何も変わっていないなら「追加した」と見せない。** 既に同じものが入っていた場合
|
|
10502
9618
|
// サーバは 200 + changed:false を返す。ここを res.ok だけで判定すると
|
|
10503
9619
|
// 「変更はありません」と言いながら追加した行が出る
|
|
10504
|
-
// 🔴 **応答そのものを渡す(j)。**
|
|
10505
|
-
//(ハーネスの entry: 既定を解決した後の時間帯・曜日)では、UI が既定を写経せずに
|
|
9620
|
+
// 🔴 **応答そのものを渡す(j)。** サーバが保存後の値を添えてくる口では、UI が既定を写経せずに
|
|
10506
9621
|
// 正しく反映できる。**流れるのは res.ok かつ changed !== false のときだけ**で、
|
|
10507
9622
|
// removeButton 経由の onDone(引数を取らない)には届かない——受け側は
|
|
10508
9623
|
// j が undefined でも動く形に保つこと(既存の4呼び出しは引数を取らない)
|
|
@@ -10572,7 +9687,7 @@ ${deriveCapTile.toString()}
|
|
|
10572
9687
|
* @param opts { okLabel, returnTo, okDanger, body }
|
|
10573
9688
|
* okDanger … OK ボタンを赤で出すか(既定 true)。false は「広げる向き」の確認用
|
|
10574
9689
|
* body … lines の後・ボタンの前に挿す DOM 節(任意)。**押す前に決めることがある確認**
|
|
10575
|
-
*
|
|
9690
|
+
* のための口(いまこの口を使っている呼び出しは無い)。
|
|
10576
9691
|
* 無指定なら従来と完全に同じ見た目・同じ挙動(節の中の操作は overlay の
|
|
10577
9692
|
* 外側クリック判定=e.target === overlay に掛からないので、素通しでよい)
|
|
10578
9693
|
*/
|
|
@@ -10597,8 +9712,8 @@ ${deriveCapTile.toString()}
|
|
|
10597
9712
|
if (o.body) box.appendChild(o.body);
|
|
10598
9713
|
var cancel = el("button", { class: "modal-cancel", type: "button", text: "キャンセル" });
|
|
10599
9714
|
// **既定は赤(ds-danger)。** ここに来る操作はほぼ全部が取り消せない向きなので、
|
|
10600
|
-
// 既定を変えない。**okDanger: false
|
|
10601
|
-
//
|
|
9715
|
+
// 既定を変えない。**okDanger: false のときだけ通常の見た目**——「広げる向き」の
|
|
9716
|
+
// 操作を削除と同じ赤で出すと、止めるべき操作に見える
|
|
10602
9717
|
var ok = el("button", { class: "ds-toggle-btn" + (o.okDanger === false ? "" : " ds-danger"), type: "button", text: o.okLabel || "はい" });
|
|
10603
9718
|
box.appendChild(el("div", { class: "modal-actions" }, [cancel, ok]));
|
|
10604
9719
|
overlay.appendChild(box);
|
|
@@ -14489,6 +13604,20 @@ ${deriveCapTile.toString()}
|
|
|
14489
13604
|
}
|
|
14490
13605
|
|
|
14491
13606
|
window.addEventListener("popstate", route);
|
|
13607
|
+
/**
|
|
13608
|
+
* ヘッダーの案件名(#379)。**ポータル配下(/p/<key>/ の下)で開いているときだけ**、
|
|
13609
|
+
* ポータルの案件一覧(同一オリジンの /)へ戻るリンクにする。
|
|
13610
|
+
* Basic 認証の Amplify で開いているときは戻る先が無いので、これまでどおり(href="?")。
|
|
13611
|
+
* 文言は増やさない——案件名そのものがリンクで、説明は title 属性だけ。
|
|
13612
|
+
* 丸(アイコン操作)とは別の口なので、renderSiteIcon には触らない
|
|
13613
|
+
*/
|
|
13614
|
+
(function linkTitleToPortal() {
|
|
13615
|
+
var a = document.getElementById("home-link");
|
|
13616
|
+
if (!a || !/^\\/p\\/[^/]+\\//.test(location.pathname)) return;
|
|
13617
|
+
a.setAttribute("href", "/");
|
|
13618
|
+
a.setAttribute("title", "案件一覧へ");
|
|
13619
|
+
a.classList.add("site-title-portal");
|
|
13620
|
+
})();
|
|
14492
13621
|
// ヘッダーの案件アイコン(#348)は #app の外なので、**起動時に1回だけ描く**。
|
|
14493
13622
|
// route() はタブを替えるたびに走るが、ヘッダーは描き替えない——描き直すと
|
|
14494
13623
|
// 保存直後の mirror とメニューの開閉が、タブを移るたびに畳まれる
|