@quolu/lattice 0.12.29 → 0.12.31
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.md +2 -1
- package/docs/bridge-setup.md +22 -2
- package/package.json +1 -1
- package/src/todo-gantt-layout.mjs +75 -15
package/README.md
CHANGED
|
@@ -119,7 +119,8 @@ dashboardはmanifestのfile identityが変わらない間のstable store readを
|
|
|
119
119
|
最近のsession activityが期限切れでも、Lattice storeの`active_set`が非空なprojectは一覧へ残ります。
|
|
120
120
|
長時間の外部処理中にCLI呼出しが途切れても進行中projectを休眠扱いしません。
|
|
121
121
|
LANや外部reverse proxyから閲覧するoptional bridgeは既定で無効です。明示したIPにだけbindする初回設定、
|
|
122
|
-
再設定、停止方法は[bridge setup](docs/bridge-setup.md)を参照してください。
|
|
122
|
+
再設定、停止方法は[bridge setup](docs/bridge-setup.md)を参照してください。reverse proxy hostへsshで
|
|
123
|
+
到達できる場合は、LANへbindせずloopbackだけを逆トンネルで公開する構成も選べます。
|
|
123
124
|
工程図の既定表示は、後続に作業中・未着手が残っていない完了工程を図から除きます。まとめnodeも置かないため、
|
|
124
125
|
完走したplanは図の場所を取りません。除いた工程は凡例の件数、右ペインの「全工程」一覧、各工程の詳細から
|
|
125
126
|
辿れ、詳細の前提・後続は除外前の依存関係を示します。総数・進捗・最長依存鎖は除外前の全工程で数えます。
|
package/docs/bridge-setup.md
CHANGED
|
@@ -10,7 +10,7 @@ portを省略するか`auto`にすると、49152–65535から候補を重複な
|
|
|
10
10
|
実際のexclusive bindとhealth確認に成功したportだけを保存する。
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
lattice bridge setup --listen 192.168.1.
|
|
13
|
+
lattice bridge setup --listen 192.168.1.50 --port auto --dashboard --allow-host lattice.example.com --json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
`--dashboard`は現在のlocal dashboard descriptorをrequestごとに解決するため、dashboard再起動でportが変わっても
|
|
@@ -20,7 +20,7 @@ listen IPは常に許可Hostへ入り、reverse proxyで公開するhostnameは`
|
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
lattice bridge status --json
|
|
23
|
-
lattice bridge reconfigure --listen 192.168.1.
|
|
23
|
+
lattice bridge reconfigure --listen 192.168.1.50 --port auto --dashboard --json
|
|
24
24
|
lattice bridge disable --json
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -45,6 +45,26 @@ daemonは各reconcileで実効アドレスを解決し直し、同一subnet(IP
|
|
|
45
45
|
ssh送信元から決めるため、各hostは自分自身しか登録できない。登録の失敗はbridgeを落とさずstderrへ
|
|
46
46
|
typedに報告する。この配線が無いと、Caddy等が持つリテラルはlease変更のたびに黙って陳腐化する。
|
|
47
47
|
|
|
48
|
+
## reverse proxyへ逆トンネルで繋ぐ(LAN bindを使わない)
|
|
49
|
+
|
|
50
|
+
reverse proxy hostへsshで到達できるなら、LANへbindせずloopbackだけで公開できる。bridgeが動くhostから
|
|
51
|
+
接続しに行くため、そのhostのLAN addressはreverse proxyのどこにも現れず、追従も自己登録も不要になる。
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
lattice bridge setup --listen 127.0.0.1 --port 53939 --dashboard --allow-host lattice.example.com --json
|
|
55
|
+
ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \
|
|
56
|
+
-R 172.18.0.1:53939:127.0.0.1:53939 proxy-host
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
reverse proxyはこの固定endpointだけを見る。転送口のbind先は、reverse proxyが到達できるaddressにする。
|
|
60
|
+
Docker上のreverse proxyでは、containerの`127.0.0.1`はcontainer自身のloopbackでありhostのそれではないため、
|
|
61
|
+
hostのloopbackへ開いた口には届かない。対象networkのgateway(`docker network inspect`の`Gateway`)へbindする。
|
|
62
|
+
|
|
63
|
+
sshdは既定の`GatewayPorts no`だと`127.0.0.1`にしかbindできない。`clientspecified`にすると、clientが明示した
|
|
64
|
+
addressだけにbindする(`yes`と違い全interfaceへは晒さない)。host firewallがINPUTをDROPしている場合は、
|
|
65
|
+
その1 portだけを許可する。`ExitOnForwardFailure=yes`は、転送口を開けないまま接続だけ生かす状態を防ぐ。
|
|
66
|
+
常駐はprocess supervisorのKeepAliveに任せ、切断時は張り直す。
|
|
67
|
+
|
|
48
68
|
## Docker Caddy/Cloudflare Tunnelへ接続する
|
|
49
69
|
|
|
50
70
|
bridgeを有効化したMacとreverse proxy hostの間で、まず許可Hostを付けたLAN到達を確認する。
|
package/package.json
CHANGED
|
@@ -436,6 +436,8 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
436
436
|
});
|
|
437
437
|
stageRowCount.push(Math.max(0, ...layer.map((key) => stageRow.get(key))) + 1);
|
|
438
438
|
}
|
|
439
|
+
const maximumLayerWidth = Math.max(1,
|
|
440
|
+
...[...transversePosition.values()].map((position) => position + 1));
|
|
439
441
|
const visibleKeys = new Set(nodes.map(({ key }) => key));
|
|
440
442
|
const displayBranches = edges.flatMap((edge, semanticIndex) => {
|
|
441
443
|
const identities = [...edge.joinIdentities.entries()]
|
|
@@ -455,14 +457,26 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
455
457
|
if (!portGroups.has(key)) portGroups.set(key, []);
|
|
456
458
|
portGroups.get(key).push(entry);
|
|
457
459
|
};
|
|
460
|
+
// 隣の段の真下へ繋ぐ辺は、出発と到着が同じ取り付け位置の集合を奪い合う。別々の
|
|
461
|
+
// 枠を取ると2本の縦線が12pxずれて短い横棒で継がれ、まっすぐ引ける線が段差付きに
|
|
462
|
+
// 見える。両端の縦線は帯の中で1点で接するだけなので、1枠を共有させて直線にする。
|
|
463
|
+
const sharedPort = new Set();
|
|
458
464
|
displayBranches.forEach((branch) => {
|
|
459
465
|
const { edge } = branch;
|
|
460
466
|
for (const gap of new Set([wave.get(edge.from), wave.get(edge.to) - 1])) {
|
|
461
467
|
if (!gapGroups.has(gap)) gapGroups.set(gap, []);
|
|
462
468
|
gapGroups.get(gap).push(branch.key);
|
|
463
469
|
}
|
|
464
|
-
|
|
465
|
-
|
|
470
|
+
const departureGap = wave.get(edge.from);
|
|
471
|
+
const fromColumn = transversePosition.get(edge.from);
|
|
472
|
+
const toColumn = transversePosition.get(edge.to);
|
|
473
|
+
if (departureGap === wave.get(edge.to) - 1 && fromColumn === toColumn) {
|
|
474
|
+
sharedPort.add(branch.key);
|
|
475
|
+
addPort(departureGap, fromColumn, portEntryKey(branch.key, 'through'));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
addPort(departureGap, fromColumn, portEntryKey(branch.key, 'departure'));
|
|
479
|
+
addPort(wave.get(edge.to) - 1, toColumn, portEntryKey(branch.key, 'arrival'));
|
|
466
480
|
});
|
|
467
481
|
for (const keys of gapGroups.values()) keys.sort(compareText);
|
|
468
482
|
for (const entries of portGroups.values()) entries.sort(compareText);
|
|
@@ -477,9 +491,51 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
477
491
|
const allGapIndexes = new Set([...gapGroups.keys(), ...connectorGroupKeysByGap.keys()]);
|
|
478
492
|
const maximumGapOccupancy = Math.max(0, ...[...allGapIndexes].map((gap) =>
|
|
479
493
|
(gapGroups.get(gap)?.length ?? 0) + (connectorGroupKeysByGap.get(gap)?.size ?? 0)));
|
|
494
|
+
// An edge that skips a stage has to travel vertically past the stages in
|
|
495
|
+
// between. It used to do that in a corridor beyond the right edge of the
|
|
496
|
+
// whole diagram, so a local hop between two neighbouring cards was drawn as a
|
|
497
|
+
// trip to the far right and back. It now descends through the vertical
|
|
498
|
+
// channel between two columns of cards, chosen from the boundaries that lie
|
|
499
|
+
// between its endpoints. Every card sits on the same column grid, so those
|
|
500
|
+
// gaps are free of cards at every stage: routing inside one is both shorter
|
|
501
|
+
// to follow and structurally unable to cross a card.
|
|
502
|
+
const channelLoad = new Map();
|
|
503
|
+
const channelAssignment = new Map();
|
|
504
|
+
const channelTraffic = (boundary) => channelLoad.get(boundary) ?? 0;
|
|
505
|
+
for (const branch of [...displayBranches].sort((left, right) => compareText(left.key, right.key))) {
|
|
506
|
+
const { edge } = branch;
|
|
507
|
+
if (wave.get(edge.from) === wave.get(edge.to) - 1) continue;
|
|
508
|
+
const fromColumn = transversePosition.get(edge.from);
|
|
509
|
+
const toColumn = transversePosition.get(edge.to);
|
|
510
|
+
const [first, last] = fromColumn === toColumn
|
|
511
|
+
? [toColumn, toColumn + 1]
|
|
512
|
+
: [Math.min(fromColumn, toColumn) + 1, Math.max(fromColumn, toColumn)];
|
|
513
|
+
let chosen = first;
|
|
514
|
+
for (let boundary = first + 1; boundary <= last; boundary += 1) {
|
|
515
|
+
const closer = Math.abs(boundary - toColumn) < Math.abs(chosen - toColumn);
|
|
516
|
+
if (channelTraffic(boundary) < channelTraffic(chosen)
|
|
517
|
+
|| (channelTraffic(boundary) === channelTraffic(chosen) && closer)) chosen = boundary;
|
|
518
|
+
}
|
|
519
|
+
channelAssignment.set(branch.key, { boundary: chosen, lane: channelTraffic(chosen) });
|
|
520
|
+
channelLoad.set(chosen, channelTraffic(chosen) + 1);
|
|
521
|
+
}
|
|
522
|
+
|
|
480
523
|
const nodeWidth = Math.max(GEOMETRY.node_width,
|
|
481
524
|
GEOMETRY.route_inset * 2 + (maximumPortTraffic + 1) * GEOMETRY.route_spacing);
|
|
482
|
-
|
|
525
|
+
// Only the boundaries that actually carry channels pay for them. Boundary 0
|
|
526
|
+
// is the margin left of the first column and only exists when something
|
|
527
|
+
// routes there, so an untravelled diagram keeps its original left edge.
|
|
528
|
+
const channelWidth = (boundary) => Math.max(24,
|
|
529
|
+
(channelTraffic(boundary) + 1) * GEOMETRY.route_spacing);
|
|
530
|
+
const columnLeft = [GEOMETRY.left + (channelTraffic(0) === 0 ? 0 : channelWidth(0))];
|
|
531
|
+
for (let column = 1; column < maximumLayerWidth; column += 1) {
|
|
532
|
+
columnLeft.push(columnLeft[column - 1] + nodeWidth + channelWidth(column));
|
|
533
|
+
}
|
|
534
|
+
const channelPosition = (edgeKey) => {
|
|
535
|
+
const { boundary, lane } = channelAssignment.get(edgeKey);
|
|
536
|
+
const left = boundary === 0 ? GEOMETRY.left : columnLeft[boundary - 1] + nodeWidth;
|
|
537
|
+
return left + GEOMETRY.route_spacing * (lane + 1);
|
|
538
|
+
};
|
|
483
539
|
// Each routing band is sized for the edges that actually cross it. One
|
|
484
540
|
// crowded band used to set the spacing for every stage in the diagram, so a
|
|
485
541
|
// long plan paid the width of its busiest junction on all of its stages.
|
|
@@ -496,7 +552,7 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
496
552
|
const projectedNodes = nodes.map((node) => {
|
|
497
553
|
const visible = visibleKeys.has(node.key);
|
|
498
554
|
const geometry = visible ? {
|
|
499
|
-
x:
|
|
555
|
+
x: columnLeft[transversePosition.get(node.key)],
|
|
500
556
|
y: stageTop[wave.get(node.key)]
|
|
501
557
|
+ stageRow.get(node.key) * (GEOMETRY.node_height + GEOMETRY.stage_row_gap),
|
|
502
558
|
width: nodeWidth,
|
|
@@ -521,20 +577,18 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
521
577
|
return stageTop[waveIndex] + stageHeight(waveIndex)
|
|
522
578
|
+ GEOMETRY.route_spacing * (index + 1);
|
|
523
579
|
};
|
|
524
|
-
const maximumLayerWidth = Math.max(1,
|
|
525
|
-
...[...transversePosition.values()].map((position) => position + 1));
|
|
526
|
-
const routeRight = GEOMETRY.left + (maximumLayerWidth - 1) * laneGap + nodeWidth + 12;
|
|
527
580
|
const branchCounts = new Map();
|
|
528
581
|
for (const branch of displayBranches) {
|
|
529
582
|
branchCounts.set(branch.semanticIndex, (branchCounts.get(branch.semanticIndex) ?? 0) + 1);
|
|
530
583
|
}
|
|
531
584
|
const portPosition = (edgeKey, direction, gap, row, nodeX) => {
|
|
532
585
|
const entries = portGroups.get(portGroupKey(gap, row));
|
|
586
|
+
const entry = portEntryKey(edgeKey, sharedPort.has(edgeKey) ? 'through' : direction);
|
|
533
587
|
return nodeX + GEOMETRY.route_inset
|
|
534
|
-
+ GEOMETRY.route_spacing * (entries.indexOf(
|
|
588
|
+
+ GEOMETRY.route_spacing * (entries.indexOf(entry) + 1);
|
|
535
589
|
};
|
|
536
590
|
|
|
537
|
-
const projectedEdges = displayBranches.map((branch
|
|
591
|
+
const projectedEdges = displayBranches.map((branch) => {
|
|
538
592
|
const { edge } = branch;
|
|
539
593
|
const onLongestChain = longestEdgeKeys.has(edge.key);
|
|
540
594
|
const from = coordinates.get(edge.from);
|
|
@@ -549,11 +603,17 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
549
603
|
const endY = to.y;
|
|
550
604
|
const departureY = gapPosition(branch.key, departureGap);
|
|
551
605
|
const arrivalY = gapPosition(branch.key, arrivalGap);
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
606
|
+
const corners = [[startX, startY], [startX, departureY]];
|
|
607
|
+
if (departureGap === arrivalGap) corners.push([endX, arrivalY]);
|
|
608
|
+
else {
|
|
609
|
+
const channelX = channelPosition(branch.key);
|
|
610
|
+
corners.push([channelX, departureY], [channelX, arrivalY], [endX, arrivalY]);
|
|
611
|
+
}
|
|
612
|
+
corners.push([endX, endY]);
|
|
613
|
+
// 1枠を共有する直線では折れ点が重なる。長さゼロの区間を残すと交差判定にも
|
|
614
|
+
// 描画にも意味のない点が混ざるので畳む。
|
|
615
|
+
const route = corners.filter(([x, y], index) =>
|
|
616
|
+
index === 0 || x !== corners[index - 1][0] || y !== corners[index - 1][1]);
|
|
557
617
|
return {
|
|
558
618
|
id: branchCounts.get(branch.semanticIndex) === 1
|
|
559
619
|
? `edge-${branch.semanticIndex}` : `edge-${branch.semanticIndex}-join-${branch.branchIndex}`,
|
|
@@ -610,7 +670,7 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
610
670
|
}
|
|
611
671
|
addCrossingBridges([...projectedEdges, ...junctionConnectors], logicalContacts);
|
|
612
672
|
let routeMaximumX = nodes.length === 0 ? 0
|
|
613
|
-
:
|
|
673
|
+
: columnLeft[maximumLayerWidth - 1] + nodeWidth;
|
|
614
674
|
for (const edge of projectedEdges) {
|
|
615
675
|
for (const [x] of edge.route) routeMaximumX = Math.max(routeMaximumX, x);
|
|
616
676
|
}
|