bugpatrol 0.0.1 → 0.1.4
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/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +35 -2
- package/dist/bin.js +12474 -0
- package/package.json +39 -5
- package/src/ui/app.js +1620 -0
- package/src/ui/graph-layout.js +224 -0
- package/src/ui/index.html +40 -0
- package/src/ui/style.css +1230 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Pure layout for the agent app map. Coordinates are SVG user units.
|
|
2
|
+
export function layoutGraph(screens, edges, entryId) {
|
|
3
|
+
const w = 190,
|
|
4
|
+
h = 150,
|
|
5
|
+
xGap = 130,
|
|
6
|
+
yGap = 55,
|
|
7
|
+
pad = 45;
|
|
8
|
+
const xStep = w + xGap,
|
|
9
|
+
yStep = h + yGap;
|
|
10
|
+
const ids = new Set(screens.map((screen) => screen.id));
|
|
11
|
+
const virtual = new Set(screens.filter((screen) => screen.virtual).map((screen) => screen.id));
|
|
12
|
+
const valid = edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to) && edge.from !== edge.to);
|
|
13
|
+
const neighbors = new Map([...ids].map((id) => [id, []]));
|
|
14
|
+
const outgoing = new Map([...ids].map((id) => [id, []]));
|
|
15
|
+
const incoming = new Map([...ids].map((id) => [id, 0]));
|
|
16
|
+
for (const edge of valid) {
|
|
17
|
+
neighbors.get(edge.from).push(edge.to);
|
|
18
|
+
neighbors.get(edge.to).push(edge.from);
|
|
19
|
+
if (edge.kind !== 'back') {
|
|
20
|
+
outgoing.get(edge.from).push(edge.to);
|
|
21
|
+
incoming.set(edge.to, incoming.get(edge.to) + 1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isolated = [...ids].filter((id) => neighbors.get(id).length === 0).sort();
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const components = [];
|
|
28
|
+
for (const id of [...ids].sort()) {
|
|
29
|
+
if (seen.has(id) || neighbors.get(id).length === 0) continue;
|
|
30
|
+
const members = [];
|
|
31
|
+
const queue = [id];
|
|
32
|
+
seen.add(id);
|
|
33
|
+
for (const current of queue) {
|
|
34
|
+
members.push(current);
|
|
35
|
+
for (const next of neighbors.get(current))
|
|
36
|
+
if (!seen.has(next)) {
|
|
37
|
+
seen.add(next);
|
|
38
|
+
queue.push(next);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
components.push(members.sort());
|
|
42
|
+
}
|
|
43
|
+
components.sort((a, b) => Number(b.includes(entryId)) - Number(a.includes(entryId)) || a[0].localeCompare(b[0]));
|
|
44
|
+
|
|
45
|
+
const blocks = [];
|
|
46
|
+
for (const members of components) {
|
|
47
|
+
const memberSet = new Set(members);
|
|
48
|
+
const reachedFromEntry = new Set();
|
|
49
|
+
if (memberSet.has(entryId)) {
|
|
50
|
+
const queue = [entryId];
|
|
51
|
+
reachedFromEntry.add(entryId);
|
|
52
|
+
for (const id of queue)
|
|
53
|
+
for (const to of outgoing.get(id))
|
|
54
|
+
if (!reachedFromEntry.has(to)) {
|
|
55
|
+
reachedFromEntry.add(to);
|
|
56
|
+
queue.push(to);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const roots = [
|
|
60
|
+
...(memberSet.has(entryId) ? [entryId] : []),
|
|
61
|
+
...members.filter((id) => id !== entryId && incoming.get(id) === 0 && !reachedFromEntry.has(id)),
|
|
62
|
+
];
|
|
63
|
+
const layer = new Map(roots.map((id) => [id, 0]));
|
|
64
|
+
const queue = [...roots];
|
|
65
|
+
const visit = () => {
|
|
66
|
+
for (let index = 0; index < queue.length; index++) {
|
|
67
|
+
const id = queue[index];
|
|
68
|
+
for (const to of outgoing.get(id))
|
|
69
|
+
if (memberSet.has(to) && !layer.has(to)) {
|
|
70
|
+
layer.set(to, layer.get(id) + 1);
|
|
71
|
+
queue.push(to);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
visit();
|
|
76
|
+
// A directed cycle can have no zero-incoming root.
|
|
77
|
+
for (const id of members)
|
|
78
|
+
if (!layer.has(id)) {
|
|
79
|
+
layer.set(id, 0);
|
|
80
|
+
queue.push(id);
|
|
81
|
+
visit();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const columns = Array.from({ length: Math.max(...layer.values()) + 1 }, () => []);
|
|
85
|
+
for (const id of members) columns[layer.get(id)].push(id);
|
|
86
|
+
const order = new Map();
|
|
87
|
+
for (const list of columns) for (const [index, id] of list.entries()) order.set(id, index);
|
|
88
|
+
const sweep = (column, neighbor) => {
|
|
89
|
+
const list = columns[column];
|
|
90
|
+
const center = (id) => {
|
|
91
|
+
const positions = valid
|
|
92
|
+
.filter(
|
|
93
|
+
(edge) =>
|
|
94
|
+
edge.kind !== 'back' &&
|
|
95
|
+
((edge.from === id && layer.get(edge.to) === neighbor) ||
|
|
96
|
+
(edge.to === id && layer.get(edge.from) === neighbor)),
|
|
97
|
+
)
|
|
98
|
+
.map((edge) => order.get(edge.from === id ? edge.to : edge.from))
|
|
99
|
+
.filter((value) => value !== undefined);
|
|
100
|
+
return positions.length ? positions.reduce((sum, value) => sum + value, 0) / positions.length : order.get(id);
|
|
101
|
+
};
|
|
102
|
+
list.sort((a, b) => center(a) - center(b) || a.localeCompare(b));
|
|
103
|
+
for (const [index, id] of list.entries()) order.set(id, index);
|
|
104
|
+
};
|
|
105
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
106
|
+
for (let col = 1; col < columns.length; col++) sweep(col, col - 1);
|
|
107
|
+
for (let col = columns.length - 2; col >= 0; col--) sweep(col, col + 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const nodes = [];
|
|
111
|
+
let xColumn = 0;
|
|
112
|
+
for (const list of columns) {
|
|
113
|
+
list.forEach((id, index) => {
|
|
114
|
+
const x = (xColumn + Math.floor(index / 10)) * xStep,
|
|
115
|
+
y = (index % 10) * yStep;
|
|
116
|
+
nodes.push(virtual.has(id) ? { id, x: x + 35, y: y + 53, w: 120, h: 44 } : { id, x, y, w, h });
|
|
117
|
+
});
|
|
118
|
+
xColumn += Math.ceil(list.length / 10);
|
|
119
|
+
}
|
|
120
|
+
blocks.push({
|
|
121
|
+
nodes,
|
|
122
|
+
width: xColumn * xStep - xGap,
|
|
123
|
+
height: Math.max(...nodes.map((node) => node.y + node.h)),
|
|
124
|
+
isolated: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (isolated.length) {
|
|
129
|
+
blocks.push({
|
|
130
|
+
nodes: isolated.map((id, index) => {
|
|
131
|
+
const x = Math.floor(index / 6) * xStep,
|
|
132
|
+
y = 35 + (index % 6) * yStep;
|
|
133
|
+
return virtual.has(id) ? { id, x: x + 35, y: y + 53, w: 120, h: 44 } : { id, x, y, w, h };
|
|
134
|
+
}),
|
|
135
|
+
width: Math.ceil(isolated.length / 6) * xStep - xGap,
|
|
136
|
+
height: 35 + (Math.min(6, isolated.length) - 1) * yStep + h,
|
|
137
|
+
isolated: true,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const gap = 100;
|
|
142
|
+
const widths = new Set();
|
|
143
|
+
for (let start = 0; start < blocks.length; start++) {
|
|
144
|
+
let width = 0;
|
|
145
|
+
for (let end = start; end < blocks.length; end++) {
|
|
146
|
+
width += blocks[end].width + (end === start ? 0 : gap);
|
|
147
|
+
widths.add(width);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let targetWidth = 0,
|
|
151
|
+
bestScore = Infinity;
|
|
152
|
+
for (const width of widths) {
|
|
153
|
+
let rowWidth = 0,
|
|
154
|
+
rowHeight = 0,
|
|
155
|
+
usedWidth = 0,
|
|
156
|
+
usedHeight = 0;
|
|
157
|
+
for (const block of blocks) {
|
|
158
|
+
if (rowWidth && rowWidth + gap + block.width > width) {
|
|
159
|
+
usedHeight += rowHeight + gap;
|
|
160
|
+
rowWidth = 0;
|
|
161
|
+
rowHeight = 0;
|
|
162
|
+
}
|
|
163
|
+
rowWidth += block.width + (rowWidth ? gap : 0);
|
|
164
|
+
rowHeight = Math.max(rowHeight, block.height);
|
|
165
|
+
usedWidth = Math.max(usedWidth, rowWidth);
|
|
166
|
+
}
|
|
167
|
+
const ratio = (usedWidth + pad * 2) / (usedHeight + rowHeight + pad * 2);
|
|
168
|
+
const score = Math.abs(Math.log(ratio / 1.6));
|
|
169
|
+
if (score < bestScore) {
|
|
170
|
+
bestScore = score;
|
|
171
|
+
targetWidth = width;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const nodes = [];
|
|
175
|
+
let x = pad,
|
|
176
|
+
y = pad,
|
|
177
|
+
rowHeight = 0,
|
|
178
|
+
maxRight = 0,
|
|
179
|
+
maxBottom = 0;
|
|
180
|
+
let unlinkedX = null,
|
|
181
|
+
unlinkedY = null;
|
|
182
|
+
for (const block of blocks) {
|
|
183
|
+
if (x > pad && x - pad + block.width > targetWidth) {
|
|
184
|
+
x = pad;
|
|
185
|
+
y += rowHeight + gap;
|
|
186
|
+
rowHeight = 0;
|
|
187
|
+
}
|
|
188
|
+
nodes.push(...block.nodes.map((node) => ({ ...node, x: node.x + x, y: node.y + y })));
|
|
189
|
+
if (block.isolated) {
|
|
190
|
+
unlinkedX = x;
|
|
191
|
+
unlinkedY = y;
|
|
192
|
+
}
|
|
193
|
+
maxRight = Math.max(maxRight, x + block.width);
|
|
194
|
+
maxBottom = Math.max(maxBottom, y + block.height);
|
|
195
|
+
rowHeight = Math.max(rowHeight, block.height);
|
|
196
|
+
x += block.width + gap;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const positions = new Map(nodes.map((node) => [node.id, node]));
|
|
200
|
+
const paths = valid.map((edge, index) => {
|
|
201
|
+
const from = positions.get(edge.from),
|
|
202
|
+
to = positions.get(edge.to);
|
|
203
|
+
if (to.x > from.x) {
|
|
204
|
+
const x1 = from.x + from.w,
|
|
205
|
+
x2 = to.x,
|
|
206
|
+
y1 = from.y + from.h / 2,
|
|
207
|
+
y2 = to.y + to.h / 2;
|
|
208
|
+
const bend = Math.max(65, (x2 - x1) / 2);
|
|
209
|
+
return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
|
|
210
|
+
}
|
|
211
|
+
const y = Math.min(from.y, to.y) - 25 - (index % 5) * 12;
|
|
212
|
+
const x1 = from.x + from.w / 2,
|
|
213
|
+
x2 = to.x + to.w / 2;
|
|
214
|
+
return { ...edge, path: `M ${x1} ${from.y} C ${x1} ${y}, ${x2} ${y}, ${x2} ${to.y}` };
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
nodes,
|
|
218
|
+
edges: paths,
|
|
219
|
+
width: Math.max(320, maxRight + pad),
|
|
220
|
+
height: Math.max(320, maxBottom + pad),
|
|
221
|
+
unlinkedX,
|
|
222
|
+
unlinkedY,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Bugpatrol</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="topbar">
|
|
11
|
+
<div class="brand">
|
|
12
|
+
<span class="mark" aria-hidden="true"></span>
|
|
13
|
+
<strong id="project-name">Bugpatrol</strong>
|
|
14
|
+
</div>
|
|
15
|
+
<nav class="tabs" id="tabs">
|
|
16
|
+
<button type="button" data-view="overview" class="active">Overview</button>
|
|
17
|
+
<button type="button" data-view="issues">Issues</button>
|
|
18
|
+
<button type="button" data-view="activity">Activity</button>
|
|
19
|
+
<button type="button" data-view="screens">Screens</button>
|
|
20
|
+
<button type="button" data-view="memory">Memory</button>
|
|
21
|
+
<button type="button" data-view="checks">Checks</button>
|
|
22
|
+
</nav>
|
|
23
|
+
<div class="status">
|
|
24
|
+
<span id="live" class="live" title="Live connection to the local watcher">live</span>
|
|
25
|
+
<span id="platform" class="muted"></span>
|
|
26
|
+
<span id="patrol" class="pill"></span>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<main id="view"></main>
|
|
31
|
+
|
|
32
|
+
<div id="lightbox" class="lightbox" hidden>
|
|
33
|
+
<button type="button" class="lightbox-close" aria-label="Close">×</button>
|
|
34
|
+
<img id="lightbox-img" alt="">
|
|
35
|
+
<p id="lightbox-caption"></p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<script type="module" src="/app.js"></script>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|