@quolu/lattice 0.49.0 → 0.50.0

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/README.ja.md CHANGED
@@ -51,7 +51,7 @@ planを再コンパイルします。共有面が共有でなくなるので、
51
51
  `design_memo`を必ず持ちます。空欄やファイル参照だけでは登録できません。本当に何も考えていない場合だけ、
52
52
  正確なsentinel `NO_PLAN`を本文にします。Latticeはauthoring時に次の問いを返し、空欄のまま通しません。
53
53
 
54
- > If you have not thought through this ToDo, write exactly `NO_PLAN` in its design memo.
54
+ > あなたがこのToDoに対して、何も考えていないならば、設計メモに `NO_PLAN` と書いてください
55
55
 
56
56
  通常の`lattice todo show`と、成功するすべての`lattice todo start`は、この初期設計メモを自動で返します。
57
57
  作業開始後に増えた方針、棄却案、調査結果、注意、未解決事項は、別のappend-only note chainへ追記できます。
@@ -65,7 +65,7 @@ lattice todo show --plan <key> --task <id> --json
65
65
 
66
66
  動的工程表は、選択したToDoの右ペインへ初期設計メモと追記noteの本文を表示します。あわせて前提工程・
67
67
  後続工程それぞれの状態(未着手/作業中/完了/ブロック中)と、その工程の並列可否を示します。
68
- note本文を落とすのはrepo外へHTMLを出す公開配信面だけです。project別HTMLの生成や再生成は不要です。
68
+ note本文はHTMLを描くすべての面へ載ります。project別HTMLの生成や再生成は不要です。
69
69
 
70
70
  ### 変換の受入五条件
71
71
 
@@ -102,7 +102,7 @@ MarkItDownは別区分の第三者CLIです。
102
102
  [docs/00_product-contract.md](docs/00_product-contract.md)を参照してください。
103
103
 
104
104
  CLIの全体像は`lattice --help`、各公開namespaceの正規構文は
105
- `lattice <plan|run|event|todo|sensor|factory-diagnostics|runtime-errors|bridge> --help`で確認できます。
105
+ `lattice <plan|run|event|todo|sensor|factory-diagnostics|runtime-errors|bridge|hooks> --help`で確認できます。
106
106
  個別操作は`lattice <namespace> <subcommand> --help`または`lattice help <namespace> <subcommand>`で
107
107
  正規optionをstore非依存に確認できます。
108
108
 
@@ -126,7 +126,8 @@ codex-sidecar diagnostics --project . --preset auditor --json
126
126
  未初期化projectで`sensor sync`した場合は`LATTICE_SENSOR_NOT_INITIALIZED`と正規`next_action`を返します。
127
127
  その他のsensor失敗もexit code、signal、bounded stderrをtyped detailへ残し、原因を隠しません。
128
128
 
129
- Node.js 22.13以上を使用します。境界観測は配布物に同梱したLattice sensorだけを使い、PATH上の
129
+ Node.js 22.13以上、ただし25.xを除きます(`engines: >=22.13 <25 || >=26`。Node 25.xはV8 turboshaft
130
+ WASM JITの不具合で同梱sensorが壊れるため、bannerを出して起動を止めます)。境界観測は配布物に同梱したLattice sensorだけを使い、PATH上の
130
131
  廃止済みruntimeや旧cache/dataへfallbackしません。Spotterはproject単位で生成stateの所有境界を守ります。
131
132
 
132
133
  どのrepoでも、Latticeの導入状態はdirectoryの有無を推測せず、最初に次のtyped discoveryで判定します。
@@ -136,12 +137,12 @@ lattice status --json
136
137
  ```
137
138
 
138
139
  `state`は`uninitialized | ready | active_run | invalid`のいずれかです。`uninitialized`は
139
- 正常な未初期化状態で、`next_action`が正規の初期authoring入口を返します。初回planは
140
- 新規planはPhase監査とToDo schedulingを分離し、全ToDoに設計メモを持つ`lattice.plan_create_input.v4`のcanonical
141
- JSON+LFを用意し、次で作成します。既存v2/v4は互換契約として維持されます。
140
+ 正常な未初期化状態で、`next_action`が正規の初期authoring入口を返します。新規planはPhase監査と
141
+ ToDo schedulingを分離し、全ToDoに設計メモを持つ`lattice.plan_create_input.v4`のcanonical
142
+ JSON+LFを用意し、次で作成します。既存のv1〜v3は互換契約として維持され、`--schema-version`で取得できます。
142
143
 
143
144
  ```bash
144
- lattice plan create --schema-version 4 --json
145
+ lattice plan create --schema-version 4 --json # v4のJSON Schemaを取る(作成はしない)
145
146
  ```
146
147
 
147
148
  ```bash
@@ -303,8 +304,9 @@ daemonの生死はdescriptor 1枚では持ちません。daemonは自分のpid
303
304
  配信を続けているdaemonを引き取ります。signalを送るのはその場で再認証を通った相手だけで、応答しない
304
305
  pidへは送りません(pid再利用で無関係のprocessを止めうるためです)。規約は
305
306
  [ADR 0157](docs/adr/0157-dashboard-daemons-are-discoverable-by-record.md)が正です。
306
- 状態を書き込む`start / block / unblock / done / evidence promote / reopen / revise / revise-phase / revise-set`
307
- では、監査actorとして次の3環境変数をすべて設定してください。
307
+ 状態を書き込む`start / block / unblock / done / evidence promote / reopen / note / independence mode /
308
+ revise / revise-phase / revise-set / phase review|accept|reject|reopen|close-unaudited|baseline /
309
+ dashboard adopt`では、監査actorとして次の3環境変数をすべて設定してください。
308
310
 
309
311
  ```bash
310
312
  export LATTICE_TODO_ACTOR_HOST=<host-id>
package/README.md CHANGED
@@ -78,8 +78,8 @@ lattice todo show --plan <key> --task <id> --json
78
78
 
79
79
  The selected ToDo's detail pane shows the initial design memo and the append-only note bodies, plus the
80
80
  status of each prerequisite and dependent ToDo (pending, in progress, done, blocked) and whether the ToDo
81
- can run in parallel. Only a public distribution surface HTML served outside the repository — drops note
82
- bodies. Static per-project HTML is not generated; `lattice todo gantt serve` and the shared dashboard
81
+ can run in parallel. Note bodies are carried by every rendered surface, including HTML served outside the
82
+ repository. Static per-project HTML is not generated; `lattice todo gantt serve` and the shared dashboard
83
83
  read the store dynamically.
84
84
 
85
85
  ### The five acceptance conditions
@@ -120,7 +120,8 @@ the operating AI.
120
120
  npm install -g @quolu/lattice
121
121
  ```
122
122
 
123
- Requires **Node.js 22.13+**. The structure sensor ships inside the package there is nothing
123
+ Requires **Node.js 22.13 or newer, except 25.x** (`engines: >=22.13 <25 || >=26`; Node 25's V8
124
+ turboshaft WASM JIT breaks the bundled sensor, so it is blocked with a banner). The structure sensor ships inside the package — there is nothing
124
125
  else to install, and Lattice never falls back to a sensor on your `PATH`.
125
126
 
126
127
  ## Quick start
@@ -156,7 +157,7 @@ lattice todo seam-proposal land --plan <key> --names names.json
156
157
  ```
157
158
 
158
159
  Full CLI surface: `lattice --help`, then
159
- `lattice <plan|run|event|todo|sensor|bridge|runtime-errors> --help`.
160
+ `lattice <plan|run|event|todo|sensor|factory-diagnostics|runtime-errors|bridge|hooks> --help`.
160
161
 
161
162
  ## Design principles
162
163
 
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { readTodoStoreStable } from '../src/todo-store.mjs';
7
7
  import { TODO_STATUS_DISPATCH_ONLY, projectTodoStatus } from '../src/todo-status.mjs';
8
8
  import { ganttLiveHeadDigest, renderTodoGanttForProject } from '../src/todo-cli.mjs';
9
+ import { readProjectExternalPane } from '../src/project-identity.mjs';
9
10
  import {
10
11
  forgetTodoDashboardDaemonRecord,
11
12
  readVisibleTodoDashboardProjects,
@@ -96,6 +97,10 @@ async function synchronize() {
96
97
  return {
97
98
  html: result.rendered.html,
98
99
  head_digest: await ganttLiveHeadDigest({ repoRoot: entry.repo_root, store }),
100
+ // 配信のたびに読む。外部ペインを差した/外した側にdaemon再起動を要求しない。
101
+ external_pane: await readProjectExternalPane({
102
+ repoRoot: entry.repo_root, projectId: entry.project_id,
103
+ }),
99
104
  };
100
105
  },
101
106
  readHead: async () => ganttLiveHeadDigest({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Schedulability compiler for multi-agent development: observe real code boundaries, refactor the conflicting seam, recompile the plan for parallel execution",
5
5
  "author": {
6
6
  "name": "Quo / クオ at kitepon.dev",
@@ -4,6 +4,9 @@ import { parseTree } from 'jsonc-parser';
4
4
 
5
5
  export const PROJECT_IDENTITY_REF = '.lattice/project.json';
6
6
  const MAX_BYTES = 65_536;
7
+ const URL_MAX_BYTES = 2_048;
8
+ const IDENTITY_KEYS = 'display_name,project_id,schema';
9
+ const IDENTITY_KEYS_WITH_PANE = 'display_name,external_pane,project_id,schema';
7
10
  const CONTROL = /[\u0000-\u001f\u007f]/u;
8
11
 
9
12
  function fail(reason) {
@@ -30,23 +33,45 @@ function duplicateKey(node) {
30
33
  return false;
31
34
  }
32
35
 
33
- export async function resolveProjectIdentity({ repoRoot, projectId, env = process.env }) {
34
- if (!path.isAbsolute(repoRoot) || typeof projectId !== 'string' || projectId.length === 0
35
- || env === null || typeof env !== 'object' || Array.isArray(env)) {
36
- throw new TypeError('project identity options invalid');
37
- }
38
- const override = env.LATTICE_PROJECT_DISPLAY_NAME;
39
- if (override !== undefined) {
40
- if (!displayName(override)) fail('environment_display_name_invalid');
41
- return Object.freeze({ projectId, displayName: override, source: 'environment' });
36
+ /** 外部ペインのURLは、閲覧者のブラウザが実際に取りに行く先である。絶対URLだけを通す。 */
37
+ function paneUrl(value) {
38
+ if (typeof value !== 'string' || value.length === 0 || CONTROL.test(value)
39
+ || Buffer.byteLength(value, 'utf8') > URL_MAX_BYTES) return null;
40
+ let url;
41
+ try { url = new URL(value); } catch { return null; }
42
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') return null;
43
+ return url;
44
+ }
45
+
46
+ /**
47
+ * projectごとに1枚だけ差せる外部ペインの設定。Latticeは中身が何のサービスかを知らない
48
+ * ——題名・埋め込み先URL・生存probe URLの3つだけを受け取る汎用の口である。
49
+ */
50
+ function externalPane(value) {
51
+ if (value === null || typeof value !== 'object' || Array.isArray(value)
52
+ || Object.keys(value).sort().join(',') !== 'probe_url,title,url') {
53
+ fail('external_pane_schema_invalid');
42
54
  }
55
+ if (!displayName(value.title)) fail('external_pane_title_invalid');
56
+ const url = paneUrl(value.url);
57
+ const probe = paneUrl(value.probe_url);
58
+ if (url === null) fail('external_pane_url_invalid');
59
+ if (probe === null) fail('external_pane_probe_url_invalid');
60
+ return Object.freeze({
61
+ title: value.title,
62
+ url: url.href,
63
+ probeUrl: probe.href,
64
+ frameOrigin: url.origin,
65
+ probeOrigin: probe.origin,
66
+ });
67
+ }
68
+
69
+ async function readIdentityDocument({ repoRoot, projectId }) {
43
70
  const canonicalRoot = await realpath(repoRoot);
44
71
  const ref = path.join(canonicalRoot, PROJECT_IDENTITY_REF);
45
72
  let stats;
46
73
  try { stats = await lstat(ref); } catch (error) {
47
- if (error?.code === 'ENOENT') {
48
- return Object.freeze({ projectId, displayName: projectId, source: 'project_id' });
49
- }
74
+ if (error?.code === 'ENOENT') return null;
50
75
  throw error;
51
76
  }
52
77
  if (!stats.isFile() || stats.isSymbolicLink() || stats.size > MAX_BYTES) fail('identity_file_unsafe');
@@ -61,11 +86,45 @@ export async function resolveProjectIdentity({ repoRoot, projectId, env = proces
61
86
  if (errors.length > 0 || tree === undefined || duplicateKey(tree)) fail('identity_json_invalid');
62
87
  let document;
63
88
  try { document = JSON.parse(text); } catch { fail('identity_json_invalid'); }
64
- if (document === null || typeof document !== 'object' || Array.isArray(document)
65
- || Object.keys(document).sort().join(',') !== 'display_name,project_id,schema'
89
+ const keys = document === null || typeof document !== 'object' || Array.isArray(document)
90
+ ? '' : Object.keys(document).sort().join(',');
91
+ if ((keys !== IDENTITY_KEYS && keys !== IDENTITY_KEYS_WITH_PANE)
66
92
  || document.schema !== 'lattice.project_identity.v1'
67
93
  || document.project_id !== projectId || !displayName(document.display_name)) {
68
94
  fail('identity_schema_invalid');
69
95
  }
96
+ return document;
97
+ }
98
+
99
+ export async function resolveProjectIdentity({ repoRoot, projectId, env = process.env }) {
100
+ if (!path.isAbsolute(repoRoot) || typeof projectId !== 'string' || projectId.length === 0
101
+ || env === null || typeof env !== 'object' || Array.isArray(env)) {
102
+ throw new TypeError('project identity options invalid');
103
+ }
104
+ const override = env.LATTICE_PROJECT_DISPLAY_NAME;
105
+ if (override !== undefined) {
106
+ if (!displayName(override)) fail('environment_display_name_invalid');
107
+ return Object.freeze({ projectId, displayName: override, source: 'environment' });
108
+ }
109
+ const document = await readIdentityDocument({ repoRoot, projectId });
110
+ if (document === null) {
111
+ return Object.freeze({ projectId, displayName: projectId, source: 'project_id' });
112
+ }
113
+ // external_paneは表示名の出典ではない。ここでは形の妥当性だけ確かめて落とす
114
+ // ——壊れた設定を黙って無視すると、差したはずのペインが出ない理由が追えなくなる。
115
+ if (document.external_pane !== undefined) externalPane(document.external_pane);
70
116
  return Object.freeze({ projectId, displayName: document.display_name, source: 'project_file' });
71
117
  }
118
+
119
+ /**
120
+ * 差してあるなら外部ペイン設定を、無ければnullを返す。配信のたびに読むので、
121
+ * 設定を書いた/消した側はdaemonの再起動を要さない(reloadで反映される)。
122
+ */
123
+ export async function readProjectExternalPane({ repoRoot, projectId }) {
124
+ if (!path.isAbsolute(repoRoot) || typeof projectId !== 'string' || projectId.length === 0) {
125
+ throw new TypeError('project identity options invalid');
126
+ }
127
+ const document = await readIdentityDocument({ repoRoot, projectId });
128
+ if (document === null || document.external_pane === undefined) return null;
129
+ return externalPane(document.external_pane);
130
+ }
package/src/todo-cli.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  ensureTodoDashboardActivity,
28
28
  removeTodoDashboardProject,
29
29
  } from './todo-dashboard-registry.mjs';
30
- import { resolveProjectIdentity } from './project-identity.mjs';
30
+ import { readProjectExternalPane, resolveProjectIdentity } from './project-identity.mjs';
31
31
  import { layoutTodoGantt } from './todo-gantt-layout.mjs';
32
32
  import { TODO_GANTT_SCOPES } from './todo-gantt-scope.mjs';
33
33
  import { loadTodoGanttPresentation } from './todo-gantt-presentation.mjs';
@@ -2299,7 +2299,7 @@ async function seamProposalsForGantt({ repoRoot, store }) {
2299
2299
 
2300
2300
  export async function renderTodoGanttForProject({
2301
2301
  repoRoot, stable = false, displayName = null, env = process.env, readModel = null,
2302
- scope = DEFAULT_GANTT_SCOPE, includeNotes = true,
2302
+ scope = DEFAULT_GANTT_SCOPE,
2303
2303
  }) {
2304
2304
  const store = readModel
2305
2305
  ?? (stable ? await readTodoStoreStable({ repoRoot }) : await readTodoStore({ repoRoot }));
@@ -2312,8 +2312,7 @@ export async function renderTodoGanttForProject({
2312
2312
  const [independence, seamProposals, notes] = await Promise.all([
2313
2313
  independenceForGantt({ repoRoot, store }),
2314
2314
  seamProposalsForGantt({ repoRoot, store }),
2315
- includeNotes ? notesForGantt({ repoRoot, store })
2316
- : { contexts: null, warnings: [], headBindings: [] },
2315
+ notesForGantt({ repoRoot, store }),
2317
2316
  ]);
2318
2317
  const layout = layoutTodoGantt(store, chain, { scope, independence, seamProposals });
2319
2318
  // When the diagram hides history, the page also carries the full diagram so
@@ -2366,16 +2365,6 @@ export async function renderTodoGanttForProject({
2366
2365
  return { store, metadata, memberBindings, rendered };
2367
2366
  }
2368
2367
 
2369
- /**
2370
- * 公開配信面はToDo本体の設計メモを表示し、append-only作業記録は除外する。
2371
- *
2372
- * loopbackのdashboardと`gantt serve`は公開配信面ではない。作業者本人が読む面なので
2373
- * 記録込みで描く。この入口はrepo外へHTMLを出す面が生えた時に使う。
2374
- */
2375
- export async function renderPublicTodoGanttForProject(options = {}) {
2376
- return renderTodoGanttForProject({ ...options, includeNotes: false });
2377
- }
2378
-
2379
2368
  async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_SCOPE }) {
2380
2369
  const initialStore = await readTodoStoreStable({ repoRoot });
2381
2370
  const identity = await resolveProjectIdentity({ repoRoot, projectId: initialStore.project_id, env });
@@ -2388,7 +2377,9 @@ async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_S
2388
2377
  const { rendered } = await renderTodoGanttForProject({
2389
2378
  repoRoot, stable: true, displayName: identity.displayName, scope, readModel: store,
2390
2379
  });
2391
- return { html: rendered.html, head_digest: await ganttLiveHeadDigest({ repoRoot, store }) };
2380
+ return { html: rendered.html, head_digest: await ganttLiveHeadDigest({ repoRoot, store }),
2381
+ // ローカルの動的dashboardも公開daemonと同じ口を通す(毎描画で読むので差し外しが即反映される)。
2382
+ external_pane: await readProjectExternalPane({ repoRoot, projectId: store.project_id }) };
2392
2383
  },
2393
2384
  readHead: async () => ganttLiveHeadDigest({
2394
2385
  repoRoot, store: await readTodoStoreStable({ repoRoot }),
@@ -11,16 +11,63 @@ function projectPath(projectId) {
11
11
  return `/projects/${encodeURIComponent(projectId)}/`;
12
12
  }
13
13
 
14
- function liveHtml(html, headDigest, eventsPath) {
14
+ /**
15
+ * 注入点が消えていたら黙って注入をやめない。タブが出ない理由を後から追えるように、
16
+ * 差せなかった事実をtyped errorとして立てる(差し込み位置は描画部品の実装に依る)。
17
+ */
18
+ function injectOnce(html, marker, replacement) {
19
+ const at = html.indexOf(marker);
20
+ if (at === -1 || html.indexOf(marker, at + marker.length) !== -1) {
21
+ const error = new Error(`external pane injection point not unique: ${marker}`);
22
+ error.code = 'EXTERNAL_PANE_INJECTION_FAILED';
23
+ error.detail = { marker };
24
+ throw error;
25
+ }
26
+ return `${html.slice(0, at)}${replacement}${html.slice(at + marker.length)}`;
27
+ }
28
+
29
+ const EXTERNAL_PANE_STYLE = '<style>.narrative-pane{display:flex;flex-direction:column}[data-right-panel="external"]{display:flex;flex:1 1 auto;min-height:0}[data-right-panel="external"]>iframe{flex:1 1 auto;width:100%;min-height:0;border:0;background:var(--surface-1)}</style>';
30
+
31
+ /**
32
+ * 生存判定は「非空の一覧を返すこと」だけを根拠にする。Latticeは差された先が何の
33
+ * サービスかを知らないので、payloadの意味には触らない(非空配列、または配列値の
34
+ * プロパティが1つ以上非空)。非200・fetch失敗・空一覧はすべて概要タブのままにする。
35
+ */
36
+ function externalPaneController(pane) {
37
+ return `<script>(()=>{const root=document.querySelector('[data-gantt-root]');const panel=root.querySelector('[data-right-panel="external"]');const frame=panel.querySelector('iframe');const content=root.querySelector('.right-content');const button=root.querySelector('[data-show-external-pane]');const show=()=>{if(frame.getAttribute('src')===null)frame.setAttribute('src',frame.dataset.src);content.hidden=true;panel.hidden=false;button.setAttribute('aria-pressed','true');root.dataset.viewState='external';};const leave=()=>{if(root.dataset.viewState==='external')return;panel.hidden=true;content.hidden=false;button.setAttribute('aria-pressed','false');};new MutationObserver(leave).observe(root,{attributeFilter:['data-view-state']});button.addEventListener('click',show);const nonEmpty=value=>Array.isArray(value)&&value.length>0;fetch(${JSON.stringify(pane.probeUrl)},{cache:'no-store'}).then(response=>response.ok?response.json():null).then(payload=>{if(nonEmpty(payload)||(payload!==null&&typeof payload==='object'&&Object.values(payload).some(nonEmpty)))show();}).catch(()=>{});})();</script>`;
38
+ }
39
+
40
+ /**
41
+ * projectに外部ペインが差してある時だけ、右ペインへタブ1枚とiframeを足す。
42
+ * Latticeは題名・埋め込み先・生存probeの3つしか知らない(差す側が明示的に書く口)。
43
+ */
44
+ function withExternalPane(html, pane) {
45
+ const tab = `<button type="button" data-show-external-pane aria-pressed="false">${escapeHtml(pane.title)}</button>`;
46
+ const iframe = `<section data-right-panel="external" hidden><iframe data-src="${escapeHtml(pane.url)}" title="${escapeHtml(pane.title)}"></iframe></section>`;
47
+ return injectOnce(
48
+ injectOnce(
49
+ injectOnce(
50
+ injectOnce(html, "default-src 'none'; connect-src 'self';",
51
+ `default-src 'none'; connect-src 'self' ${pane.probeOrigin}; frame-src ${pane.frameOrigin};`),
52
+ '</head>', `${EXTERNAL_PANE_STYLE}</head>`,
53
+ ),
54
+ '<button type="button" data-show-overview>概要</button>', `${tab}<button type="button" data-show-overview>概要</button>`,
55
+ ),
56
+ '<div class="right-content">', `${iframe}<div class="right-content">`,
57
+ ).replace('</body>', `${externalPaneController(pane)}</body>`);
58
+ }
59
+
60
+ function liveHtml(html, headDigest, eventsPath, externalPane = null) {
15
61
  const controller = `<script>(()=>{const badge=document.createElement('div');badge.setAttribute('role','status');badge.style.cssText='position:fixed;right:12px;bottom:12px;z-index:99;padding:6px 10px;border:1px solid #d9d8d4;border-radius:4px;background:#fcfcfb;font:600 12px system-ui';badge.textContent='進捗: 接続中';document.body.append(badge);let head=${JSON.stringify(headDigest)};const stream=new EventSource(${JSON.stringify(eventsPath)});stream.addEventListener('state',event=>{const next=JSON.parse(event.data);badge.textContent='進捗: 最新';if(next.head_digest!==head){badge.textContent='進捗: 更新を反映中';location.reload();}});stream.addEventListener('lattice-error',event=>{const detail=JSON.parse(event.data);badge.textContent='進捗: エラー '+detail.code;badge.style.borderColor='#d03b3b';});stream.onerror=()=>{badge.textContent='進捗: 再接続中';};})();</script>`;
16
62
  const publicMetadata = '<meta name="description" content="Latticeで管理しているプロジェクトの依存工程と進捗を確認できます。"><meta name="robots" content="noindex, nofollow"><meta name="theme-color" content="#f7f3ea">';
17
63
  const publicStyle = '<style>body[data-gantt-root]{grid-template-rows:auto minmax(0,1fr)}.lattice-live-brand{z-index:10;display:flex;align-items:center;gap:8px;min-width:0;padding:10px 16px;border-bottom:1px solid #d8d0c5;background:#f7f3ea;color:#6c655d;font:600 12px/1.5 system-ui,-apple-system,"Hiragino Sans","Yu Gothic UI",sans-serif}.lattice-live-brand a{color:#201d19;text-decoration:none}.lattice-live-brand a:hover{color:#315cbe}.lattice-live-brand strong{color:#201d19}.lattice-live-brand-note{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lattice-live-back{margin-left:auto!important;color:#315cbe!important;font-weight:750}@media(max-width:560px){.lattice-live-brand{padding:9px 12px}.lattice-live-brand-note{display:none}}</style>';
18
64
  const publicHeader = '<header class="lattice-live-brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong><span class="lattice-live-brand-note">公開工程表</span><a class="lattice-live-back" href="/projects/">一覧へ戻る</a></header>';
19
- return html
65
+ const live = html
20
66
  .replace("default-src 'none';", "default-src 'none'; connect-src 'self';")
21
67
  .replace('</head>', `${publicMetadata}${publicStyle}</head>`)
22
68
  .replace(/<body([^>]*)>/u, `<body$1>${publicHeader}`)
23
69
  .replace('</body>', `${controller}</body>`);
70
+ return externalPane === null ? live : withExternalPane(live, externalPane);
24
71
  }
25
72
 
26
73
  function escapeHtml(value) {
@@ -221,7 +268,9 @@ export async function startTodoGanttDashboardServer({ registry, port = 0, redire
221
268
  displayName: project.displayName,
222
269
  });
223
270
  lastHeads.set(project.projectId, rendered.head_digest);
224
- const html = liveHtml(rendered.html, rendered.head_digest, `${projectPath(project.projectId)}events`);
271
+ // 外部ペイン設定は描画のたびにrender側が読み直す。差した/外した側は再起動を要さない。
272
+ const html = liveHtml(rendered.html, rendered.head_digest,
273
+ `${projectPath(project.projectId)}events`, rendered.external_pane ?? null);
225
274
  response.writeHead(200, { 'content-type': 'text/html; charset=utf-8',
226
275
  'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
227
276
  response.end(html);