@datasynx/agentic-ai-cartography 0.4.0 → 0.6.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.md +18 -8
- package/dist/{chunk-7NRS3WP6.js → chunk-A7FTULDM.js} +63 -2
- package/dist/chunk-A7FTULDM.js.map +1 -0
- package/dist/cli.js +1605 -680
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +180 -102
- package/dist/index.js +1657 -674
- package/dist/index.js.map +1 -1
- package/dist/{types-A6K73WE4.js → types-ZD6G5JKR.js} +12 -2
- package/package.json +1 -1
- package/dist/chunk-7NRS3WP6.js.map +0 -1
- /package/dist/{types-A6K73WE4.js.map → types-ZD6G5JKR.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DOMAIN_COLORS,
|
|
4
|
+
DOMAIN_PALETTE,
|
|
3
5
|
EDGE_RELATIONSHIPS,
|
|
4
6
|
NODE_TYPES,
|
|
5
7
|
SOPStepSchema,
|
|
6
8
|
defaultConfig
|
|
7
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-A7FTULDM.js";
|
|
8
10
|
import {
|
|
9
11
|
scanAllBookmarks,
|
|
10
12
|
scanAllHistory
|
|
@@ -1213,8 +1215,8 @@ import { join as join2 } from "path";
|
|
|
1213
1215
|
|
|
1214
1216
|
// src/hex.ts
|
|
1215
1217
|
function hexToPixel(q, r, size) {
|
|
1216
|
-
const x = size * (
|
|
1217
|
-
const y = size * (3 / 2 * r);
|
|
1218
|
+
const x = size * (3 / 2 * q);
|
|
1219
|
+
const y = size * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r);
|
|
1218
1220
|
return { x, y };
|
|
1219
1221
|
}
|
|
1220
1222
|
var HEX_DIRECTIONS = [
|
|
@@ -1229,7 +1231,7 @@ function hexDistance(a, b) {
|
|
|
1229
1231
|
return (Math.abs(a.q - b.q) + Math.abs(a.q + a.r - b.q - b.r) + Math.abs(a.r - b.r)) / 2;
|
|
1230
1232
|
}
|
|
1231
1233
|
function hexRing(center, radius) {
|
|
1232
|
-
if (radius === 0) return [{
|
|
1234
|
+
if (radius === 0) return [{ q: center.q, r: center.r }];
|
|
1233
1235
|
const results = [];
|
|
1234
1236
|
let hex = {
|
|
1235
1237
|
q: center.q + HEX_DIRECTIONS[4].q * radius,
|
|
@@ -1250,8 +1252,8 @@ function hexSpiral(center, count) {
|
|
|
1250
1252
|
const positions = [];
|
|
1251
1253
|
let ring = 0;
|
|
1252
1254
|
while (positions.length < count) {
|
|
1253
|
-
const
|
|
1254
|
-
for (const pos of
|
|
1255
|
+
const ringPositions = hexRing(center, ring);
|
|
1256
|
+
for (const pos of ringPositions) {
|
|
1255
1257
|
if (positions.length >= count) break;
|
|
1256
1258
|
positions.push(pos);
|
|
1257
1259
|
}
|
|
@@ -1261,73 +1263,74 @@ function hexSpiral(center, count) {
|
|
|
1261
1263
|
}
|
|
1262
1264
|
|
|
1263
1265
|
// src/cluster.ts
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
// 0 deep navy
|
|
1267
|
-
"#1e3a8a",
|
|
1268
|
-
// 1 dark blue
|
|
1269
|
-
"#1d4ed8",
|
|
1270
|
-
// 2 medium blue
|
|
1271
|
-
"#2563eb",
|
|
1272
|
-
// 3 blue
|
|
1273
|
-
"#3b82f6",
|
|
1274
|
-
// 4 light blue
|
|
1275
|
-
"#6366f1",
|
|
1276
|
-
// 5 indigo
|
|
1277
|
-
"#818cf8",
|
|
1278
|
-
// 6 periwinkle
|
|
1279
|
-
"#7c9fc3",
|
|
1280
|
-
// 7 slate blue
|
|
1281
|
-
"#0e7490",
|
|
1282
|
-
// 8 dark teal
|
|
1283
|
-
"#0891b2",
|
|
1284
|
-
// 9 teal
|
|
1285
|
-
"#06b6d4",
|
|
1286
|
-
// 10 cyan
|
|
1287
|
-
"#22d3ee",
|
|
1288
|
-
// 11 light cyan
|
|
1289
|
-
"#0d9488",
|
|
1290
|
-
// 12 dark teal-green
|
|
1291
|
-
"#14b8a6",
|
|
1292
|
-
// 13 teal
|
|
1293
|
-
"#2dd4bf",
|
|
1294
|
-
// 14 light teal
|
|
1295
|
-
"#5eead4"
|
|
1296
|
-
// 15 pale teal
|
|
1297
|
-
];
|
|
1298
|
-
function domainColor(domain, allDomains) {
|
|
1266
|
+
function assignColor(domain, allDomains) {
|
|
1267
|
+
if (DOMAIN_COLORS[domain]) return DOMAIN_COLORS[domain];
|
|
1299
1268
|
const idx = allDomains.indexOf(domain);
|
|
1300
1269
|
return DOMAIN_PALETTE[idx % DOMAIN_PALETTE.length];
|
|
1301
1270
|
}
|
|
1302
|
-
|
|
1271
|
+
function assignColors(domains) {
|
|
1272
|
+
const result = {};
|
|
1273
|
+
for (const d of domains) {
|
|
1274
|
+
result[d] = assignColor(d, domains);
|
|
1275
|
+
}
|
|
1276
|
+
return result;
|
|
1277
|
+
}
|
|
1278
|
+
function groupByDomain(assets) {
|
|
1279
|
+
const map = /* @__PURE__ */ new Map();
|
|
1280
|
+
for (const a of assets) {
|
|
1281
|
+
const d = a.domain || "Other";
|
|
1282
|
+
if (!map.has(d)) map.set(d, []);
|
|
1283
|
+
map.get(d).push(a);
|
|
1284
|
+
}
|
|
1285
|
+
return map;
|
|
1286
|
+
}
|
|
1303
1287
|
var CLUSTER_GAP = 3;
|
|
1304
|
-
function layoutClusters(
|
|
1305
|
-
const
|
|
1288
|
+
function layoutClusters(groups, hexSize) {
|
|
1289
|
+
const allDomains = Array.from(groups.keys());
|
|
1290
|
+
const colors = assignColors(allDomains);
|
|
1291
|
+
const sorted = Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
1306
1292
|
const occupied = /* @__PURE__ */ new Set();
|
|
1307
1293
|
const key = (q, r) => `${q},${r}`;
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
const
|
|
1314
|
-
for (
|
|
1315
|
-
|
|
1294
|
+
const clusters = [];
|
|
1295
|
+
const allAssets = [];
|
|
1296
|
+
for (const [domain, domainAssets] of sorted) {
|
|
1297
|
+
const origin = clusters.length === 0 ? { q: 0, r: 0 } : findFreeOrigin(occupied, domainAssets.length, CLUSTER_GAP);
|
|
1298
|
+
const positions = hexSpiral(origin, domainAssets.length);
|
|
1299
|
+
const assetIds = [];
|
|
1300
|
+
for (let i = 0; i < domainAssets.length; i++) {
|
|
1301
|
+
const asset = domainAssets[i];
|
|
1302
|
+
asset.position = positions[i];
|
|
1303
|
+
assetIds.push(asset.id);
|
|
1304
|
+
occupied.add(key(positions[i].q, positions[i].r));
|
|
1305
|
+
allAssets.push(asset);
|
|
1316
1306
|
}
|
|
1317
|
-
|
|
1307
|
+
const centroid = computeCentroid(positions, hexSize);
|
|
1308
|
+
clusters.push({
|
|
1309
|
+
id: `cluster:${domain}`,
|
|
1310
|
+
label: domain,
|
|
1311
|
+
domain,
|
|
1312
|
+
color: colors[domain],
|
|
1313
|
+
assetIds,
|
|
1314
|
+
centroid
|
|
1315
|
+
});
|
|
1318
1316
|
}
|
|
1319
|
-
return
|
|
1317
|
+
return { clusters, assets: allAssets };
|
|
1320
1318
|
}
|
|
1321
|
-
function findFreeOrigin(
|
|
1322
|
-
const
|
|
1323
|
-
for (let
|
|
1324
|
-
const candidates = hexSpiral({ q: 0, r: 0 },
|
|
1319
|
+
function findFreeOrigin(occupied, count, gap) {
|
|
1320
|
+
const key = (q, r) => `${q},${r}`;
|
|
1321
|
+
for (let searchRadius = 1; searchRadius < 100; searchRadius++) {
|
|
1322
|
+
const candidates = hexSpiral({ q: 0, r: 0 }, 1 + 6 * searchRadius * (searchRadius + 1) / 2);
|
|
1325
1323
|
for (const candidate of candidates) {
|
|
1326
|
-
const testPositions = hexSpiral(candidate,
|
|
1324
|
+
const testPositions = hexSpiral(candidate, count);
|
|
1327
1325
|
let fits = true;
|
|
1328
1326
|
for (const tp of testPositions) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1327
|
+
if (occupied.has(key(tp.q, tp.r))) {
|
|
1328
|
+
fits = false;
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
for (const oKey of occupied) {
|
|
1332
|
+
const [oq, or] = oKey.split(",").map(Number);
|
|
1333
|
+
if (hexDistance(tp, { q: oq, r: or }) < gap) {
|
|
1331
1334
|
fits = false;
|
|
1332
1335
|
break;
|
|
1333
1336
|
}
|
|
@@ -1337,97 +1340,90 @@ function findFreeOrigin(existing, occupied, newCount, gap) {
|
|
|
1337
1340
|
if (fits) return candidate;
|
|
1338
1341
|
}
|
|
1339
1342
|
}
|
|
1340
|
-
return { q:
|
|
1343
|
+
return { q: occupied.size * 5, r: 0 };
|
|
1341
1344
|
}
|
|
1342
|
-
function
|
|
1343
|
-
|
|
1344
|
-
}
|
|
1345
|
-
function computeCentroid(positions, size) {
|
|
1345
|
+
function computeCentroid(positions, hexSize) {
|
|
1346
|
+
if (positions.length === 0) return { x: 0, y: 0 };
|
|
1346
1347
|
let sx = 0, sy = 0;
|
|
1347
1348
|
for (const { q, r } of positions) {
|
|
1348
|
-
const { x, y } = hexToPixel(q, r,
|
|
1349
|
+
const { x, y } = hexToPixel(q, r, hexSize);
|
|
1349
1350
|
sx += x;
|
|
1350
1351
|
sy += y;
|
|
1351
1352
|
}
|
|
1352
1353
|
return { x: sx / positions.length, y: sy / positions.length };
|
|
1353
1354
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
const placements = layoutClusters(domainSizes);
|
|
1379
|
-
const clusters = [];
|
|
1380
|
-
const subClustersMap = /* @__PURE__ */ new Map();
|
|
1381
|
-
for (const placement of placements) {
|
|
1382
|
-
const { domain, positions } = placement;
|
|
1383
|
-
const domainNodes = byDomain.get(domain) ?? [];
|
|
1384
|
-
const color = domainColor(domain, allDomains);
|
|
1385
|
-
const assets = domainNodes.map((n, i) => ({
|
|
1386
|
-
id: n.id,
|
|
1387
|
-
name: n.name,
|
|
1388
|
-
domain: n.domain ?? "Other",
|
|
1389
|
-
subDomain: n.subDomain,
|
|
1390
|
-
qualityScore: n.qualityScore,
|
|
1391
|
-
metadata: n.metadata ?? {},
|
|
1392
|
-
position: positions[i] ?? { q: 0, r: 0 }
|
|
1393
|
-
}));
|
|
1394
|
-
const centroid = computeCentroid(positions.slice(0, assets.length), size);
|
|
1395
|
-
const clusterId = `cluster:${domain}`;
|
|
1396
|
-
clusters.push({
|
|
1397
|
-
id: clusterId,
|
|
1398
|
-
label: domain,
|
|
1399
|
-
domain,
|
|
1400
|
-
color,
|
|
1401
|
-
assets,
|
|
1402
|
-
centroid
|
|
1403
|
-
});
|
|
1404
|
-
const subs = groupBySubDomain(assets);
|
|
1405
|
-
const assetById = new Map(assets.map((a) => [a.id, a]));
|
|
1406
|
-
for (const sub of subs) {
|
|
1407
|
-
const subPositions = sub.assetIds.map((id) => assetById.get(id)?.position).filter(Boolean);
|
|
1408
|
-
if (subPositions.length > 0) {
|
|
1409
|
-
sub.centroid = computeCentroid(subPositions, size);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
if (subs.length > 0) subClustersMap.set(clusterId, subs);
|
|
1355
|
+
|
|
1356
|
+
// src/mapper.ts
|
|
1357
|
+
var TYPE_TO_DOMAIN = {
|
|
1358
|
+
database_server: "Data Layer",
|
|
1359
|
+
database: "Data Layer",
|
|
1360
|
+
table: "Data Layer",
|
|
1361
|
+
cache_server: "Data Layer",
|
|
1362
|
+
web_service: "Web / API",
|
|
1363
|
+
api_endpoint: "Web / API",
|
|
1364
|
+
message_broker: "Messaging",
|
|
1365
|
+
queue: "Messaging",
|
|
1366
|
+
topic: "Messaging",
|
|
1367
|
+
host: "Infrastructure",
|
|
1368
|
+
container: "Infrastructure",
|
|
1369
|
+
pod: "Infrastructure",
|
|
1370
|
+
k8s_cluster: "Infrastructure",
|
|
1371
|
+
config_file: "Infrastructure",
|
|
1372
|
+
saas_tool: "SaaS Tools"
|
|
1373
|
+
};
|
|
1374
|
+
function resolveDomain(node) {
|
|
1375
|
+
if (node.domain) return node.domain;
|
|
1376
|
+
const meta = node.metadata;
|
|
1377
|
+
if (typeof meta["category"] === "string" && meta["category"].length > 0) {
|
|
1378
|
+
return meta["category"];
|
|
1413
1379
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
const { x, y } = hexToPixel(asset.position.q, asset.position.r, size);
|
|
1418
|
-
if (x < minX) minX = x;
|
|
1419
|
-
if (y < minY) minY = y;
|
|
1420
|
-
if (x > maxX) maxX = x;
|
|
1421
|
-
if (y > maxY) maxY = y;
|
|
1380
|
+
for (const tag of node.tags ?? []) {
|
|
1381
|
+
if (tag.length > 2 && tag[0] === tag[0].toUpperCase()) {
|
|
1382
|
+
return tag;
|
|
1422
1383
|
}
|
|
1423
1384
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1385
|
+
return TYPE_TO_DOMAIN[node.type] ?? "Other";
|
|
1386
|
+
}
|
|
1387
|
+
function nodesToAssets(nodes) {
|
|
1388
|
+
return nodes.map((n) => ({
|
|
1389
|
+
id: n.id,
|
|
1390
|
+
name: n.name,
|
|
1391
|
+
domain: resolveDomain(n),
|
|
1392
|
+
subDomain: n.subDomain,
|
|
1393
|
+
qualityScore: n.qualityScore ?? Math.round(n.confidence * 100),
|
|
1394
|
+
metadata: n.metadata ?? {},
|
|
1395
|
+
position: { q: 0, r: 0 }
|
|
1396
|
+
// will be assigned by layoutClusters
|
|
1397
|
+
}));
|
|
1398
|
+
}
|
|
1399
|
+
function edgesToConnections(edges) {
|
|
1400
|
+
return edges.map((e) => ({
|
|
1401
|
+
id: e.id,
|
|
1402
|
+
sourceAssetId: e.sourceId,
|
|
1403
|
+
targetAssetId: e.targetId,
|
|
1404
|
+
type: e.relationship
|
|
1405
|
+
}));
|
|
1406
|
+
}
|
|
1407
|
+
var HEX_SIZE = 24;
|
|
1408
|
+
function buildMapData(nodes, edges, options) {
|
|
1409
|
+
const rawAssets = nodesToAssets(nodes);
|
|
1410
|
+
const connections = edgesToConnections(edges);
|
|
1411
|
+
if (rawAssets.length === 0) {
|
|
1412
|
+
return {
|
|
1413
|
+
assets: [],
|
|
1414
|
+
clusters: [],
|
|
1415
|
+
connections,
|
|
1416
|
+
meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
|
|
1417
|
+
};
|
|
1429
1418
|
}
|
|
1430
|
-
|
|
1419
|
+
const groups = groupByDomain(rawAssets);
|
|
1420
|
+
const { clusters, assets } = layoutClusters(groups, HEX_SIZE);
|
|
1421
|
+
return {
|
|
1422
|
+
assets,
|
|
1423
|
+
clusters,
|
|
1424
|
+
connections,
|
|
1425
|
+
meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
|
|
1426
|
+
};
|
|
1431
1427
|
}
|
|
1432
1428
|
|
|
1433
1429
|
// src/exporter.ts
|
|
@@ -2174,58 +2170,13 @@ function exportSOPMarkdown(sop) {
|
|
|
2174
2170
|
}
|
|
2175
2171
|
return lines.join("\n");
|
|
2176
2172
|
}
|
|
2177
|
-
function
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
const
|
|
2182
|
-
const
|
|
2183
|
-
|
|
2184
|
-
writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
2185
|
-
writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
2186
|
-
process.stderr.write("\u2713 topology.mermaid, dependencies.mermaid\n");
|
|
2187
|
-
}
|
|
2188
|
-
if (formats.includes("json")) {
|
|
2189
|
-
writeFileSync(join2(outputDir, "catalog.json"), exportJSON(db, sessionId));
|
|
2190
|
-
process.stderr.write("\u2713 catalog.json\n");
|
|
2191
|
-
}
|
|
2192
|
-
if (formats.includes("yaml")) {
|
|
2193
|
-
writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
2194
|
-
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
2195
|
-
}
|
|
2196
|
-
if (formats.includes("html")) {
|
|
2197
|
-
writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
|
|
2198
|
-
process.stderr.write("\u2713 topology.html\n");
|
|
2199
|
-
}
|
|
2200
|
-
if (formats.includes("sops")) {
|
|
2201
|
-
const sops = db.getSOPs(sessionId);
|
|
2202
|
-
for (const sop of sops) {
|
|
2203
|
-
const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
|
|
2204
|
-
writeFileSync(join2(outputDir, "sops", filename), exportSOPMarkdown(sop));
|
|
2205
|
-
const wfFilename = `workflow-${sop.workflowId.substring(0, 8)}.mermaid`;
|
|
2206
|
-
writeFileSync(join2(outputDir, "workflows", wfFilename), generateWorkflowMermaid(sop));
|
|
2207
|
-
}
|
|
2208
|
-
if (sops.length > 0) {
|
|
2209
|
-
process.stderr.write(`\u2713 ${sops.length} SOPs + workflow diagrams
|
|
2210
|
-
`);
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
if (formats.includes("hexmap")) {
|
|
2214
|
-
const connections = db.getConnections(sessionId);
|
|
2215
|
-
writeFileSync(join2(outputDir, "hexmap.html"), exportHexMap(nodes, connections));
|
|
2216
|
-
process.stderr.write("\u2713 hexmap.html\n");
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
function exportHexMap(nodes, connections) {
|
|
2220
|
-
const layout = buildClusterLayout(nodes);
|
|
2221
|
-
const { clusters, subClusters, hexSize, bounds } = layout;
|
|
2222
|
-
const clustersJson = JSON.stringify(clusters.map((c) => ({
|
|
2223
|
-
id: c.id,
|
|
2224
|
-
label: c.label,
|
|
2225
|
-
domain: c.domain,
|
|
2226
|
-
color: c.color,
|
|
2227
|
-
centroid: c.centroid,
|
|
2228
|
-
assets: c.assets.map((a) => ({
|
|
2173
|
+
function exportCartographyMap(nodes, edges, options) {
|
|
2174
|
+
const mapData = buildMapData(nodes, edges, options);
|
|
2175
|
+
const { assets, clusters, connections, meta } = mapData;
|
|
2176
|
+
const isEmpty = assets.length === 0;
|
|
2177
|
+
const HEX_SIZE2 = 24;
|
|
2178
|
+
const dataJson = JSON.stringify({
|
|
2179
|
+
assets: assets.map((a) => ({
|
|
2229
2180
|
id: a.id,
|
|
2230
2181
|
name: a.name,
|
|
2231
2182
|
domain: a.domain,
|
|
@@ -2234,24 +2185,22 @@ function exportHexMap(nodes, connections) {
|
|
|
2234
2185
|
metadata: a.metadata,
|
|
2235
2186
|
q: a.position.q,
|
|
2236
2187
|
r: a.position.r
|
|
2188
|
+
})),
|
|
2189
|
+
clusters: clusters.map((c) => ({
|
|
2190
|
+
id: c.id,
|
|
2191
|
+
label: c.label,
|
|
2192
|
+
domain: c.domain,
|
|
2193
|
+
color: c.color,
|
|
2194
|
+
assetIds: c.assetIds,
|
|
2195
|
+
centroid: c.centroid
|
|
2196
|
+
})),
|
|
2197
|
+
connections: connections.map((c) => ({
|
|
2198
|
+
id: c.id,
|
|
2199
|
+
sourceAssetId: c.sourceAssetId,
|
|
2200
|
+
targetAssetId: c.targetAssetId,
|
|
2201
|
+
type: c.type ?? "connection"
|
|
2237
2202
|
}))
|
|
2238
|
-
})
|
|
2239
|
-
const subClustersJson = JSON.stringify(
|
|
2240
|
-
Object.fromEntries(
|
|
2241
|
-
Array.from(subClusters.entries()).map(([cid, subs]) => [
|
|
2242
|
-
cid,
|
|
2243
|
-
subs.map((s) => ({ subDomain: s.subDomain, assetIds: s.assetIds, centroid: s.centroid }))
|
|
2244
|
-
])
|
|
2245
|
-
)
|
|
2246
|
-
);
|
|
2247
|
-
const connectionsJson = JSON.stringify(connections.map((c) => ({
|
|
2248
|
-
id: c.id,
|
|
2249
|
-
sourceAssetId: c.sourceAssetId,
|
|
2250
|
-
targetAssetId: c.targetAssetId,
|
|
2251
|
-
type: c.type ?? "connection"
|
|
2252
|
-
})));
|
|
2253
|
-
const hexSizeVal = hexSize;
|
|
2254
|
-
const isEmpty = nodes.length === 0;
|
|
2203
|
+
});
|
|
2255
2204
|
return `<!DOCTYPE html>
|
|
2256
2205
|
<html lang="en">
|
|
2257
2206
|
<head>
|
|
@@ -2261,28 +2210,20 @@ function exportHexMap(nodes, connections) {
|
|
|
2261
2210
|
<style>
|
|
2262
2211
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
2263
2212
|
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
2264
|
-
body{display:flex;flex-direction:column;background
|
|
2213
|
+
body{display:flex;flex-direction:column;background:${meta.theme === "dark" ? "#0f172a" : "#f8fafc"};color:${meta.theme === "dark" ? "#e2e8f0" : "#1e293b"}}
|
|
2265
2214
|
#topbar{
|
|
2266
2215
|
height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2267
|
-
background
|
|
2268
|
-
}
|
|
2269
|
-
#topbar h1{font-size:15px;font-weight:600;color:#0f172a;letter-spacing:-0.01em}
|
|
2270
|
-
#topbar .nav-items{display:flex;gap:4px;margin-left:auto}
|
|
2271
|
-
#topbar .nav-item{
|
|
2272
|
-
padding:5px 12px;border-radius:6px;font-size:13px;cursor:pointer;
|
|
2273
|
-
color:#64748b;border:none;background:transparent;
|
|
2216
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};z-index:10;flex-shrink:0;
|
|
2274
2217
|
}
|
|
2275
|
-
#topbar
|
|
2276
|
-
#topbar .nav-item.active{background:#eff6ff;color:#2563eb;font-weight:500}
|
|
2218
|
+
#topbar h1{font-size:15px;font-weight:600;letter-spacing:-0.01em}
|
|
2277
2219
|
#search-box{
|
|
2278
|
-
display:flex;align-items:center;gap:8px;background
|
|
2279
|
-
border-radius:8px;padding:5px 10px;margin-left:
|
|
2220
|
+
display:flex;align-items:center;gap:8px;background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"};
|
|
2221
|
+
border-radius:8px;padding:5px 10px;margin-left:auto;
|
|
2280
2222
|
}
|
|
2281
2223
|
#search-box input{
|
|
2282
|
-
border:none;background:transparent;font-size:13px;outline:none;width:
|
|
2224
|
+
border:none;background:transparent;font-size:13px;outline:none;width:180px;color:inherit;
|
|
2283
2225
|
}
|
|
2284
2226
|
#search-box input::placeholder{color:#94a3b8}
|
|
2285
|
-
#search-icon{color:#94a3b8;font-size:14px}
|
|
2286
2227
|
#main{flex:1;display:flex;overflow:hidden;position:relative}
|
|
2287
2228
|
#canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2288
2229
|
#canvas-wrap.dragging{cursor:grabbing}
|
|
@@ -2290,47 +2231,38 @@ body{display:flex;flex-direction:column;background:#f8fafc;color:#1e293b}
|
|
|
2290
2231
|
canvas{display:block;width:100%;height:100%}
|
|
2291
2232
|
/* Detail panel */
|
|
2292
2233
|
#detail-panel{
|
|
2293
|
-
width:280px;background
|
|
2234
|
+
width:280px;background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-left:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2294
2235
|
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2295
2236
|
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2296
2237
|
}
|
|
2297
2238
|
#detail-panel.open{transform:translateX(0)}
|
|
2298
2239
|
#detail-panel .panel-header{
|
|
2299
|
-
padding:16px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:10px;
|
|
2240
|
+
padding:16px;border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};display:flex;align-items:center;gap:10px;
|
|
2300
2241
|
}
|
|
2301
2242
|
#detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2302
2243
|
#detail-panel .close-btn{
|
|
2303
2244
|
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2304
2245
|
color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2305
2246
|
}
|
|
2306
|
-
#detail-panel .close-btn:hover{background
|
|
2247
|
+
#detail-panel .close-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2307
2248
|
#detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2308
2249
|
#detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2309
2250
|
#detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
|
|
2310
|
-
#detail-panel .meta-value{font-size:13px;
|
|
2311
|
-
#detail-panel .quality-bar{height:6px;border-radius:3px;background
|
|
2251
|
+
#detail-panel .meta-value{font-size:13px;word-break:break-all}
|
|
2252
|
+
#detail-panel .quality-bar{height:6px;border-radius:3px;background:${meta.theme === "dark" ? "#334155" : "#e2e8f0"};margin-top:4px}
|
|
2312
2253
|
#detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2313
|
-
#detail-panel .badge{
|
|
2314
|
-
display:inline-flex;align-items:center;gap:4px;padding:2px 8px;
|
|
2315
|
-
border-radius:12px;font-size:11px;font-weight:500;
|
|
2316
|
-
}
|
|
2317
2254
|
/* Bottom-left toolbar */
|
|
2318
2255
|
#toolbar-left{
|
|
2319
2256
|
position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
|
|
2320
2257
|
}
|
|
2321
2258
|
.tb-btn{
|
|
2322
|
-
width:40px;height:40px;border-radius:10px;border:1px solid #e2e8f0;
|
|
2323
|
-
background
|
|
2259
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2260
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2324
2261
|
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2325
|
-
transition:all .15s;
|
|
2326
|
-
}
|
|
2327
|
-
.tb-btn:hover{border-color:#94a3b8;box-shadow:0 2px 8px rgba(0,0,0,.12)}
|
|
2328
|
-
.tb-btn.active{background:#eff6ff;border-color:#3b82f6}
|
|
2329
|
-
.tb-btn[title]:hover::after{
|
|
2330
|
-
content:attr(title);position:absolute;bottom:calc(100% + 6px);left:50%;
|
|
2331
|
-
transform:translateX(-50%);background:#1e293b;color:#fff;padding:4px 8px;
|
|
2332
|
-
border-radius:5px;font-size:11px;white-space:nowrap;pointer-events:none;
|
|
2262
|
+
transition:all .15s;color:inherit;
|
|
2333
2263
|
}
|
|
2264
|
+
.tb-btn:hover{border-color:#94a3b8}
|
|
2265
|
+
.tb-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6}
|
|
2334
2266
|
/* Bottom-right toolbar */
|
|
2335
2267
|
#toolbar-right{
|
|
2336
2268
|
position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
|
|
@@ -2338,116 +2270,106 @@ canvas{display:block;width:100%;height:100%}
|
|
|
2338
2270
|
}
|
|
2339
2271
|
#zoom-controls{display:flex;align-items:center;gap:6px}
|
|
2340
2272
|
.zoom-btn{
|
|
2341
|
-
width:34px;height:34px;border-radius:8px;border:1px solid #e2e8f0;
|
|
2342
|
-
background
|
|
2343
|
-
font-size:18px;color
|
|
2344
|
-
}
|
|
2345
|
-
.zoom-btn:hover{background:#f1f5f9}
|
|
2346
|
-
#zoom-pct{
|
|
2347
|
-
font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center;
|
|
2273
|
+
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2274
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2275
|
+
font-size:18px;color:inherit;display:flex;align-items:center;justify-content:center;
|
|
2348
2276
|
}
|
|
2277
|
+
.zoom-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2278
|
+
#zoom-pct{font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center}
|
|
2349
2279
|
#detail-selector{display:flex;flex-direction:column;gap:4px}
|
|
2350
2280
|
.detail-btn{
|
|
2351
|
-
width:34px;height:34px;border-radius:8px;border:1px solid #e2e8f0;
|
|
2352
|
-
background
|
|
2281
|
+
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2282
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2353
2283
|
font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
|
|
2354
2284
|
}
|
|
2355
|
-
.detail-btn:hover{background
|
|
2356
|
-
.detail-btn.active{background
|
|
2285
|
+
.detail-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2286
|
+
.detail-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6;color:#2563eb}
|
|
2357
2287
|
#connect-btn{
|
|
2358
|
-
width:40px;height:40px;border-radius:10px;border:1px solid #e2e8f0;
|
|
2359
|
-
background
|
|
2360
|
-
font-size:18px;display:flex;align-items:center;justify-content:center;
|
|
2288
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2289
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2290
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2361
2291
|
}
|
|
2362
2292
|
#connect-btn.active{background:#fef3c7;border-color:#f59e0b}
|
|
2363
2293
|
/* Tooltip */
|
|
2364
2294
|
#tooltip{
|
|
2365
2295
|
position:fixed;background:#1e293b;color:#fff;border-radius:8px;
|
|
2366
2296
|
padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
|
|
2367
|
-
display:none;max-width:
|
|
2297
|
+
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
|
2368
2298
|
}
|
|
2369
2299
|
#tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2370
2300
|
#tooltip .tt-domain{color:#94a3b8;font-size:11px}
|
|
2301
|
+
#tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2371
2302
|
/* Empty state */
|
|
2372
2303
|
#empty-state{
|
|
2373
2304
|
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2374
2305
|
align-items:center;justify-content:center;gap:12px;color:#94a3b8;
|
|
2375
2306
|
}
|
|
2376
|
-
#empty-state .es-icon{font-size:48px}
|
|
2377
2307
|
#empty-state p{font-size:14px}
|
|
2378
2308
|
/* Theme toggle */
|
|
2379
2309
|
#theme-btn{
|
|
2380
|
-
width:40px;height:40px;border-radius:10px;border:1px solid #e2e8f0;
|
|
2381
|
-
background
|
|
2382
|
-
font-size:18px;display:flex;align-items:center;justify-content:center;
|
|
2310
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2311
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2312
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2383
2313
|
}
|
|
2384
|
-
/* Dark mode overrides */
|
|
2314
|
+
/* Dark mode overrides (toggled via JS) */
|
|
2385
2315
|
body.dark{background:#0f172a;color:#e2e8f0}
|
|
2386
2316
|
body.dark #topbar{background:#1e293b;border-color:#334155}
|
|
2387
|
-
body.dark #topbar h1{color:#f1f5f9}
|
|
2388
|
-
body.dark #topbar .nav-item{color:#94a3b8}
|
|
2389
|
-
body.dark #topbar .nav-item:hover{background:#334155;color:#f1f5f9}
|
|
2390
2317
|
body.dark #search-box{background:#334155}
|
|
2391
|
-
body.dark #search-box input{color:#f1f5f9}
|
|
2392
2318
|
body.dark #detail-panel{background:#1e293b;border-color:#334155}
|
|
2393
|
-
body.dark #detail-panel .panel-header{border-color:#334155}
|
|
2394
|
-
body.dark #detail-panel .meta-value{color:#e2e8f0}
|
|
2395
2319
|
body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
|
|
2396
2320
|
background:#1e293b;border-color:#334155;color:#e2e8f0;
|
|
2397
2321
|
}
|
|
2398
|
-
|
|
2399
|
-
body.
|
|
2400
|
-
|
|
2322
|
+
/* Light mode overrides */
|
|
2323
|
+
body.light{background:#f8fafc;color:#1e293b}
|
|
2324
|
+
body.light #topbar{background:#fff;border-color:#e2e8f0}
|
|
2325
|
+
/* Connection hint */
|
|
2401
2326
|
#connect-hint{
|
|
2402
2327
|
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2403
2328
|
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2404
2329
|
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2405
2330
|
display:none;z-index:20;pointer-events:none;
|
|
2406
2331
|
}
|
|
2332
|
+
/* Screen reader only */
|
|
2333
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
|
|
2407
2334
|
</style>
|
|
2408
2335
|
</head>
|
|
2409
|
-
<body>
|
|
2336
|
+
<body class="${meta.theme}">
|
|
2410
2337
|
<!-- Top bar -->
|
|
2411
2338
|
<div id="topbar">
|
|
2412
|
-
<h1
|
|
2413
|
-
<div class="nav-items">
|
|
2414
|
-
<button class="nav-item active">Data Product Map</button>
|
|
2415
|
-
<button class="nav-item">Raw Data Map</button>
|
|
2416
|
-
<button class="nav-item">Analysis</button>
|
|
2417
|
-
</div>
|
|
2339
|
+
<h1>Data Cartography Map</h1>
|
|
2418
2340
|
<div id="search-box">
|
|
2419
|
-
<span
|
|
2420
|
-
<input id="search-input" type="text" placeholder="Search assets
|
|
2341
|
+
<span style="color:#94a3b8;font-size:14px">⌕</span>
|
|
2342
|
+
<input id="search-input" type="text" placeholder="Search assets..." aria-label="Search data assets"/>
|
|
2421
2343
|
</div>
|
|
2422
|
-
<button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme"
|
|
2344
|
+
<button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme">${meta.theme === "dark" ? "☼" : "☾"}</button>
|
|
2345
|
+
</div>
|
|
2346
|
+
<!-- SR summary -->
|
|
2347
|
+
<div class="sr-only" role="status" aria-live="polite" id="sr-summary">
|
|
2348
|
+
Data cartography map with ${assets.length} assets in ${clusters.length} clusters.
|
|
2423
2349
|
</div>
|
|
2424
|
-
|
|
2425
2350
|
<!-- Main area -->
|
|
2426
2351
|
<div id="main">
|
|
2427
2352
|
<div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
|
|
2428
2353
|
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
2429
|
-
${isEmpty ? '<div id="empty-state"><
|
|
2354
|
+
${isEmpty ? '<div id="empty-state"><p style="font-size:48px">🗺</p><p>No data assets available</p><p style="font-size:12px">Run <code>datasynx-cartography discover</code> to populate the map</p></div>' : ""}
|
|
2430
2355
|
</div>
|
|
2431
2356
|
<div id="detail-panel" role="complementary" aria-label="Asset details">
|
|
2432
2357
|
<div class="panel-header">
|
|
2433
|
-
<h3 id="dp-name"
|
|
2434
|
-
<button class="close-btn" id="dp-close" aria-label="Close panel"
|
|
2358
|
+
<h3 id="dp-name">—</h3>
|
|
2359
|
+
<button class="close-btn" id="dp-close" aria-label="Close panel">✕</button>
|
|
2435
2360
|
</div>
|
|
2436
2361
|
<div class="panel-body" id="dp-body"></div>
|
|
2437
2362
|
</div>
|
|
2438
2363
|
</div>
|
|
2439
|
-
|
|
2440
2364
|
<!-- Bottom-left toolbar -->
|
|
2441
2365
|
<div id="toolbar-left">
|
|
2442
|
-
<button class="tb-btn active" id="btn-
|
|
2443
|
-
<button class="tb-btn
|
|
2444
|
-
<button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">\u{1F441}</button>
|
|
2366
|
+
<button class="tb-btn active" id="btn-labels" title="Show labels" aria-pressed="true" aria-label="Toggle labels">🏷</button>
|
|
2367
|
+
<button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">👁</button>
|
|
2445
2368
|
</div>
|
|
2446
|
-
|
|
2447
2369
|
<!-- Bottom-right toolbar -->
|
|
2448
2370
|
<div id="toolbar-right">
|
|
2449
2371
|
<div id="zoom-controls">
|
|
2450
|
-
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out"
|
|
2372
|
+
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out">−</button>
|
|
2451
2373
|
<span id="zoom-pct">100%</span>
|
|
2452
2374
|
<button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
|
|
2453
2375
|
</div>
|
|
@@ -2457,16 +2379,15 @@ body.dark #zoom-pct{color:#94a3b8}
|
|
|
2457
2379
|
<button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
|
|
2458
2380
|
<button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
|
|
2459
2381
|
</div>
|
|
2460
|
-
<button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool"
|
|
2382
|
+
<button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">🔗</button>
|
|
2461
2383
|
</div>
|
|
2462
|
-
|
|
2463
|
-
<!-- Connection mode hint -->
|
|
2384
|
+
<!-- Connection hint -->
|
|
2464
2385
|
<div id="connect-hint">Click two assets to create a connection</div>
|
|
2465
|
-
|
|
2466
|
-
<!-- Hover tooltip -->
|
|
2386
|
+
<!-- Tooltip -->
|
|
2467
2387
|
<div id="tooltip" role="tooltip">
|
|
2468
2388
|
<div class="tt-name" id="tt-name"></div>
|
|
2469
2389
|
<div class="tt-domain" id="tt-domain"></div>
|
|
2390
|
+
<div class="tt-quality" id="tt-quality"></div>
|
|
2470
2391
|
</div>
|
|
2471
2392
|
|
|
2472
2393
|
<script>
|
|
@@ -2474,26 +2395,28 @@ body.dark #zoom-pct{color:#94a3b8}
|
|
|
2474
2395
|
'use strict';
|
|
2475
2396
|
|
|
2476
2397
|
// \u2500\u2500 Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2477
|
-
const
|
|
2478
|
-
const
|
|
2479
|
-
const CONNECTIONS = ${connectionsJson};
|
|
2480
|
-
const HEX_SIZE = ${hexSizeVal};
|
|
2398
|
+
const MAP = ${dataJson};
|
|
2399
|
+
const HEX_SIZE = ${HEX_SIZE2};
|
|
2481
2400
|
const IS_EMPTY = ${isEmpty};
|
|
2482
2401
|
|
|
2483
|
-
// Build
|
|
2402
|
+
// Build asset index
|
|
2484
2403
|
const assetIndex = new Map();
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2404
|
+
const clusterByAsset = new Map();
|
|
2405
|
+
for (const c of MAP.clusters) {
|
|
2406
|
+
for (const aid of c.assetIds) {
|
|
2407
|
+
clusterByAsset.set(aid, c);
|
|
2488
2408
|
}
|
|
2489
2409
|
}
|
|
2410
|
+
for (const a of MAP.assets) {
|
|
2411
|
+
assetIndex.set(a.id, a);
|
|
2412
|
+
}
|
|
2490
2413
|
|
|
2491
|
-
// \u2500\u2500 Canvas
|
|
2414
|
+
// \u2500\u2500 Canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2492
2415
|
const canvas = document.getElementById('hexmap');
|
|
2493
2416
|
const ctx = canvas.getContext('2d');
|
|
2494
2417
|
const wrap = document.getElementById('canvas-wrap');
|
|
2495
|
-
|
|
2496
2418
|
let W = 0, H = 0;
|
|
2419
|
+
|
|
2497
2420
|
function resize() {
|
|
2498
2421
|
const dpr = window.devicePixelRatio || 1;
|
|
2499
2422
|
W = wrap.clientWidth; H = wrap.clientHeight;
|
|
@@ -2504,140 +2427,123 @@ function resize() {
|
|
|
2504
2427
|
}
|
|
2505
2428
|
window.addEventListener('resize', resize);
|
|
2506
2429
|
|
|
2507
|
-
// \u2500\u2500 Viewport
|
|
2430
|
+
// \u2500\u2500 Viewport \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2508
2431
|
let vx = 0, vy = 0, scale = 1;
|
|
2509
|
-
let detailLevel = 2;
|
|
2510
|
-
let
|
|
2511
|
-
let
|
|
2512
|
-
let
|
|
2513
|
-
let isDark = false;
|
|
2514
|
-
let connectMode = false;
|
|
2515
|
-
let connectFirst = null;
|
|
2516
|
-
let hoveredAssetId = null;
|
|
2517
|
-
let selectedAssetId = null;
|
|
2432
|
+
let detailLevel = 2, showLabels = true, showQuality = false;
|
|
2433
|
+
let isDark = document.body.classList.contains('dark');
|
|
2434
|
+
let connectMode = false, connectFirst = null;
|
|
2435
|
+
let hoveredAssetId = null, selectedAssetId = null;
|
|
2518
2436
|
let searchQuery = '';
|
|
2519
|
-
let localConnections = [...
|
|
2437
|
+
let localConnections = [...MAP.connections];
|
|
2438
|
+
|
|
2439
|
+
// Flat-top hex math
|
|
2440
|
+
function htp_x(q, r) { return HEX_SIZE * (3/2 * q); }
|
|
2441
|
+
function htp_y(q, r) { return HEX_SIZE * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r); }
|
|
2442
|
+
function w2s(wx, wy) { return { x: wx*scale+vx, y: wy*scale+vy }; }
|
|
2443
|
+
function s2w(sx, sy) { return { x: (sx-vx)/scale, y: (sy-vy)/scale }; }
|
|
2520
2444
|
|
|
2521
2445
|
function fitToView() {
|
|
2522
|
-
if (IS_EMPTY ||
|
|
2523
|
-
let
|
|
2524
|
-
for (const
|
|
2525
|
-
const px
|
|
2526
|
-
|
|
2527
|
-
if(px<minX)minX=px; if(py<minY)minY=py;
|
|
2528
|
-
if(px>maxX)maxX=px; if(py>maxY)maxY=py;
|
|
2446
|
+
if (IS_EMPTY || MAP.assets.length === 0) { vx = 0; vy = 0; scale = 1; return; }
|
|
2447
|
+
let mnx=Infinity,mny=Infinity,mxx=-Infinity,mxy=-Infinity;
|
|
2448
|
+
for (const a of MAP.assets) {
|
|
2449
|
+
const px=htp_x(a.q,a.r), py=htp_y(a.q,a.r);
|
|
2450
|
+
if(px<mnx)mnx=px;if(py<mny)mny=py;if(px>mxx)mxx=px;if(py>mxy)mxy=py;
|
|
2529
2451
|
}
|
|
2530
|
-
const pw
|
|
2531
|
-
scale = Math.min(W/pw, H/ph,
|
|
2532
|
-
vx = W/2 - ((
|
|
2533
|
-
vy = H/2 - ((
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
// \u2500\u2500 Hex math (inline for self-contained HTML) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2537
|
-
function hexToPixelX(q, r) { return HEX_SIZE * (Math.sqrt(3)*q + Math.sqrt(3)/2*r); }
|
|
2538
|
-
function hexToPixelY(q, r) { return HEX_SIZE * (3/2*r); }
|
|
2539
|
-
|
|
2540
|
-
function worldToScreen(wx, wy) {
|
|
2541
|
-
return { x: wx*scale+vx, y: wy*scale+vy };
|
|
2542
|
-
}
|
|
2543
|
-
function screenToWorld(sx, sy) {
|
|
2544
|
-
return { x: (sx-vx)/scale, y: (sy-vy)/scale };
|
|
2452
|
+
const pw=mxx-mnx+HEX_SIZE*4, ph=mxy-mny+HEX_SIZE*4;
|
|
2453
|
+
scale = Math.min(W/pw, H/ph, 2) * 0.85;
|
|
2454
|
+
vx = W/2 - ((mnx+mxx)/2)*scale;
|
|
2455
|
+
vy = H/2 - ((mny+mxy)/2)*scale;
|
|
2545
2456
|
}
|
|
2546
2457
|
|
|
2547
2458
|
// \u2500\u2500 Drawing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2548
2459
|
function hexPath(cx, cy, r) {
|
|
2549
2460
|
ctx.beginPath();
|
|
2550
2461
|
for (let i=0;i<6;i++) {
|
|
2551
|
-
const angle = Math.PI/180*(60*i
|
|
2552
|
-
const x
|
|
2553
|
-
i===0
|
|
2462
|
+
const angle = Math.PI/180*(60*i);
|
|
2463
|
+
const x=cx+r*Math.cos(angle), y=cy+r*Math.sin(angle);
|
|
2464
|
+
i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
|
|
2554
2465
|
}
|
|
2555
2466
|
ctx.closePath();
|
|
2556
2467
|
}
|
|
2557
2468
|
|
|
2558
|
-
function
|
|
2559
|
-
|
|
2469
|
+
function shadeV(hex, amt) {
|
|
2470
|
+
if(!hex||hex.length<7)return hex;
|
|
2471
|
+
const n=parseInt(hex.replace('#',''),16);
|
|
2472
|
+
const r=Math.min(255,(n>>16)+amt), g=Math.min(255,((n>>8)&0xff)+amt), b=Math.min(255,(n&0xff)+amt);
|
|
2473
|
+
return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0');
|
|
2474
|
+
}
|
|
2560
2475
|
|
|
2561
|
-
|
|
2476
|
+
function draw() {
|
|
2477
|
+
ctx.clearRect(0,0,W,H);
|
|
2562
2478
|
ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
|
|
2563
|
-
ctx.fillRect(0,
|
|
2564
|
-
|
|
2479
|
+
ctx.fillRect(0,0,W,H);
|
|
2565
2480
|
if (IS_EMPTY) return;
|
|
2566
2481
|
|
|
2567
2482
|
const size = HEX_SIZE * scale;
|
|
2568
2483
|
const matchedIds = getSearchMatches();
|
|
2569
2484
|
const hasSearch = searchQuery.length > 0;
|
|
2570
2485
|
|
|
2571
|
-
//
|
|
2486
|
+
// Draw connections
|
|
2572
2487
|
ctx.save();
|
|
2573
|
-
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.
|
|
2488
|
+
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
2574
2489
|
ctx.lineWidth = 1.5;
|
|
2575
2490
|
ctx.setLineDash([4,4]);
|
|
2576
2491
|
for (const conn of localConnections) {
|
|
2577
2492
|
const src = assetIndex.get(conn.sourceAssetId);
|
|
2578
2493
|
const tgt = assetIndex.get(conn.targetAssetId);
|
|
2579
|
-
if (!src
|
|
2580
|
-
const sp
|
|
2581
|
-
const tp
|
|
2582
|
-
ctx.beginPath();
|
|
2494
|
+
if (!src||!tgt) continue;
|
|
2495
|
+
const sp=w2s(htp_x(src.q,src.r),htp_y(src.q,src.r));
|
|
2496
|
+
const tp=w2s(htp_x(tgt.q,tgt.r),htp_y(tgt.q,tgt.r));
|
|
2497
|
+
ctx.beginPath();ctx.moveTo(sp.x,sp.y);ctx.lineTo(tp.x,tp.y);ctx.stroke();
|
|
2583
2498
|
}
|
|
2584
2499
|
ctx.setLineDash([]);
|
|
2585
2500
|
ctx.restore();
|
|
2586
2501
|
|
|
2587
|
-
//
|
|
2588
|
-
for (const cluster of
|
|
2502
|
+
// Draw hexagons per cluster
|
|
2503
|
+
for (const cluster of MAP.clusters) {
|
|
2589
2504
|
const baseColor = cluster.color;
|
|
2590
|
-
const
|
|
2591
|
-
|
|
2592
|
-
);
|
|
2505
|
+
const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
|
|
2506
|
+
const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
|
|
2593
2507
|
const clusterDim = hasSearch && !isClusterMatch;
|
|
2594
2508
|
|
|
2595
|
-
for (let ai=0; ai<
|
|
2596
|
-
const asset =
|
|
2597
|
-
const wx
|
|
2598
|
-
const
|
|
2599
|
-
const s =
|
|
2600
|
-
const cx = s.x, cy = s.y;
|
|
2509
|
+
for (let ai=0; ai<clusterAssets.length; ai++) {
|
|
2510
|
+
const asset = clusterAssets[ai];
|
|
2511
|
+
const wx=htp_x(asset.q,asset.r), wy=htp_y(asset.q,asset.r);
|
|
2512
|
+
const s=w2s(wx,wy);
|
|
2513
|
+
const cx=s.x, cy=s.y;
|
|
2601
2514
|
|
|
2602
2515
|
// Frustum cull
|
|
2603
|
-
if
|
|
2516
|
+
if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
|
|
2604
2517
|
|
|
2605
|
-
// Shade variation
|
|
2606
|
-
const shade = ai%3===0
|
|
2607
|
-
let fillColor =
|
|
2518
|
+
// Shade variation
|
|
2519
|
+
const shade = ai%3===0?18:ai%3===1?8:0;
|
|
2520
|
+
let fillColor = shadeV(baseColor, shade);
|
|
2608
2521
|
|
|
2609
|
-
// Quality
|
|
2610
|
-
if (showQuality && asset.qualityScore !== null) {
|
|
2522
|
+
// Quality overlay
|
|
2523
|
+
if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
2611
2524
|
const q = asset.qualityScore;
|
|
2612
2525
|
if (q < 40) fillColor = '#ef4444';
|
|
2613
2526
|
else if (q < 70) fillColor = '#f97316';
|
|
2614
2527
|
}
|
|
2615
2528
|
|
|
2616
|
-
// Dim non-matching in search
|
|
2617
2529
|
const alpha = clusterDim ? 0.18 : 1;
|
|
2618
|
-
|
|
2619
|
-
// Hover / selected highlight
|
|
2620
2530
|
const isHovered = asset.id === hoveredAssetId;
|
|
2621
2531
|
const isSelected = asset.id === selectedAssetId;
|
|
2622
2532
|
const isConnectFirst = asset.id === connectFirst;
|
|
2623
2533
|
|
|
2624
2534
|
ctx.save();
|
|
2625
2535
|
ctx.globalAlpha = alpha;
|
|
2626
|
-
|
|
2627
2536
|
hexPath(cx, cy, size*0.92);
|
|
2628
2537
|
|
|
2629
|
-
if (isDark) {
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
ctx.shadowColor = fillColor;
|
|
2633
|
-
ctx.shadowBlur = isSelected ? 16 : 8;
|
|
2634
|
-
}
|
|
2538
|
+
if (isDark && (isHovered||isSelected||isConnectFirst)) {
|
|
2539
|
+
ctx.shadowColor = fillColor;
|
|
2540
|
+
ctx.shadowBlur = isSelected ? 16 : 8;
|
|
2635
2541
|
}
|
|
2636
2542
|
|
|
2637
2543
|
ctx.fillStyle = fillColor;
|
|
2638
2544
|
ctx.fill();
|
|
2639
2545
|
|
|
2640
|
-
if (isSelected
|
|
2546
|
+
if (isSelected||isConnectFirst) {
|
|
2641
2547
|
ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
|
|
2642
2548
|
ctx.lineWidth = 2.5;
|
|
2643
2549
|
ctx.stroke();
|
|
@@ -2650,352 +2556,1389 @@ function draw() {
|
|
|
2650
2556
|
ctx.lineWidth = 1;
|
|
2651
2557
|
ctx.stroke();
|
|
2652
2558
|
}
|
|
2653
|
-
|
|
2654
2559
|
ctx.restore();
|
|
2655
2560
|
|
|
2656
|
-
// Quality dot
|
|
2657
|
-
if (showQuality && asset.qualityScore
|
|
2561
|
+
// Quality dot
|
|
2562
|
+
if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
|
|
2658
2563
|
const q = asset.qualityScore;
|
|
2659
2564
|
if (q < 70) {
|
|
2660
2565
|
ctx.beginPath();
|
|
2661
|
-
ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,
|
|
2662
|
-
ctx.fillStyle = q<40
|
|
2566
|
+
ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,size*0.14), 0, Math.PI*2);
|
|
2567
|
+
ctx.fillStyle = q<40?'#ef4444':'#f97316';
|
|
2663
2568
|
ctx.fill();
|
|
2664
2569
|
}
|
|
2665
2570
|
}
|
|
2666
2571
|
|
|
2667
|
-
// Asset
|
|
2668
|
-
const showAssetLabel = showLabels && !clusterDim &&
|
|
2669
|
-
(detailLevel
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
if (showAssetLabel && size > 14) {
|
|
2673
|
-
const label = asset.name.length > 12 ? asset.name.substring(0,11)+'\u2026' : asset.name;
|
|
2572
|
+
// Asset labels (detail 4, or 3 at high zoom)
|
|
2573
|
+
const showAssetLabel = showLabels && !clusterDim &&
|
|
2574
|
+
((detailLevel>=4)||(detailLevel===3 && scale>=0.8));
|
|
2575
|
+
if (showAssetLabel && size>14) {
|
|
2576
|
+
const label = asset.name.length>12 ? asset.name.substring(0,11)+'...' : asset.name;
|
|
2674
2577
|
ctx.save();
|
|
2675
|
-
ctx.font =
|
|
2578
|
+
ctx.font = Math.max(8,Math.min(11,size*0.38))+'px -apple-system,sans-serif';
|
|
2676
2579
|
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
2677
|
-
ctx.textAlign
|
|
2678
|
-
ctx.textBaseline = 'middle';
|
|
2580
|
+
ctx.textAlign='center';ctx.textBaseline='middle';
|
|
2679
2581
|
ctx.fillText(label, cx, cy);
|
|
2680
2582
|
ctx.restore();
|
|
2681
2583
|
}
|
|
2682
2584
|
}
|
|
2683
2585
|
}
|
|
2684
2586
|
|
|
2685
|
-
//
|
|
2686
|
-
if (showLabels && detailLevel
|
|
2687
|
-
for (const cluster of
|
|
2688
|
-
if (cluster.
|
|
2689
|
-
|
|
2690
|
-
const
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
const s = worldToScreen(cluster.centroid.x, cluster.centroid.y);
|
|
2694
|
-
drawPillLabel(s.x, s.y, cluster.label, cluster.color, 14, isDark);
|
|
2587
|
+
// Cluster labels (pill badges)
|
|
2588
|
+
if (showLabels && detailLevel>=1) {
|
|
2589
|
+
for (const cluster of MAP.clusters) {
|
|
2590
|
+
if (cluster.assetIds.length===0) continue;
|
|
2591
|
+
if (hasSearch && !cluster.assetIds.some(id=>matchedIds.has(id))) continue;
|
|
2592
|
+
const s=w2s(cluster.centroid.x, cluster.centroid.y);
|
|
2593
|
+
drawPill(s.x, s.y-size*1.2, cluster.label, cluster.color, 14);
|
|
2695
2594
|
}
|
|
2696
2595
|
}
|
|
2697
2596
|
|
|
2698
|
-
//
|
|
2699
|
-
if (showLabels && detailLevel
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2597
|
+
// Sub-domain labels (detail 2+)
|
|
2598
|
+
if (showLabels && detailLevel>=2) {
|
|
2599
|
+
const subGroups = new Map();
|
|
2600
|
+
for (const a of MAP.assets) {
|
|
2601
|
+
if (!a.subDomain) continue;
|
|
2602
|
+
const key = a.domain+'|'+a.subDomain;
|
|
2603
|
+
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
2604
|
+
subGroups.get(key).push(a);
|
|
2605
|
+
}
|
|
2606
|
+
for (const [, group] of subGroups) {
|
|
2607
|
+
let sx=0,sy=0;
|
|
2608
|
+
for (const a of group) { sx+=htp_x(a.q,a.r); sy+=htp_y(a.q,a.r); }
|
|
2609
|
+
const cx=sx/group.length, cy=sy/group.length;
|
|
2610
|
+
const s = w2s(cx, cy);
|
|
2611
|
+
drawPill(s.x, s.y+size*1.5, group[0].subDomain, '#64748b', 11);
|
|
2706
2612
|
}
|
|
2707
2613
|
}
|
|
2708
2614
|
}
|
|
2709
2615
|
|
|
2710
|
-
function
|
|
2711
|
-
if
|
|
2712
|
-
const num = parseInt(hex.replace('#',''), 16);
|
|
2713
|
-
const r = Math.min(255, (num>>16) + amount);
|
|
2714
|
-
const g = Math.min(255, ((num>>8)&0xff) + amount);
|
|
2715
|
-
const b = Math.min(255, (num&0xff) + amount);
|
|
2716
|
-
return '#' + r.toString(16).padStart(2,'0') + g.toString(16).padStart(2,'0') + b.toString(16).padStart(2,'0');
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
function drawPillLabel(x, y, text, color, fontSize, dark) {
|
|
2720
|
-
if (!text) return;
|
|
2616
|
+
function drawPill(x, y, text, color, fontSize) {
|
|
2617
|
+
if(!text) return;
|
|
2721
2618
|
ctx.save();
|
|
2722
|
-
ctx.font =
|
|
2723
|
-
const tw
|
|
2724
|
-
const ph
|
|
2725
|
-
// Pill background
|
|
2619
|
+
ctx.font = '600 '+fontSize+'px -apple-system,sans-serif';
|
|
2620
|
+
const tw=ctx.measureText(text).width;
|
|
2621
|
+
const ph=fontSize+8, pw=tw+20;
|
|
2726
2622
|
ctx.beginPath();
|
|
2727
|
-
ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
|
|
2728
|
-
ctx.
|
|
2729
|
-
ctx.
|
|
2730
|
-
ctx.shadowBlur
|
|
2731
|
-
ctx.fill();
|
|
2732
|
-
ctx.
|
|
2733
|
-
|
|
2734
|
-
ctx.fillStyle = dark ? '#e2e8f0' : '#0f172a';
|
|
2735
|
-
ctx.textAlign = 'center';
|
|
2736
|
-
ctx.textBaseline = 'middle';
|
|
2623
|
+
if (ctx.roundRect) ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
|
|
2624
|
+
else { ctx.rect(x-pw/2, y-ph/2, pw, ph); }
|
|
2625
|
+
ctx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
2626
|
+
ctx.shadowColor='rgba(0,0,0,0.15)'; ctx.shadowBlur=6;
|
|
2627
|
+
ctx.fill(); ctx.shadowBlur=0;
|
|
2628
|
+
ctx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
2629
|
+
ctx.textAlign='center'; ctx.textBaseline='middle';
|
|
2737
2630
|
ctx.fillText(text, x, y);
|
|
2738
2631
|
ctx.restore();
|
|
2739
2632
|
}
|
|
2740
2633
|
|
|
2741
2634
|
// \u2500\u2500 Hit testing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2742
|
-
function
|
|
2743
|
-
const w
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
const dx = Math.abs(w.x-wx), dy = Math.abs(w.y-wy);
|
|
2750
|
-
const hw = Math.sqrt(3)/2*size;
|
|
2751
|
-
if (dx > hw || dy > size) continue;
|
|
2752
|
-
if (hw*size - size*dx - (hw/2)*dy >= 0) return asset;
|
|
2753
|
-
}
|
|
2635
|
+
function getAssetAt(sx, sy) {
|
|
2636
|
+
const w=s2w(sx,sy);
|
|
2637
|
+
for (const a of MAP.assets) {
|
|
2638
|
+
const wx=htp_x(a.q,a.r), wy=htp_y(a.q,a.r);
|
|
2639
|
+
const dx=Math.abs(w.x-wx), dy=Math.abs(w.y-wy);
|
|
2640
|
+
if (dx>HEX_SIZE||dy>HEX_SIZE) continue;
|
|
2641
|
+
if (dx*dx+dy*dy < HEX_SIZE*HEX_SIZE) return a;
|
|
2754
2642
|
}
|
|
2755
2643
|
return null;
|
|
2756
2644
|
}
|
|
2757
2645
|
|
|
2758
2646
|
// \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2759
2647
|
function getSearchMatches() {
|
|
2760
|
-
if
|
|
2761
|
-
const q
|
|
2762
|
-
const
|
|
2763
|
-
for
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
(a.domain && a.domain.toLowerCase().includes(q)) ||
|
|
2767
|
-
(a.subDomain && a.subDomain.toLowerCase().includes(q))) {
|
|
2768
|
-
matches.add(a.id);
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2648
|
+
if(!searchQuery) return new Set();
|
|
2649
|
+
const q=searchQuery.toLowerCase();
|
|
2650
|
+
const m=new Set();
|
|
2651
|
+
for(const a of MAP.assets){
|
|
2652
|
+
if(a.name.toLowerCase().includes(q)||(a.domain&&a.domain.toLowerCase().includes(q))||
|
|
2653
|
+
(a.subDomain&&a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
2771
2654
|
}
|
|
2772
|
-
return
|
|
2655
|
+
return m;
|
|
2773
2656
|
}
|
|
2774
2657
|
|
|
2775
2658
|
// \u2500\u2500 Pan & Zoom \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2776
|
-
let dragging
|
|
2659
|
+
let dragging=false, lastMX=0, lastMY=0;
|
|
2777
2660
|
|
|
2778
|
-
wrap.addEventListener('mousedown', e
|
|
2779
|
-
if
|
|
2780
|
-
dragging
|
|
2661
|
+
wrap.addEventListener('mousedown', e=>{
|
|
2662
|
+
if(e.button!==0)return;
|
|
2663
|
+
dragging=true; lastMX=e.clientX; lastMY=e.clientY;
|
|
2781
2664
|
wrap.classList.add('dragging');
|
|
2782
2665
|
});
|
|
2783
|
-
window.addEventListener('mouseup', ()
|
|
2784
|
-
window.addEventListener('mousemove', e
|
|
2785
|
-
if
|
|
2786
|
-
vx
|
|
2787
|
-
lastMX
|
|
2788
|
-
draw();
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
const rect = wrap.getBoundingClientRect();
|
|
2792
|
-
const sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
2793
|
-
const asset = getAssetAtScreen(sx, sy);
|
|
2794
|
-
const newId = asset ? asset.id : null;
|
|
2795
|
-
if (newId !== hoveredAssetId) { hoveredAssetId = newId; draw(); }
|
|
2796
|
-
if (asset) {
|
|
2797
|
-
const tooltip = document.getElementById('tooltip');
|
|
2798
|
-
document.getElementById('tt-name').textContent = asset.name;
|
|
2799
|
-
document.getElementById('tt-domain').textContent = asset.domain + (asset.subDomain ? ' \u203A '+asset.subDomain : '');
|
|
2800
|
-
tooltip.style.display = 'block';
|
|
2801
|
-
tooltip.style.left = (e.clientX+12)+'px';
|
|
2802
|
-
tooltip.style.top = (e.clientY-8)+'px';
|
|
2803
|
-
} else {
|
|
2804
|
-
document.getElementById('tooltip').style.display = 'none';
|
|
2666
|
+
window.addEventListener('mouseup', ()=>{dragging=false;wrap.classList.remove('dragging');});
|
|
2667
|
+
window.addEventListener('mousemove', e=>{
|
|
2668
|
+
if(dragging){
|
|
2669
|
+
vx+=e.clientX-lastMX; vy+=e.clientY-lastMY;
|
|
2670
|
+
lastMX=e.clientX; lastMY=e.clientY; draw(); return;
|
|
2805
2671
|
}
|
|
2672
|
+
const rect=wrap.getBoundingClientRect();
|
|
2673
|
+
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
2674
|
+
const asset=getAssetAt(sx,sy);
|
|
2675
|
+
const newId=asset?asset.id:null;
|
|
2676
|
+
if(newId!==hoveredAssetId){hoveredAssetId=newId;draw();}
|
|
2677
|
+
const tt=document.getElementById('tooltip');
|
|
2678
|
+
if(asset){
|
|
2679
|
+
document.getElementById('tt-name').textContent=asset.name;
|
|
2680
|
+
document.getElementById('tt-domain').textContent=asset.domain+(asset.subDomain?' > '+asset.subDomain:'');
|
|
2681
|
+
document.getElementById('tt-quality').textContent=asset.qualityScore!==null?'Quality: '+asset.qualityScore+'/100':'';
|
|
2682
|
+
tt.style.display='block';tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY-8)+'px';
|
|
2683
|
+
} else { tt.style.display='none'; }
|
|
2806
2684
|
});
|
|
2807
2685
|
|
|
2808
|
-
wrap.addEventListener('click', e
|
|
2809
|
-
const rect
|
|
2810
|
-
const sx
|
|
2811
|
-
const asset
|
|
2812
|
-
if
|
|
2813
|
-
if
|
|
2814
|
-
if
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
const conn = {id: crypto.randomUUID(), sourceAssetId: connectFirst, targetAssetId: asset.id, type:'connection'};
|
|
2819
|
-
localConnections.push(conn);
|
|
2820
|
-
connectFirst = null;
|
|
2821
|
-
draw();
|
|
2686
|
+
wrap.addEventListener('click', e=>{
|
|
2687
|
+
const rect=wrap.getBoundingClientRect();
|
|
2688
|
+
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
2689
|
+
const asset=getAssetAt(sx,sy);
|
|
2690
|
+
if(connectMode){
|
|
2691
|
+
if(!asset) return;
|
|
2692
|
+
if(!connectFirst){connectFirst=asset.id;draw();}
|
|
2693
|
+
else if(connectFirst!==asset.id){
|
|
2694
|
+
localConnections.push({id:crypto.randomUUID(),sourceAssetId:connectFirst,targetAssetId:asset.id,type:'connection'});
|
|
2695
|
+
connectFirst=null;draw();
|
|
2822
2696
|
}
|
|
2823
2697
|
return;
|
|
2824
2698
|
}
|
|
2825
|
-
if
|
|
2826
|
-
|
|
2827
|
-
showDetailPanel(asset);
|
|
2828
|
-
} else {
|
|
2829
|
-
selectedAssetId = null;
|
|
2830
|
-
document.getElementById('detail-panel').classList.remove('open');
|
|
2831
|
-
}
|
|
2699
|
+
if(asset){selectedAssetId=asset.id;showDetailPanel(asset);}
|
|
2700
|
+
else{selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');}
|
|
2832
2701
|
draw();
|
|
2833
2702
|
});
|
|
2834
2703
|
|
|
2835
|
-
// Touch
|
|
2836
|
-
let lastTouches
|
|
2837
|
-
wrap.addEventListener('touchstart',
|
|
2838
|
-
wrap.addEventListener('touchmove',
|
|
2839
|
-
if
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
const
|
|
2846
|
-
const
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
wrap.addEventListener('wheel', e => {
|
|
2704
|
+
// Touch
|
|
2705
|
+
let lastTouches=[];
|
|
2706
|
+
wrap.addEventListener('touchstart',e=>{lastTouches=[...e.touches];},{passive:true});
|
|
2707
|
+
wrap.addEventListener('touchmove',e=>{
|
|
2708
|
+
if(e.touches.length===1){
|
|
2709
|
+
vx+=e.touches[0].clientX-lastTouches[0].clientX;
|
|
2710
|
+
vy+=e.touches[0].clientY-lastTouches[0].clientY;draw();
|
|
2711
|
+
} else if(e.touches.length===2){
|
|
2712
|
+
const d0=Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX,lastTouches[0].clientY-lastTouches[1].clientY);
|
|
2713
|
+
const d1=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
|
|
2714
|
+
const mx=(e.touches[0].clientX+e.touches[1].clientX)/2;
|
|
2715
|
+
const my=(e.touches[0].clientY+e.touches[1].clientY)/2;
|
|
2716
|
+
applyZoom(d1/d0,mx,my);
|
|
2717
|
+
}
|
|
2718
|
+
lastTouches=[...e.touches];
|
|
2719
|
+
},{passive:true});
|
|
2720
|
+
|
|
2721
|
+
wrap.addEventListener('wheel',e=>{
|
|
2855
2722
|
e.preventDefault();
|
|
2856
|
-
const rect
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
const
|
|
2863
|
-
|
|
2864
|
-
scale
|
|
2865
|
-
vx = sx - wx*scale; vy = sy - wy*scale;
|
|
2866
|
-
document.getElementById('zoom-pct').textContent = Math.round(scale*100)+'%';
|
|
2867
|
-
draw();
|
|
2723
|
+
const rect=wrap.getBoundingClientRect();
|
|
2724
|
+
applyZoom(e.deltaY<0?1.12:1/1.12,e.clientX-rect.left,e.clientY-rect.top);
|
|
2725
|
+
},{passive:false});
|
|
2726
|
+
|
|
2727
|
+
function applyZoom(factor,sx,sy){
|
|
2728
|
+
const ns=Math.max(0.05,Math.min(8,scale*factor));
|
|
2729
|
+
const wx=(sx-vx)/scale,wy=(sy-vy)/scale;
|
|
2730
|
+
scale=ns;vx=sx-wx*scale;vy=sy-wy*scale;
|
|
2731
|
+
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';draw();
|
|
2868
2732
|
}
|
|
2869
|
-
|
|
2870
|
-
document.getElementById('zoom-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
if
|
|
2877
|
-
else if
|
|
2878
|
-
else if
|
|
2879
|
-
else if
|
|
2880
|
-
else if
|
|
2881
|
-
else if
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
document.getElementById('detail-panel').classList.remove('open');
|
|
2885
|
-
if (connectMode) toggleConnectMode();
|
|
2886
|
-
draw();
|
|
2733
|
+
document.getElementById('zoom-in').addEventListener('click',()=>applyZoom(1.25,W/2,H/2));
|
|
2734
|
+
document.getElementById('zoom-out').addEventListener('click',()=>applyZoom(1/1.25,W/2,H/2));
|
|
2735
|
+
|
|
2736
|
+
// Keyboard
|
|
2737
|
+
wrap.addEventListener('keydown',e=>{
|
|
2738
|
+
const step=40;
|
|
2739
|
+
if(e.key==='ArrowLeft'){vx+=step;draw();}
|
|
2740
|
+
else if(e.key==='ArrowRight'){vx-=step;draw();}
|
|
2741
|
+
else if(e.key==='ArrowUp'){vy+=step;draw();}
|
|
2742
|
+
else if(e.key==='ArrowDown'){vy-=step;draw();}
|
|
2743
|
+
else if(e.key==='+'||e.key==='=')applyZoom(1.2,W/2,H/2);
|
|
2744
|
+
else if(e.key==='-')applyZoom(1/1.2,W/2,H/2);
|
|
2745
|
+
else if(e.key==='Escape'){
|
|
2746
|
+
selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');
|
|
2747
|
+
if(connectMode)toggleConnect();draw();
|
|
2887
2748
|
}
|
|
2888
2749
|
});
|
|
2889
2750
|
|
|
2890
2751
|
// \u2500\u2500 Detail Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2891
2752
|
function showDetailPanel(asset) {
|
|
2892
|
-
document.getElementById('dp-name').textContent
|
|
2893
|
-
const body
|
|
2894
|
-
const rows
|
|
2895
|
-
['
|
|
2896
|
-
[
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
<div class="meta-value">\${value}</div>
|
|
2905
|
-
</div>\`).join('');
|
|
2906
|
-
|
|
2907
|
-
// Connections
|
|
2908
|
-
const related = localConnections.filter(c => c.sourceAssetId===asset.id || c.targetAssetId===asset.id);
|
|
2909
|
-
if (related.length > 0) {
|
|
2910
|
-
body.innerHTML += \`<div class="meta-row"><div class="meta-label">Connections (\${related.length})</div><div>\${
|
|
2911
|
-
related.map(c => {
|
|
2912
|
-
const otherId = c.sourceAssetId===asset.id ? c.targetAssetId : c.sourceAssetId;
|
|
2913
|
-
const other = assetIndex.get(otherId);
|
|
2914
|
-
return \`<div class="meta-value" style="margin-top:4px;font-size:12px">\${other ? escHtml(other.name) : otherId}</div>\`;
|
|
2915
|
-
}).join('')
|
|
2916
|
-
}</div></div>\`;
|
|
2753
|
+
document.getElementById('dp-name').textContent=asset.name;
|
|
2754
|
+
const body=document.getElementById('dp-body');
|
|
2755
|
+
const rows=[['Domain',asset.domain],['Sub-domain',asset.subDomain],
|
|
2756
|
+
['Quality Score',asset.qualityScore!==null?renderQuality(asset.qualityScore):null],
|
|
2757
|
+
...Object.entries(asset.metadata||{}).slice(0,8).map(([k,v])=>[k,String(v)])
|
|
2758
|
+
].filter(([,v])=>v!==null&&v!==undefined&&v!=='');
|
|
2759
|
+
body.innerHTML=rows.map(([l,v])=>'<div class="meta-row"><div class="meta-label">'+esc(String(l))+'</div><div class="meta-value">'+v+'</div></div>').join('');
|
|
2760
|
+
const related=localConnections.filter(c=>c.sourceAssetId===asset.id||c.targetAssetId===asset.id);
|
|
2761
|
+
if(related.length>0){
|
|
2762
|
+
body.innerHTML+='<div class="meta-row"><div class="meta-label">Connections ('+related.length+')</div><div>'+
|
|
2763
|
+
related.map(c=>{const oid=c.sourceAssetId===asset.id?c.targetAssetId:c.sourceAssetId;
|
|
2764
|
+
const o=assetIndex.get(oid);return '<div class="meta-value" style="margin-top:4px;font-size:12px">'+(o?esc(o.name):oid)+'</div>';}).join('')+'</div></div>';
|
|
2917
2765
|
}
|
|
2918
|
-
|
|
2919
2766
|
document.getElementById('detail-panel').classList.add('open');
|
|
2920
2767
|
}
|
|
2768
|
+
function renderQuality(s){
|
|
2769
|
+
const c=s>=70?'#22c55e':s>=40?'#f97316':'#ef4444';
|
|
2770
|
+
return s+'/100 <div class="quality-bar"><div class="quality-fill" style="width:'+s+'%;background:'+c+'"></div></div>';
|
|
2771
|
+
}
|
|
2772
|
+
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
2773
|
+
document.getElementById('dp-close').addEventListener('click',()=>{
|
|
2774
|
+
document.getElementById('detail-panel').classList.remove('open');selectedAssetId=null;draw();
|
|
2775
|
+
});
|
|
2921
2776
|
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2777
|
+
// \u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2778
|
+
[1,2,3,4].forEach(n=>{
|
|
2779
|
+
document.getElementById('dl-'+n).addEventListener('click',()=>{
|
|
2780
|
+
detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
|
|
2781
|
+
document.getElementById('dl-'+n).classList.add('active');draw();
|
|
2782
|
+
});
|
|
2783
|
+
});
|
|
2784
|
+
document.getElementById('btn-labels').addEventListener('click',()=>{
|
|
2785
|
+
showLabels=!showLabels;document.getElementById('btn-labels').classList.toggle('active',showLabels);draw();
|
|
2786
|
+
});
|
|
2787
|
+
document.getElementById('btn-quality').addEventListener('click',()=>{
|
|
2788
|
+
showQuality=!showQuality;document.getElementById('btn-quality').classList.toggle('active',showQuality);draw();
|
|
2789
|
+
});
|
|
2790
|
+
function toggleConnect(){
|
|
2791
|
+
connectMode=!connectMode;connectFirst=null;
|
|
2792
|
+
document.getElementById('connect-btn').classList.toggle('active',connectMode);
|
|
2793
|
+
wrap.classList.toggle('connecting',connectMode);
|
|
2794
|
+
document.getElementById('connect-hint').style.display=connectMode?'block':'none';draw();
|
|
2795
|
+
}
|
|
2796
|
+
document.getElementById('connect-btn').addEventListener('click',toggleConnect);
|
|
2797
|
+
document.getElementById('theme-btn').addEventListener('click',()=>{
|
|
2798
|
+
isDark=!isDark;
|
|
2799
|
+
document.body.classList.toggle('dark',isDark);document.body.classList.toggle('light',!isDark);
|
|
2800
|
+
document.getElementById('theme-btn').innerHTML=isDark?'☼':'☾';draw();
|
|
2801
|
+
});
|
|
2802
|
+
document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
|
|
2803
|
+
|
|
2804
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2805
|
+
resize(); fitToView();
|
|
2806
|
+
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';
|
|
2807
|
+
draw();
|
|
2808
|
+
})();
|
|
2809
|
+
</script>
|
|
2810
|
+
</body>
|
|
2811
|
+
</html>`;
|
|
2812
|
+
}
|
|
2813
|
+
function exportDiscoveryApp(nodes, edges, options) {
|
|
2814
|
+
const theme = options?.theme ?? "dark";
|
|
2815
|
+
const graphData = JSON.stringify({
|
|
2816
|
+
nodes: nodes.map((n) => ({
|
|
2817
|
+
id: n.id,
|
|
2818
|
+
name: n.name,
|
|
2819
|
+
type: n.type,
|
|
2820
|
+
layer: nodeLayer(n.type),
|
|
2821
|
+
confidence: n.confidence,
|
|
2822
|
+
discoveredVia: n.discoveredVia,
|
|
2823
|
+
discoveredAt: n.discoveredAt,
|
|
2824
|
+
tags: n.tags,
|
|
2825
|
+
metadata: n.metadata
|
|
2826
|
+
})),
|
|
2827
|
+
links: edges.map((e) => ({
|
|
2828
|
+
source: e.sourceId,
|
|
2829
|
+
target: e.targetId,
|
|
2830
|
+
relationship: e.relationship,
|
|
2831
|
+
confidence: e.confidence,
|
|
2832
|
+
evidence: e.evidence
|
|
2833
|
+
}))
|
|
2834
|
+
});
|
|
2835
|
+
const { assets, clusters, connections } = buildMapData(nodes, edges, { theme });
|
|
2836
|
+
const isEmpty = assets.length === 0;
|
|
2837
|
+
const HEX_SIZE2 = 24;
|
|
2838
|
+
const mapJson = JSON.stringify({
|
|
2839
|
+
assets: assets.map((a) => ({
|
|
2840
|
+
id: a.id,
|
|
2841
|
+
name: a.name,
|
|
2842
|
+
domain: a.domain,
|
|
2843
|
+
subDomain: a.subDomain ?? null,
|
|
2844
|
+
qualityScore: a.qualityScore ?? null,
|
|
2845
|
+
metadata: a.metadata,
|
|
2846
|
+
q: a.position.q,
|
|
2847
|
+
r: a.position.r
|
|
2848
|
+
})),
|
|
2849
|
+
clusters: clusters.map((c) => ({
|
|
2850
|
+
id: c.id,
|
|
2851
|
+
label: c.label,
|
|
2852
|
+
domain: c.domain,
|
|
2853
|
+
color: c.color,
|
|
2854
|
+
assetIds: c.assetIds,
|
|
2855
|
+
centroid: c.centroid
|
|
2856
|
+
})),
|
|
2857
|
+
connections: connections.map((c) => ({
|
|
2858
|
+
id: c.id,
|
|
2859
|
+
sourceAssetId: c.sourceAssetId,
|
|
2860
|
+
targetAssetId: c.targetAssetId,
|
|
2861
|
+
type: c.type ?? "connection"
|
|
2862
|
+
}))
|
|
2863
|
+
});
|
|
2864
|
+
const nodeCount = nodes.length;
|
|
2865
|
+
const edgeCount = edges.length;
|
|
2866
|
+
const assetCount = assets.length;
|
|
2867
|
+
const clusterCount = clusters.length;
|
|
2868
|
+
return `<!DOCTYPE html>
|
|
2869
|
+
<html lang="en" data-theme="${theme}">
|
|
2870
|
+
<head>
|
|
2871
|
+
<meta charset="UTF-8"/>
|
|
2872
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
2873
|
+
<title>Cartography \u2014 Datasynx Discovery</title>
|
|
2874
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
2875
|
+
<style>
|
|
2876
|
+
/* \u2500\u2500 CSS Custom Properties \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2877
|
+
:root{
|
|
2878
|
+
--bg-base:#0f172a;--bg-surface:#1e293b;--bg-elevated:#273148;
|
|
2879
|
+
--border:#334155;--border-dim:#1e293b;
|
|
2880
|
+
--text:#e2e8f0;--text-muted:#94a3b8;--text-dim:#475569;
|
|
2881
|
+
--accent:#3b82f6;--accent-hover:#2563eb;--accent-dim:rgba(59,130,246,.12);
|
|
2882
|
+
}
|
|
2883
|
+
[data-theme="light"]{
|
|
2884
|
+
--bg-base:#f8fafc;--bg-surface:#ffffff;--bg-elevated:#f1f5f9;
|
|
2885
|
+
--border:#e2e8f0;--border-dim:#f1f5f9;
|
|
2886
|
+
--text:#0f172a;--text-muted:#64748b;--text-dim:#94a3b8;
|
|
2887
|
+
--accent:#2563eb;--accent-hover:#1d4ed8;--accent-dim:rgba(37,99,235,.08);
|
|
2925
2888
|
}
|
|
2926
2889
|
|
|
2927
|
-
|
|
2928
|
-
|
|
2890
|
+
/* \u2500\u2500 Reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2891
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2892
|
+
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',sans-serif}
|
|
2893
|
+
body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--text)}
|
|
2894
|
+
|
|
2895
|
+
/* \u2500\u2500 Topbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2896
|
+
#topbar{
|
|
2897
|
+
height:56px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2898
|
+
background:var(--bg-surface);border-bottom:1px solid var(--border);z-index:100;flex-shrink:0;
|
|
2899
|
+
}
|
|
2900
|
+
.tb-left{display:flex;align-items:center;gap:10px}
|
|
2901
|
+
.brand-logo{flex-shrink:0}
|
|
2902
|
+
.brand-name{font-size:15px;font-weight:700;color:var(--accent);letter-spacing:-.02em}
|
|
2903
|
+
.brand-product{font-size:14px;font-weight:500;color:var(--text-muted);margin-left:2px}
|
|
2904
|
+
.brand-sep{width:1px;height:24px;background:var(--border);margin:0 6px}
|
|
2905
|
+
.tb-center{display:flex;align-items:center;gap:2px;margin-left:auto;
|
|
2906
|
+
background:var(--bg-elevated);border-radius:8px;padding:3px}
|
|
2907
|
+
.tab-btn{
|
|
2908
|
+
padding:6px 16px;border:none;border-radius:6px;font-size:13px;font-weight:500;
|
|
2909
|
+
cursor:pointer;color:var(--text-muted);background:transparent;font-family:inherit;
|
|
2910
|
+
transition:all .15s;
|
|
2911
|
+
}
|
|
2912
|
+
.tab-btn:hover{color:var(--text)}
|
|
2913
|
+
.tab-btn.active{background:var(--accent);color:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2)}
|
|
2914
|
+
.tb-right{display:flex;align-items:center;gap:8px;margin-left:auto}
|
|
2915
|
+
.tb-search{
|
|
2916
|
+
display:flex;align-items:center;gap:6px;background:var(--bg-elevated);
|
|
2917
|
+
border:1px solid var(--border);border-radius:8px;padding:5px 10px;
|
|
2918
|
+
}
|
|
2919
|
+
.tb-search input{
|
|
2920
|
+
border:none;background:transparent;font-size:13px;outline:none;width:160px;
|
|
2921
|
+
color:var(--text);font-family:inherit;
|
|
2922
|
+
}
|
|
2923
|
+
.tb-search input::placeholder{color:var(--text-dim)}
|
|
2924
|
+
.tb-search svg{flex-shrink:0;color:var(--text-dim)}
|
|
2925
|
+
.icon-btn{
|
|
2926
|
+
width:36px;height:36px;border-radius:8px;border:1px solid var(--border);
|
|
2927
|
+
background:var(--bg-surface);cursor:pointer;display:flex;align-items:center;
|
|
2928
|
+
justify-content:center;color:var(--text-muted);text-decoration:none;transition:all .15s;font-size:16px;
|
|
2929
|
+
}
|
|
2930
|
+
.icon-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
|
|
2931
|
+
.tb-stats{font-size:11px;color:var(--text-dim);white-space:nowrap}
|
|
2932
|
+
|
|
2933
|
+
/* \u2500\u2500 Views \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2934
|
+
.view{flex:1;display:none;overflow:hidden;position:relative}
|
|
2935
|
+
.view.active{display:flex}
|
|
2936
|
+
|
|
2937
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2938
|
+
MAP VIEW
|
|
2939
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
2940
|
+
#map-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2941
|
+
#map-wrap.dragging{cursor:grabbing}
|
|
2942
|
+
#map-wrap.connecting{cursor:crosshair}
|
|
2943
|
+
#map-wrap canvas{display:block;width:100%;height:100%}
|
|
2944
|
+
#map-detail{
|
|
2945
|
+
width:280px;background:var(--bg-surface);border-left:1px solid var(--border);
|
|
2946
|
+
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2947
|
+
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2948
|
+
}
|
|
2949
|
+
#map-detail.open{transform:translateX(0)}
|
|
2950
|
+
#map-detail .panel-header{
|
|
2951
|
+
padding:16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;
|
|
2952
|
+
}
|
|
2953
|
+
#map-detail .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2954
|
+
.close-btn{
|
|
2955
|
+
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2956
|
+
color:var(--text-muted);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2957
|
+
}
|
|
2958
|
+
.close-btn:hover{background:var(--bg-elevated)}
|
|
2959
|
+
.panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2960
|
+
.meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2961
|
+
.meta-label{font-size:11px;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
|
|
2962
|
+
.meta-value{font-size:13px;word-break:break-all}
|
|
2963
|
+
.quality-bar{height:6px;border-radius:3px;background:var(--bg-elevated);margin-top:4px}
|
|
2964
|
+
.quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2965
|
+
|
|
2966
|
+
/* Map toolbars */
|
|
2967
|
+
#map-tb-left{position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10}
|
|
2968
|
+
#map-tb-right{position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;align-items:flex-end;gap:8px;z-index:10}
|
|
2969
|
+
.tb-tool{
|
|
2970
|
+
width:40px;height:40px;border-radius:10px;border:1px solid var(--border);
|
|
2971
|
+
background:var(--bg-surface);box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2972
|
+
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2973
|
+
transition:all .15s;color:var(--text);
|
|
2974
|
+
}
|
|
2975
|
+
.tb-tool:hover{border-color:var(--text-muted)}
|
|
2976
|
+
.tb-tool.active{background:var(--accent-dim);border-color:var(--accent)}
|
|
2977
|
+
.map-zoom{display:flex;align-items:center;gap:6px}
|
|
2978
|
+
.zoom-btn{
|
|
2979
|
+
width:34px;height:34px;border-radius:8px;border:1px solid var(--border);
|
|
2980
|
+
background:var(--bg-surface);cursor:pointer;font-size:18px;color:var(--text);
|
|
2981
|
+
display:flex;align-items:center;justify-content:center;
|
|
2982
|
+
}
|
|
2983
|
+
.zoom-btn:hover{background:var(--bg-elevated)}
|
|
2984
|
+
#map-zoom-pct{font-size:12px;font-weight:500;color:var(--text-dim);min-width:38px;text-align:center}
|
|
2985
|
+
#map-connect-hint{
|
|
2986
|
+
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2987
|
+
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2988
|
+
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2989
|
+
display:none;z-index:20;pointer-events:none;
|
|
2990
|
+
}
|
|
2991
|
+
#map-tooltip{
|
|
2992
|
+
position:fixed;background:var(--bg-surface);color:var(--text);border-radius:8px;
|
|
2993
|
+
padding:8px 12px;font-size:12px;pointer-events:none;z-index:200;
|
|
2994
|
+
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.25);border:1px solid var(--border);
|
|
2995
|
+
}
|
|
2996
|
+
#map-tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2997
|
+
#map-tooltip .tt-domain{color:var(--text-muted);font-size:11px}
|
|
2998
|
+
#map-tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2999
|
+
#map-empty{
|
|
3000
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
3001
|
+
align-items:center;justify-content:center;gap:12px;color:var(--text-muted);
|
|
3002
|
+
}
|
|
3003
|
+
#map-empty p{font-size:14px}
|
|
3004
|
+
|
|
3005
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3006
|
+
TOPOLOGY VIEW
|
|
3007
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
3008
|
+
#topo-panel{
|
|
3009
|
+
width:220px;min-width:220px;height:100%;overflow:hidden;
|
|
3010
|
+
background:var(--bg-surface);border-right:1px solid var(--border);
|
|
3011
|
+
display:flex;flex-direction:column;
|
|
3012
|
+
}
|
|
3013
|
+
#topo-panel-header{
|
|
3014
|
+
padding:10px 12px 8px;border-bottom:1px solid var(--border);
|
|
3015
|
+
font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.6px;
|
|
3016
|
+
}
|
|
3017
|
+
#topo-search{
|
|
3018
|
+
width:calc(100% - 16px);margin:8px;padding:5px 8px;
|
|
3019
|
+
background:var(--bg-elevated);border:1px solid var(--border);border-radius:5px;
|
|
3020
|
+
color:var(--text);font-size:11px;font-family:inherit;outline:none;
|
|
3021
|
+
}
|
|
3022
|
+
#topo-search:focus{border-color:var(--accent)}
|
|
3023
|
+
#topo-list{flex:1;overflow-y:auto;padding-bottom:8px}
|
|
3024
|
+
.topo-item{
|
|
3025
|
+
padding:5px 12px;cursor:pointer;font-size:11px;
|
|
3026
|
+
display:flex;align-items:center;gap:6px;border-left:2px solid transparent;
|
|
3027
|
+
}
|
|
3028
|
+
.topo-item:hover{background:var(--bg-elevated)}
|
|
3029
|
+
.topo-item.active{background:var(--accent-dim);border-left-color:var(--accent)}
|
|
3030
|
+
.topo-dot{width:7px;height:7px;border-radius:2px;flex-shrink:0}
|
|
3031
|
+
.topo-name{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
3032
|
+
.topo-type{color:var(--text-dim);font-size:9px;flex-shrink:0}
|
|
3033
|
+
|
|
3034
|
+
#topo-graph{flex:1;height:100%;position:relative}
|
|
3035
|
+
#topo-graph svg{width:100%;height:100%}
|
|
3036
|
+
.hull{opacity:.12;stroke-width:1.5;stroke-opacity:.25}
|
|
3037
|
+
.hull-label{font-size:13px;font-weight:700;letter-spacing:1px;text-transform:uppercase;fill-opacity:.5;pointer-events:none}
|
|
3038
|
+
.link{stroke-opacity:.4}
|
|
3039
|
+
.link-label{font-size:8px;fill:var(--text-dim);pointer-events:none;opacity:0}
|
|
3040
|
+
.node-hex{stroke-width:1.8;cursor:pointer;transition:opacity .15s}
|
|
3041
|
+
.node-hex:hover{filter:brightness(1.3);stroke-width:3}
|
|
3042
|
+
.node-hex.selected{stroke-width:3.5;filter:brightness(1.5)}
|
|
3043
|
+
.node-label{font-size:10px;fill:var(--text);pointer-events:none;opacity:0}
|
|
3044
|
+
|
|
3045
|
+
#topo-sidebar{
|
|
3046
|
+
width:300px;min-width:300px;height:100%;overflow-y:auto;
|
|
3047
|
+
background:var(--bg-surface);border-left:1px solid var(--border);
|
|
3048
|
+
padding:16px;font-size:12px;line-height:1.6;
|
|
3049
|
+
}
|
|
3050
|
+
#topo-sidebar h2{margin:0 0 8px;font-size:14px;color:var(--accent)}
|
|
3051
|
+
#topo-sidebar .meta-table{width:100%;border-collapse:collapse}
|
|
3052
|
+
#topo-sidebar .meta-table td{padding:3px 6px;border-bottom:1px solid var(--border-dim);vertical-align:top}
|
|
3053
|
+
#topo-sidebar .meta-table td:first-child{color:var(--text-dim);white-space:nowrap;width:90px}
|
|
3054
|
+
#topo-sidebar .tag{display:inline-block;background:var(--bg-elevated);border-radius:3px;padding:1px 5px;margin:1px;font-size:10px}
|
|
3055
|
+
#topo-sidebar .conf-bar{height:5px;border-radius:3px;background:var(--bg-elevated);margin-top:3px}
|
|
3056
|
+
#topo-sidebar .conf-fill{height:100%;border-radius:3px}
|
|
3057
|
+
#topo-sidebar .edges-list{margin-top:12px}
|
|
3058
|
+
#topo-sidebar .edge-item{padding:4px 0;border-bottom:1px solid var(--border-dim);color:var(--text-dim);font-size:11px}
|
|
3059
|
+
#topo-sidebar .edge-item span{color:var(--text)}
|
|
3060
|
+
.hint{color:var(--text-dim);font-size:11px;margin-top:8px}
|
|
3061
|
+
|
|
3062
|
+
#topo-hud{
|
|
3063
|
+
position:absolute;top:10px;left:10px;background:rgba(15,23,42,.88);
|
|
3064
|
+
padding:10px 14px;border-radius:8px;font-size:12px;border:1px solid var(--border);pointer-events:none;
|
|
3065
|
+
}
|
|
3066
|
+
#topo-hud strong{color:var(--accent)}
|
|
3067
|
+
#topo-hud .stats{color:var(--text-dim)}
|
|
3068
|
+
#topo-hud .zoom-level{color:var(--text-dim);font-size:10px;margin-top:2px}
|
|
3069
|
+
|
|
3070
|
+
#topo-toolbar{position:absolute;top:10px;right:10px;display:flex;flex-wrap:wrap;gap:4px;pointer-events:auto;align-items:center}
|
|
3071
|
+
.filter-btn{
|
|
3072
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
3073
|
+
color:var(--text);padding:4px 10px;font-size:11px;cursor:pointer;
|
|
3074
|
+
font-family:inherit;display:flex;align-items:center;gap:5px;
|
|
2929
3075
|
}
|
|
3076
|
+
.filter-btn:hover{border-color:var(--text-dim)}
|
|
3077
|
+
.filter-btn.off{opacity:.35}
|
|
3078
|
+
.filter-dot{width:8px;height:8px;border-radius:2px;display:inline-block}
|
|
3079
|
+
.export-btn{
|
|
3080
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
3081
|
+
color:var(--accent);padding:4px 12px;font-size:11px;cursor:pointer;font-family:inherit;
|
|
3082
|
+
}
|
|
3083
|
+
.export-btn:hover{border-color:var(--accent);background:var(--accent-dim)}
|
|
3084
|
+
</style>
|
|
3085
|
+
</head>
|
|
3086
|
+
<body>
|
|
3087
|
+
|
|
3088
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3089
|
+
TOPBAR
|
|
3090
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
3091
|
+
<header id="topbar">
|
|
3092
|
+
<div class="tb-left">
|
|
3093
|
+
<svg class="brand-logo" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
3094
|
+
<path d="M16 1.5L29.5 8.75V23.25L16 30.5L2.5 23.25V8.75L16 1.5Z" fill="#0F2347" stroke="#2563EB" stroke-width="1.2"/>
|
|
3095
|
+
<circle cx="10" cy="16" r="2.8" fill="#60A5FA"/><circle cx="22" cy="10.5" r="2.2" fill="#38BDF8"/>
|
|
3096
|
+
<circle cx="22" cy="21.5" r="2.2" fill="#38BDF8"/>
|
|
3097
|
+
<line x1="12.5" y1="14.8" x2="19.8" y2="11.2" stroke="#93C5FD" stroke-width="1.2"/>
|
|
3098
|
+
<line x1="12.5" y1="17.2" x2="19.8" y2="20.8" stroke="#93C5FD" stroke-width="1.2"/>
|
|
3099
|
+
<line x1="22" y1="12.7" x2="22" y2="19.3" stroke="#93C5FD" stroke-width="1" stroke-dasharray="2 1.5"/>
|
|
3100
|
+
</svg>
|
|
3101
|
+
<span class="brand-name">datasynx</span>
|
|
3102
|
+
<span class="brand-sep"></span>
|
|
3103
|
+
<span class="brand-product">Cartography</span>
|
|
3104
|
+
</div>
|
|
3105
|
+
<div class="tb-center">
|
|
3106
|
+
<button class="tab-btn active" id="tab-map-btn" data-tab="map">Map</button>
|
|
3107
|
+
<button class="tab-btn" id="tab-topo-btn" data-tab="topo">Topology</button>
|
|
3108
|
+
</div>
|
|
3109
|
+
<div class="tb-right">
|
|
3110
|
+
<span class="tb-stats">${nodeCount} nodes · ${edgeCount} edges</span>
|
|
3111
|
+
<div class="tb-search">
|
|
3112
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3113
|
+
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
|
|
3114
|
+
</svg>
|
|
3115
|
+
<input id="global-search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false"/>
|
|
3116
|
+
</div>
|
|
3117
|
+
<a href="https://www.linkedin.com/company/datasynx-ai/" target="_blank" rel="noopener noreferrer"
|
|
3118
|
+
class="icon-btn" title="Datasynx on LinkedIn" aria-label="LinkedIn">
|
|
3119
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
|
3120
|
+
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
|
3121
|
+
</svg>
|
|
3122
|
+
</a>
|
|
3123
|
+
<button id="theme-btn" class="icon-btn" title="Toggle theme" aria-label="Toggle theme">
|
|
3124
|
+
${theme === "dark" ? "☼" : "☾"}
|
|
3125
|
+
</button>
|
|
3126
|
+
</div>
|
|
3127
|
+
</header>
|
|
2930
3128
|
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3129
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3130
|
+
MAP VIEW
|
|
3131
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
3132
|
+
<div id="view-map" class="view active">
|
|
3133
|
+
<div id="map-wrap" tabindex="0" aria-label="Data cartography hex map">
|
|
3134
|
+
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
3135
|
+
${isEmpty ? '<div id="map-empty"><p style="font-size:48px">🗺</p><p>No data assets discovered yet</p><p style="font-size:12px">Run <code>datasynx-cartography discover</code> to populate the map</p></div>' : ""}
|
|
3136
|
+
</div>
|
|
3137
|
+
<div id="map-detail">
|
|
3138
|
+
<div class="panel-header">
|
|
3139
|
+
<h3 id="md-name">—</h3>
|
|
3140
|
+
<button class="close-btn" id="md-close" aria-label="Close">✕</button>
|
|
3141
|
+
</div>
|
|
3142
|
+
<div class="panel-body" id="md-body"></div>
|
|
3143
|
+
</div>
|
|
3144
|
+
<div id="map-tb-left">
|
|
3145
|
+
<button class="tb-tool active" id="btn-labels" title="Toggle labels">🏷</button>
|
|
3146
|
+
<button class="tb-tool" id="btn-quality" title="Quality layer">👁</button>
|
|
3147
|
+
<button class="tb-tool" id="btn-connect" title="Connection tool">🔗</button>
|
|
3148
|
+
</div>
|
|
3149
|
+
<div id="map-tb-right">
|
|
3150
|
+
<div class="map-zoom">
|
|
3151
|
+
<button class="zoom-btn" id="mz-out">−</button>
|
|
3152
|
+
<span id="map-zoom-pct">100%</span>
|
|
3153
|
+
<button class="zoom-btn" id="mz-in">+</button>
|
|
3154
|
+
</div>
|
|
3155
|
+
</div>
|
|
3156
|
+
<div id="map-connect-hint">Click two assets to create a connection</div>
|
|
3157
|
+
<div id="map-tooltip"><div class="tt-name" id="mtt-name"></div><div class="tt-domain" id="mtt-domain"></div><div class="tt-quality" id="mtt-quality"></div></div>
|
|
3158
|
+
</div>
|
|
3159
|
+
|
|
3160
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3161
|
+
TOPOLOGY VIEW
|
|
3162
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
3163
|
+
<div id="view-topo" class="view">
|
|
3164
|
+
<div id="topo-panel">
|
|
3165
|
+
<div id="topo-panel-header">Nodes (${nodeCount})</div>
|
|
3166
|
+
<input id="topo-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false"/>
|
|
3167
|
+
<div id="topo-list"></div>
|
|
3168
|
+
</div>
|
|
3169
|
+
<div id="topo-graph">
|
|
3170
|
+
<div id="topo-hud">
|
|
3171
|
+
<strong>Topology</strong>
|
|
3172
|
+
<span class="stats">${nodeCount} nodes · ${edgeCount} edges</span><br/>
|
|
3173
|
+
<span class="zoom-level">Scroll = zoom · Drag = pan · Click = details</span>
|
|
3174
|
+
</div>
|
|
3175
|
+
<div id="topo-toolbar"></div>
|
|
3176
|
+
<svg></svg>
|
|
3177
|
+
</div>
|
|
3178
|
+
<div id="topo-sidebar">
|
|
3179
|
+
<h2>Infrastructure Map</h2>
|
|
3180
|
+
<p class="hint">Click a node to view details.</p>
|
|
3181
|
+
</div>
|
|
3182
|
+
</div>
|
|
3183
|
+
|
|
3184
|
+
<script>
|
|
3185
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3186
|
+
// SHARED STATE
|
|
3187
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3188
|
+
let isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
3189
|
+
let currentTab = 'map';
|
|
3190
|
+
let topoInited = false;
|
|
3191
|
+
|
|
3192
|
+
// \u2500\u2500 Theme toggle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3193
|
+
document.getElementById('theme-btn').addEventListener('click', function() {
|
|
3194
|
+
isDark = !isDark;
|
|
3195
|
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
3196
|
+
this.innerHTML = isDark ? '\\u2606' : '\\u263E';
|
|
3197
|
+
if (typeof drawMap === 'function') drawMap();
|
|
2934
3198
|
});
|
|
2935
3199
|
|
|
2936
|
-
// \u2500\u2500
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3200
|
+
// \u2500\u2500 Tab switching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3201
|
+
document.querySelectorAll('.tab-btn').forEach(function(btn) {
|
|
3202
|
+
btn.addEventListener('click', function() {
|
|
3203
|
+
var tab = this.getAttribute('data-tab');
|
|
3204
|
+
if (tab === currentTab) return;
|
|
3205
|
+
currentTab = tab;
|
|
3206
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
3207
|
+
this.classList.add('active');
|
|
3208
|
+
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
3209
|
+
document.getElementById('view-' + tab).classList.add('active');
|
|
3210
|
+
if (tab === 'topo' && !topoInited) { initTopology(); topoInited = true; }
|
|
3211
|
+
if (tab === 'map' && typeof drawMap === 'function') { resizeMap(); }
|
|
2943
3212
|
});
|
|
2944
3213
|
});
|
|
2945
3214
|
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3215
|
+
// \u2500\u2500 Global search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3216
|
+
document.getElementById('global-search').addEventListener('input', function(e) {
|
|
3217
|
+
var q = e.target.value.trim();
|
|
3218
|
+
if (typeof setMapSearch === 'function') setMapSearch(q);
|
|
3219
|
+
if (typeof setTopoSearch === 'function') setTopoSearch(q);
|
|
2950
3220
|
});
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3221
|
+
|
|
3222
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3223
|
+
// MAP VIEW
|
|
3224
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3225
|
+
var MAP = ${mapJson};
|
|
3226
|
+
var MAP_HEX = ${HEX_SIZE2};
|
|
3227
|
+
var MAP_EMPTY = ${isEmpty};
|
|
3228
|
+
|
|
3229
|
+
var mapAssetIndex = new Map();
|
|
3230
|
+
var mapClusterByAsset = new Map();
|
|
3231
|
+
for (var ci = 0; ci < MAP.clusters.length; ci++) {
|
|
3232
|
+
var c = MAP.clusters[ci];
|
|
3233
|
+
for (var ai = 0; ai < c.assetIds.length; ai++) mapClusterByAsset.set(c.assetIds[ai], c);
|
|
3234
|
+
}
|
|
3235
|
+
for (var ni = 0; ni < MAP.assets.length; ni++) mapAssetIndex.set(MAP.assets[ni].id, MAP.assets[ni]);
|
|
3236
|
+
|
|
3237
|
+
var mapCanvas = document.getElementById('hexmap');
|
|
3238
|
+
var mapCtx = mapCanvas.getContext('2d');
|
|
3239
|
+
var mapWrap = document.getElementById('map-wrap');
|
|
3240
|
+
var mW = 0, mH = 0;
|
|
3241
|
+
var mvx = 0, mvy = 0, mScale = 1;
|
|
3242
|
+
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false;
|
|
3243
|
+
var mConnectMode = false, mConnectFirst = null;
|
|
3244
|
+
var mHoveredId = null, mSelectedId = null;
|
|
3245
|
+
var mSearchQuery = '';
|
|
3246
|
+
var mLocalConns = MAP.connections.slice();
|
|
3247
|
+
|
|
3248
|
+
function setMapSearch(q) { mSearchQuery = q; drawMap(); }
|
|
3249
|
+
|
|
3250
|
+
function resizeMap() {
|
|
3251
|
+
var dpr = window.devicePixelRatio || 1;
|
|
3252
|
+
mW = mapWrap.clientWidth; mH = mapWrap.clientHeight;
|
|
3253
|
+
mapCanvas.width = mW * dpr; mapCanvas.height = mH * dpr;
|
|
3254
|
+
mapCanvas.style.width = mW + 'px'; mapCanvas.style.height = mH + 'px';
|
|
3255
|
+
mapCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3256
|
+
drawMap();
|
|
3257
|
+
}
|
|
3258
|
+
window.addEventListener('resize', function() { if (currentTab === 'map') resizeMap(); });
|
|
3259
|
+
|
|
3260
|
+
function mHtp_x(q, r) { return MAP_HEX * (1.5 * q); }
|
|
3261
|
+
function mHtp_y(q, r) { return MAP_HEX * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r); }
|
|
3262
|
+
function mW2s(wx, wy) { return { x: wx * mScale + mvx, y: wy * mScale + mvy }; }
|
|
3263
|
+
function mS2w(sx, sy) { return { x: (sx - mvx) / mScale, y: (sy - mvy) / mScale }; }
|
|
3264
|
+
|
|
3265
|
+
function mapFitToView() {
|
|
3266
|
+
if (MAP_EMPTY || MAP.assets.length === 0) { mvx = 0; mvy = 0; mScale = 1; return; }
|
|
3267
|
+
var mnx = Infinity, mny = Infinity, mxx = -Infinity, mxy = -Infinity;
|
|
3268
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
3269
|
+
var a = MAP.assets[i], px = mHtp_x(a.q, a.r), py = mHtp_y(a.q, a.r);
|
|
3270
|
+
if (px < mnx) mnx = px; if (py < mny) mny = py; if (px > mxx) mxx = px; if (py > mxy) mxy = py;
|
|
3271
|
+
}
|
|
3272
|
+
var pw = mxx - mnx + MAP_HEX * 4, ph = mxy - mny + MAP_HEX * 4;
|
|
3273
|
+
mScale = Math.min(mW / pw, mH / ph, 2) * 0.85;
|
|
3274
|
+
mvx = mW / 2 - ((mnx + mxx) / 2) * mScale;
|
|
3275
|
+
mvy = mH / 2 - ((mny + mxy) / 2) * mScale;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function mHexPath(cx, cy, r) {
|
|
3279
|
+
mapCtx.beginPath();
|
|
3280
|
+
for (var i = 0; i < 6; i++) {
|
|
3281
|
+
var angle = Math.PI / 180 * (60 * i);
|
|
3282
|
+
var x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle);
|
|
3283
|
+
i === 0 ? mapCtx.moveTo(x, y) : mapCtx.lineTo(x, y);
|
|
3284
|
+
}
|
|
3285
|
+
mapCtx.closePath();
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
function mShadeV(hex, amt) {
|
|
3289
|
+
if (!hex || hex.length < 7) return hex;
|
|
3290
|
+
var n = parseInt(hex.replace('#', ''), 16);
|
|
3291
|
+
var r = Math.min(255, (n >> 16) + amt), g = Math.min(255, ((n >> 8) & 0xff) + amt), b = Math.min(255, (n & 0xff) + amt);
|
|
3292
|
+
return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
function mGetSearchMatches() {
|
|
3296
|
+
if (!mSearchQuery) return new Set();
|
|
3297
|
+
var q = mSearchQuery.toLowerCase(), m = new Set();
|
|
3298
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
3299
|
+
var a = MAP.assets[i];
|
|
3300
|
+
if (a.name.toLowerCase().includes(q) || (a.domain && a.domain.toLowerCase().includes(q)) ||
|
|
3301
|
+
(a.subDomain && a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
3302
|
+
}
|
|
3303
|
+
return m;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
function mDrawPill(x, y, text, color, fontSize) {
|
|
3307
|
+
if (!text) return;
|
|
3308
|
+
mapCtx.save();
|
|
3309
|
+
mapCtx.font = '600 ' + fontSize + 'px -apple-system,sans-serif';
|
|
3310
|
+
var tw = mapCtx.measureText(text).width;
|
|
3311
|
+
var ph = fontSize + 8, pw = tw + 20;
|
|
3312
|
+
mapCtx.beginPath();
|
|
3313
|
+
if (mapCtx.roundRect) mapCtx.roundRect(x - pw / 2, y - ph / 2, pw, ph, ph / 2);
|
|
3314
|
+
else mapCtx.rect(x - pw / 2, y - ph / 2, pw, ph);
|
|
3315
|
+
mapCtx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
3316
|
+
mapCtx.shadowColor = 'rgba(0,0,0,0.15)'; mapCtx.shadowBlur = 6;
|
|
3317
|
+
mapCtx.fill(); mapCtx.shadowBlur = 0;
|
|
3318
|
+
mapCtx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
3319
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
3320
|
+
mapCtx.fillText(text, x, y);
|
|
3321
|
+
mapCtx.restore();
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function drawMap() {
|
|
3325
|
+
mapCtx.clearRect(0, 0, mW, mH);
|
|
3326
|
+
var bg = getComputedStyle(document.documentElement).getPropertyValue('--bg-base').trim();
|
|
3327
|
+
mapCtx.fillStyle = bg || (isDark ? '#0f172a' : '#f8fafc');
|
|
3328
|
+
mapCtx.fillRect(0, 0, mW, mH);
|
|
3329
|
+
if (MAP_EMPTY) return;
|
|
3330
|
+
|
|
3331
|
+
var size = MAP_HEX * mScale;
|
|
3332
|
+
var matchedIds = mGetSearchMatches();
|
|
3333
|
+
var hasSearch = mSearchQuery.length > 0;
|
|
3334
|
+
|
|
3335
|
+
// Connections
|
|
3336
|
+
mapCtx.save();
|
|
3337
|
+
mapCtx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
3338
|
+
mapCtx.lineWidth = 1.5; mapCtx.setLineDash([4, 4]);
|
|
3339
|
+
for (var ci = 0; ci < mLocalConns.length; ci++) {
|
|
3340
|
+
var conn = mLocalConns[ci];
|
|
3341
|
+
var src = mapAssetIndex.get(conn.sourceAssetId), tgt = mapAssetIndex.get(conn.targetAssetId);
|
|
3342
|
+
if (!src || !tgt) continue;
|
|
3343
|
+
var sp = mW2s(mHtp_x(src.q, src.r), mHtp_y(src.q, src.r));
|
|
3344
|
+
var tp = mW2s(mHtp_x(tgt.q, tgt.r), mHtp_y(tgt.q, tgt.r));
|
|
3345
|
+
mapCtx.beginPath(); mapCtx.moveTo(sp.x, sp.y); mapCtx.lineTo(tp.x, tp.y); mapCtx.stroke();
|
|
3346
|
+
}
|
|
3347
|
+
mapCtx.setLineDash([]); mapCtx.restore();
|
|
3348
|
+
|
|
3349
|
+
// Hexagons per cluster
|
|
3350
|
+
for (var cli = 0; cli < MAP.clusters.length; cli++) {
|
|
3351
|
+
var cluster = MAP.clusters[cli];
|
|
3352
|
+
var baseColor = cluster.color;
|
|
3353
|
+
var clusterAssets = cluster.assetIds.map(function(id) { return mapAssetIndex.get(id); }).filter(Boolean);
|
|
3354
|
+
var isClusterMatch = !hasSearch || clusterAssets.some(function(a) { return matchedIds.has(a.id); });
|
|
3355
|
+
var clusterDim = hasSearch && !isClusterMatch;
|
|
3356
|
+
|
|
3357
|
+
for (var ai = 0; ai < clusterAssets.length; ai++) {
|
|
3358
|
+
var asset = clusterAssets[ai];
|
|
3359
|
+
var wx = mHtp_x(asset.q, asset.r), wy = mHtp_y(asset.q, asset.r);
|
|
3360
|
+
var s = mW2s(wx, wy), cx = s.x, cy = s.y;
|
|
3361
|
+
if (cx + size < 0 || cx - size > mW || cy + size < 0 || cy - size > mH) continue;
|
|
3362
|
+
|
|
3363
|
+
var shade = ai % 3 === 0 ? 18 : ai % 3 === 1 ? 8 : 0;
|
|
3364
|
+
var fillColor = mShadeV(baseColor, shade);
|
|
3365
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
3366
|
+
if (asset.qualityScore < 40) fillColor = '#ef4444';
|
|
3367
|
+
else if (asset.qualityScore < 70) fillColor = '#f97316';
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
var alpha = clusterDim ? 0.18 : 1;
|
|
3371
|
+
var isHov = asset.id === mHoveredId, isSel = asset.id === mSelectedId, isCF = asset.id === mConnectFirst;
|
|
3372
|
+
|
|
3373
|
+
mapCtx.save(); mapCtx.globalAlpha = alpha;
|
|
3374
|
+
mHexPath(cx, cy, size * 0.92);
|
|
3375
|
+
if (isDark && (isHov || isSel || isCF)) { mapCtx.shadowColor = fillColor; mapCtx.shadowBlur = isSel ? 16 : 8; }
|
|
3376
|
+
mapCtx.fillStyle = fillColor; mapCtx.fill();
|
|
3377
|
+
if (isSel || isCF) { mapCtx.strokeStyle = isCF ? '#f59e0b' : '#fff'; mapCtx.lineWidth = 2.5; mapCtx.stroke(); }
|
|
3378
|
+
else if (isHov) { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)'; mapCtx.lineWidth = 1.5; mapCtx.stroke(); }
|
|
3379
|
+
else { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)'; mapCtx.lineWidth = 1; mapCtx.stroke(); }
|
|
3380
|
+
mapCtx.restore();
|
|
3381
|
+
|
|
3382
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined && size > 8 && asset.qualityScore < 70) {
|
|
3383
|
+
mapCtx.beginPath(); mapCtx.arc(cx + size * 0.4, cy - size * 0.4, Math.max(3, size * 0.14), 0, Math.PI * 2);
|
|
3384
|
+
mapCtx.fillStyle = asset.qualityScore < 40 ? '#ef4444' : '#f97316'; mapCtx.fill();
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
var showAssetLabel = mShowLabels && !clusterDim && ((mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
3388
|
+
if (showAssetLabel && size > 14) {
|
|
3389
|
+
var label = asset.name.length > 12 ? asset.name.substring(0, 11) + '...' : asset.name;
|
|
3390
|
+
mapCtx.save();
|
|
3391
|
+
mapCtx.font = Math.max(8, Math.min(11, size * 0.38)) + 'px -apple-system,sans-serif';
|
|
3392
|
+
mapCtx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
3393
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
3394
|
+
mapCtx.fillText(label, cx, cy); mapCtx.restore();
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// Cluster labels
|
|
3400
|
+
if (mShowLabels && mDetailLevel >= 1) {
|
|
3401
|
+
for (var cli2 = 0; cli2 < MAP.clusters.length; cli2++) {
|
|
3402
|
+
var cl = MAP.clusters[cli2];
|
|
3403
|
+
if (cl.assetIds.length === 0) continue;
|
|
3404
|
+
if (hasSearch && !cl.assetIds.some(function(id) { return matchedIds.has(id); })) continue;
|
|
3405
|
+
var sc = mW2s(cl.centroid.x, cl.centroid.y);
|
|
3406
|
+
mDrawPill(sc.x, sc.y - size * 1.2, cl.label, cl.color, 14);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
// Sub-domain labels
|
|
3411
|
+
if (mShowLabels && mDetailLevel >= 2) {
|
|
3412
|
+
var subGroups = new Map();
|
|
3413
|
+
for (var si = 0; si < MAP.assets.length; si++) {
|
|
3414
|
+
var sa = MAP.assets[si];
|
|
3415
|
+
if (!sa.subDomain) continue;
|
|
3416
|
+
var key = sa.domain + '|' + sa.subDomain;
|
|
3417
|
+
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
3418
|
+
subGroups.get(key).push(sa);
|
|
3419
|
+
}
|
|
3420
|
+
subGroups.forEach(function(group) {
|
|
3421
|
+
var sx = 0, sy = 0;
|
|
3422
|
+
for (var gi = 0; gi < group.length; gi++) { sx += mHtp_x(group[gi].q, group[gi].r); sy += mHtp_y(group[gi].q, group[gi].r); }
|
|
3423
|
+
var cxs = sx / group.length, cys = sy / group.length;
|
|
3424
|
+
var spt = mW2s(cxs, cys);
|
|
3425
|
+
mDrawPill(spt.x, spt.y + size * 1.5, group[0].subDomain, '#64748b', 11);
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// \u2500\u2500 Map hit test \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3431
|
+
function mGetAssetAt(sx, sy) {
|
|
3432
|
+
var w = mS2w(sx, sy);
|
|
3433
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
3434
|
+
var a = MAP.assets[i], wx = mHtp_x(a.q, a.r), wy = mHtp_y(a.q, a.r);
|
|
3435
|
+
var dx = Math.abs(w.x - wx), dy = Math.abs(w.y - wy);
|
|
3436
|
+
if (dx > MAP_HEX || dy > MAP_HEX) continue;
|
|
3437
|
+
if (dx * dx + dy * dy < MAP_HEX * MAP_HEX) return a;
|
|
3438
|
+
}
|
|
3439
|
+
return null;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// \u2500\u2500 Map pan / zoom \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3443
|
+
var mDragging = false, mLastMX = 0, mLastMY = 0;
|
|
3444
|
+
mapWrap.addEventListener('mousedown', function(e) {
|
|
3445
|
+
if (e.button !== 0) return;
|
|
3446
|
+
mDragging = true; mLastMX = e.clientX; mLastMY = e.clientY;
|
|
3447
|
+
mapWrap.classList.add('dragging');
|
|
2955
3448
|
});
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
3449
|
+
window.addEventListener('mouseup', function() { mDragging = false; mapWrap.classList.remove('dragging'); });
|
|
3450
|
+
window.addEventListener('mousemove', function(e) {
|
|
3451
|
+
if (currentTab !== 'map') return;
|
|
3452
|
+
if (mDragging) {
|
|
3453
|
+
mvx += e.clientX - mLastMX; mvy += e.clientY - mLastMY;
|
|
3454
|
+
mLastMX = e.clientX; mLastMY = e.clientY; drawMap(); return;
|
|
3455
|
+
}
|
|
3456
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
3457
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
3458
|
+
var asset = mGetAssetAt(sx, sy);
|
|
3459
|
+
var newId = asset ? asset.id : null;
|
|
3460
|
+
if (newId !== mHoveredId) { mHoveredId = newId; drawMap(); }
|
|
3461
|
+
var tt = document.getElementById('map-tooltip');
|
|
3462
|
+
if (asset) {
|
|
3463
|
+
document.getElementById('mtt-name').textContent = asset.name;
|
|
3464
|
+
document.getElementById('mtt-domain').textContent = asset.domain + (asset.subDomain ? ' > ' + asset.subDomain : '');
|
|
3465
|
+
document.getElementById('mtt-quality').textContent = asset.qualityScore !== null ? 'Quality: ' + asset.qualityScore + '/100' : '';
|
|
3466
|
+
tt.style.display = 'block'; tt.style.left = (e.clientX + 12) + 'px'; tt.style.top = (e.clientY - 8) + 'px';
|
|
3467
|
+
} else { tt.style.display = 'none'; }
|
|
2960
3468
|
});
|
|
2961
3469
|
|
|
2962
|
-
function
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3470
|
+
mapWrap.addEventListener('click', function(e) {
|
|
3471
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
3472
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
3473
|
+
var asset = mGetAssetAt(sx, sy);
|
|
3474
|
+
if (mConnectMode) {
|
|
3475
|
+
if (!asset) return;
|
|
3476
|
+
if (!mConnectFirst) { mConnectFirst = asset.id; drawMap(); }
|
|
3477
|
+
else if (mConnectFirst !== asset.id) {
|
|
3478
|
+
mLocalConns.push({ id: crypto.randomUUID(), sourceAssetId: mConnectFirst, targetAssetId: asset.id, type: 'connection' });
|
|
3479
|
+
mConnectFirst = null; drawMap();
|
|
3480
|
+
}
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
if (asset) { mSelectedId = asset.id; mShowDetail(asset); }
|
|
3484
|
+
else { mSelectedId = null; document.getElementById('map-detail').classList.remove('open'); }
|
|
3485
|
+
drawMap();
|
|
3486
|
+
});
|
|
3487
|
+
|
|
3488
|
+
mapWrap.addEventListener('wheel', function(e) {
|
|
3489
|
+
e.preventDefault();
|
|
3490
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
3491
|
+
mApplyZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, e.clientX - rect.left, e.clientY - rect.top);
|
|
3492
|
+
}, { passive: false });
|
|
3493
|
+
|
|
3494
|
+
function mApplyZoom(factor, sx, sy) {
|
|
3495
|
+
var ns = Math.max(0.05, Math.min(8, mScale * factor));
|
|
3496
|
+
var wx = (sx - mvx) / mScale, wy = (sy - mvy) / mScale;
|
|
3497
|
+
mScale = ns; mvx = sx - wx * mScale; mvy = sy - wy * mScale;
|
|
3498
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
3499
|
+
drawMap();
|
|
2969
3500
|
}
|
|
2970
|
-
document.getElementById('connect-btn').addEventListener('click', toggleConnectMode);
|
|
2971
3501
|
|
|
2972
|
-
document.getElementById('
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3502
|
+
document.getElementById('mz-in').addEventListener('click', function() { mApplyZoom(1.25, mW / 2, mH / 2); });
|
|
3503
|
+
document.getElementById('mz-out').addEventListener('click', function() { mApplyZoom(1 / 1.25, mW / 2, mH / 2); });
|
|
3504
|
+
|
|
3505
|
+
// \u2500\u2500 Map detail panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3506
|
+
function mEsc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
3507
|
+
function mRenderQ(s) {
|
|
3508
|
+
var c = s >= 70 ? '#22c55e' : s >= 40 ? '#f97316' : '#ef4444';
|
|
3509
|
+
return s + '/100 <div class="quality-bar"><div class="quality-fill" style="width:' + s + '%;background:' + c + '"></div></div>';
|
|
3510
|
+
}
|
|
3511
|
+
function mShowDetail(asset) {
|
|
3512
|
+
document.getElementById('md-name').textContent = asset.name;
|
|
3513
|
+
var body = document.getElementById('md-body');
|
|
3514
|
+
var rows = [['Domain', asset.domain], ['Sub-domain', asset.subDomain],
|
|
3515
|
+
['Quality', asset.qualityScore !== null ? mRenderQ(asset.qualityScore) : null]
|
|
3516
|
+
].concat(Object.entries(asset.metadata || {}).slice(0, 8).map(function(kv) { return [kv[0], String(kv[1])]; }))
|
|
3517
|
+
.filter(function(r) { return r[1] !== null && r[1] !== undefined && r[1] !== ''; });
|
|
3518
|
+
body.innerHTML = rows.map(function(r) {
|
|
3519
|
+
return '<div class="meta-row"><div class="meta-label">' + mEsc(String(r[0])) + '</div><div class="meta-value">' + r[1] + '</div></div>';
|
|
3520
|
+
}).join('');
|
|
3521
|
+
var related = mLocalConns.filter(function(cn) { return cn.sourceAssetId === asset.id || cn.targetAssetId === asset.id; });
|
|
3522
|
+
if (related.length > 0) {
|
|
3523
|
+
body.innerHTML += '<div class="meta-row"><div class="meta-label">Connections (' + related.length + ')</div><div>' +
|
|
3524
|
+
related.map(function(cn) {
|
|
3525
|
+
var oid = cn.sourceAssetId === asset.id ? cn.targetAssetId : cn.sourceAssetId;
|
|
3526
|
+
var o = mapAssetIndex.get(oid);
|
|
3527
|
+
return '<div class="meta-value" style="margin-top:4px;font-size:12px">' + (o ? mEsc(o.name) : oid) + '</div>';
|
|
3528
|
+
}).join('') + '</div></div>';
|
|
3529
|
+
}
|
|
3530
|
+
document.getElementById('map-detail').classList.add('open');
|
|
3531
|
+
}
|
|
3532
|
+
document.getElementById('md-close').addEventListener('click', function() {
|
|
3533
|
+
document.getElementById('map-detail').classList.remove('open'); mSelectedId = null; drawMap();
|
|
2977
3534
|
});
|
|
2978
3535
|
|
|
2979
|
-
// \u2500\u2500
|
|
2980
|
-
document.getElementById('
|
|
2981
|
-
|
|
2982
|
-
|
|
3536
|
+
// \u2500\u2500 Map toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3537
|
+
document.getElementById('btn-labels').addEventListener('click', function() {
|
|
3538
|
+
mShowLabels = !mShowLabels; this.classList.toggle('active', mShowLabels); drawMap();
|
|
3539
|
+
});
|
|
3540
|
+
document.getElementById('btn-quality').addEventListener('click', function() {
|
|
3541
|
+
mShowQuality = !mShowQuality; this.classList.toggle('active', mShowQuality); drawMap();
|
|
3542
|
+
});
|
|
3543
|
+
document.getElementById('btn-connect').addEventListener('click', function() {
|
|
3544
|
+
mConnectMode = !mConnectMode; mConnectFirst = null;
|
|
3545
|
+
this.classList.toggle('active', mConnectMode);
|
|
3546
|
+
mapWrap.classList.toggle('connecting', mConnectMode);
|
|
3547
|
+
document.getElementById('map-connect-hint').style.display = mConnectMode ? 'block' : 'none'; drawMap();
|
|
2983
3548
|
});
|
|
2984
3549
|
|
|
2985
|
-
//
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3550
|
+
// Map keyboard
|
|
3551
|
+
mapWrap.addEventListener('keydown', function(e) {
|
|
3552
|
+
if (e.key === 'ArrowLeft') { mvx += 40; drawMap(); }
|
|
3553
|
+
else if (e.key === 'ArrowRight') { mvx -= 40; drawMap(); }
|
|
3554
|
+
else if (e.key === 'ArrowUp') { mvy += 40; drawMap(); }
|
|
3555
|
+
else if (e.key === 'ArrowDown') { mvy -= 40; drawMap(); }
|
|
3556
|
+
else if (e.key === '+' || e.key === '=') mApplyZoom(1.2, mW / 2, mH / 2);
|
|
3557
|
+
else if (e.key === '-') mApplyZoom(1 / 1.2, mW / 2, mH / 2);
|
|
3558
|
+
else if (e.key === 'Escape') {
|
|
3559
|
+
mSelectedId = null; document.getElementById('map-detail').classList.remove('open');
|
|
3560
|
+
if (mConnectMode) { mConnectMode = false; mConnectFirst = null; mapWrap.classList.remove('connecting'); document.getElementById('map-connect-hint').style.display = 'none'; document.getElementById('btn-connect').classList.remove('active'); }
|
|
3561
|
+
drawMap();
|
|
3562
|
+
}
|
|
3563
|
+
});
|
|
2990
3564
|
|
|
2991
|
-
|
|
3565
|
+
// Map init
|
|
3566
|
+
resizeMap(); mapFitToView();
|
|
3567
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
3568
|
+
drawMap();
|
|
3569
|
+
|
|
3570
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3571
|
+
// TOPOLOGY VIEW (lazy init)
|
|
3572
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3573
|
+
var TOPO = ${graphData};
|
|
3574
|
+
|
|
3575
|
+
var TYPE_COLORS = {
|
|
3576
|
+
host:'#4a9eff',database_server:'#ff6b6b',database:'#ff8c42',
|
|
3577
|
+
web_service:'#6bcb77',api_endpoint:'#4d96ff',cache_server:'#ffd93d',
|
|
3578
|
+
message_broker:'#c77dff',queue:'#e0aaff',topic:'#9d4edd',
|
|
3579
|
+
container:'#48cae4',pod:'#00b4d8',k8s_cluster:'#0077b6',
|
|
3580
|
+
config_file:'#adb5bd',saas_tool:'#c084fc',table:'#f97316',unknown:'#6c757d'
|
|
3581
|
+
};
|
|
3582
|
+
var LAYER_COLORS = { saas:'#c084fc',web:'#6bcb77',data:'#ff6b6b',messaging:'#c77dff',infra:'#4a9eff',config:'#adb5bd',other:'#6c757d' };
|
|
3583
|
+
var LAYER_NAMES = { saas:'SaaS Tools',web:'Web / API',data:'Data Layer',messaging:'Messaging',infra:'Infrastructure',config:'Config',other:'Other' };
|
|
3584
|
+
|
|
3585
|
+
var topoSelectedId = null;
|
|
3586
|
+
|
|
3587
|
+
function setTopoSearch(q) {
|
|
3588
|
+
var el = document.getElementById('topo-search');
|
|
3589
|
+
if (el) { el.value = q; buildTopoList(q); }
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
function buildTopoList(filter) {
|
|
3593
|
+
var listEl = document.getElementById('topo-list');
|
|
3594
|
+
var q = (filter || '').toLowerCase();
|
|
3595
|
+
listEl.innerHTML = '';
|
|
3596
|
+
var sorted = TOPO.nodes.slice().sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
3597
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
3598
|
+
var d = sorted[i];
|
|
3599
|
+
if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
|
|
3600
|
+
var item = document.createElement('div');
|
|
3601
|
+
item.className = 'topo-item' + (d.id === topoSelectedId ? ' active' : '');
|
|
3602
|
+
item.dataset.id = d.id;
|
|
3603
|
+
var color = TYPE_COLORS[d.type] || '#aaa';
|
|
3604
|
+
item.innerHTML = '<span class="topo-dot" style="background:' + color + '"></span>' +
|
|
3605
|
+
'<span class="topo-name" title="' + d.id + '">' + d.name + '</span>' +
|
|
3606
|
+
'<span class="topo-type">' + d.type.replace(/_/g, ' ') + '</span>';
|
|
3607
|
+
(function(dd) { item.onclick = function() { selectTopoNode(dd); focusTopoNode(dd); }; })(d);
|
|
3608
|
+
listEl.appendChild(item);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
document.getElementById('topo-search').addEventListener('input', function(e) { buildTopoList(e.target.value); });
|
|
3613
|
+
|
|
3614
|
+
var topoSidebar = document.getElementById('topo-sidebar');
|
|
3615
|
+
|
|
3616
|
+
function selectTopoNode(d) {
|
|
3617
|
+
topoSelectedId = d.id;
|
|
3618
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3619
|
+
showTopoNode(d);
|
|
3620
|
+
if (typeof d3 !== 'undefined') d3.selectAll('.node-hex').classed('selected', function(nd) { return nd.id === d.id; });
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
function showTopoNode(d) {
|
|
3624
|
+
var c = TYPE_COLORS[d.type] || '#aaa';
|
|
3625
|
+
var confPct = Math.round(d.confidence * 100);
|
|
3626
|
+
var tags = (d.tags || []).map(function(t) { return '<span class="tag">' + t + '</span>'; }).join('');
|
|
3627
|
+
var metaRows = Object.entries(d.metadata || {})
|
|
3628
|
+
.filter(function(kv) { return kv[1] !== null && kv[1] !== undefined && String(kv[1]).length > 0; })
|
|
3629
|
+
.map(function(kv) { return '<tr><td>' + kv[0] + '</td><td>' + JSON.stringify(kv[1]) + '</td></tr>'; }).join('');
|
|
3630
|
+
var related = TOPO.links.filter(function(l) {
|
|
3631
|
+
return (l.source.id || l.source) === d.id || (l.target.id || l.target) === d.id;
|
|
3632
|
+
});
|
|
3633
|
+
var edgeItems = related.map(function(l) {
|
|
3634
|
+
var isOut = (l.source.id || l.source) === d.id;
|
|
3635
|
+
var other = isOut ? (l.target.id || l.target) : (l.source.id || l.source);
|
|
3636
|
+
return '<div class="edge-item">' + (isOut ? '\\u2192' : '\\u2190') + ' <span>' + other + '</span> <small>[' + l.relationship + ']</small></div>';
|
|
3637
|
+
}).join('');
|
|
3638
|
+
|
|
3639
|
+
topoSidebar.innerHTML =
|
|
3640
|
+
'<h2>' + d.name + '</h2>' +
|
|
3641
|
+
'<table class="meta-table">' +
|
|
3642
|
+
'<tr><td>ID</td><td style="font-size:10px;word-break:break-all">' + d.id + '</td></tr>' +
|
|
3643
|
+
'<tr><td>Type</td><td><span style="color:' + c + '">' + d.type + '</span></td></tr>' +
|
|
3644
|
+
'<tr><td>Layer</td><td>' + d.layer + '</td></tr>' +
|
|
3645
|
+
'<tr><td>Confidence</td><td>' + confPct + '% <div class="conf-bar"><div class="conf-fill" style="width:' + confPct + '%;background:' + c + '"></div></div></td></tr>' +
|
|
3646
|
+
'<tr><td>Via</td><td>' + (d.discoveredVia || '\\u2014') + '</td></tr>' +
|
|
3647
|
+
'<tr><td>Timestamp</td><td>' + (d.discoveredAt ? d.discoveredAt.substring(0, 19).replace('T', ' ') : '\\u2014') + '</td></tr>' +
|
|
3648
|
+
(tags ? '<tr><td>Tags</td><td>' + tags + '</td></tr>' : '') +
|
|
3649
|
+
metaRows + '</table>' +
|
|
3650
|
+
(related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>' + edgeItems + '</div>' : '') +
|
|
3651
|
+
'<div style="margin-top:14px"><button class="export-btn" style="width:100%" onclick="deleteTopoNode(\\'' + d.id.replace(/'/g, "\\\\'") + '\\')">Delete node</button></div>';
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
function deleteTopoNode(id) {
|
|
3655
|
+
var idx = TOPO.nodes.findIndex(function(n) { return n.id === id; });
|
|
3656
|
+
if (idx === -1) return;
|
|
3657
|
+
TOPO.nodes.splice(idx, 1);
|
|
3658
|
+
TOPO.links = TOPO.links.filter(function(l) {
|
|
3659
|
+
return (l.source.id || l.source) !== id && (l.target.id || l.target) !== id;
|
|
3660
|
+
});
|
|
3661
|
+
topoSelectedId = null;
|
|
3662
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
|
|
3663
|
+
if (typeof rebuildTopoGraph === 'function') rebuildTopoGraph();
|
|
3664
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
function initTopology() {
|
|
3668
|
+
if (typeof d3 === 'undefined') return;
|
|
3669
|
+
|
|
3670
|
+
var svgEl = d3.select('#topo-graph svg');
|
|
3671
|
+
var graphDiv = document.getElementById('topo-graph');
|
|
3672
|
+
var gW = function() { return graphDiv.clientWidth; };
|
|
3673
|
+
var gH = function() { return graphDiv.clientHeight; };
|
|
3674
|
+
var g = svgEl.append('g');
|
|
3675
|
+
|
|
3676
|
+
svgEl.append('defs').append('marker')
|
|
3677
|
+
.attr('id', 'arrow').attr('viewBox', '0 0 10 6')
|
|
3678
|
+
.attr('refX', 10).attr('refY', 3)
|
|
3679
|
+
.attr('markerWidth', 8).attr('markerHeight', 6)
|
|
3680
|
+
.attr('orient', 'auto')
|
|
3681
|
+
.append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
|
|
3682
|
+
|
|
3683
|
+
var currentZoom = 1;
|
|
3684
|
+
var zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', function(e) {
|
|
3685
|
+
g.attr('transform', e.transform); currentZoom = e.transform.k; updateTopoLOD(currentZoom);
|
|
3686
|
+
});
|
|
3687
|
+
svgEl.call(zoomBehavior);
|
|
3688
|
+
|
|
3689
|
+
// Layer filters
|
|
3690
|
+
var layers = Array.from(new Set(TOPO.nodes.map(function(d) { return d.layer; })));
|
|
3691
|
+
var layerVisible = {};
|
|
3692
|
+
layers.forEach(function(l) { layerVisible[l] = true; });
|
|
3693
|
+
|
|
3694
|
+
var toolbarEl = document.getElementById('topo-toolbar');
|
|
3695
|
+
layers.forEach(function(layer) {
|
|
3696
|
+
var btn = document.createElement('button');
|
|
3697
|
+
btn.className = 'filter-btn';
|
|
3698
|
+
btn.innerHTML = '<span class="filter-dot" style="background:' + (LAYER_COLORS[layer] || '#666') + '"></span>' + (LAYER_NAMES[layer] || layer);
|
|
3699
|
+
btn.onclick = function() { layerVisible[layer] = !layerVisible[layer]; btn.classList.toggle('off', !layerVisible[layer]); updateTopoVisibility(); };
|
|
3700
|
+
toolbarEl.appendChild(btn);
|
|
3701
|
+
});
|
|
3702
|
+
|
|
3703
|
+
// JGF export button
|
|
3704
|
+
var jgfBtn = document.createElement('button');
|
|
3705
|
+
jgfBtn.className = 'export-btn'; jgfBtn.textContent = '\\u2193 JGF'; jgfBtn.title = 'Export JSON Graph Format';
|
|
3706
|
+
jgfBtn.onclick = function() {
|
|
3707
|
+
var jgf = { graph: { directed: true, type: 'cartography', label: 'Infrastructure Map',
|
|
3708
|
+
metadata: { exportedAt: new Date().toISOString() },
|
|
3709
|
+
nodes: Object.fromEntries(TOPO.nodes.map(function(n) { return [n.id, { label: n.name, metadata: { type: n.type, layer: n.layer, confidence: n.confidence, discoveredVia: n.discoveredVia, discoveredAt: n.discoveredAt, tags: n.tags } }]; })),
|
|
3710
|
+
edges: TOPO.links.map(function(l) { return { source: l.source.id || l.source, target: l.target.id || l.target, relation: l.relationship, metadata: { confidence: l.confidence, evidence: l.evidence } }; })
|
|
3711
|
+
}};
|
|
3712
|
+
var blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
|
|
3713
|
+
var url = URL.createObjectURL(blob);
|
|
3714
|
+
var a = document.createElement('a'); a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
|
|
3715
|
+
URL.revokeObjectURL(url);
|
|
3716
|
+
};
|
|
3717
|
+
toolbarEl.appendChild(jgfBtn);
|
|
3718
|
+
|
|
3719
|
+
// Hex helpers
|
|
3720
|
+
var T_HEX = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
|
|
3721
|
+
function tHexSize(d) { return T_HEX[d.type] || T_HEX.default; }
|
|
3722
|
+
function tHexPath(size) {
|
|
3723
|
+
var pts = [];
|
|
3724
|
+
for (var i = 0; i < 6; i++) {
|
|
3725
|
+
var angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
3726
|
+
pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
|
|
3727
|
+
}
|
|
3728
|
+
return 'M' + pts.map(function(p) { return p.join(','); }).join('L') + 'Z';
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// Cluster force
|
|
3732
|
+
function clusterForce(alpha) {
|
|
3733
|
+
var centroids = {}, counts = {};
|
|
3734
|
+
TOPO.nodes.forEach(function(d) {
|
|
3735
|
+
if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
|
|
3736
|
+
centroids[d.layer].x += d.x || 0; centroids[d.layer].y += d.y || 0; counts[d.layer]++;
|
|
3737
|
+
});
|
|
3738
|
+
for (var l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
|
|
3739
|
+
var strength = alpha * 0.15;
|
|
3740
|
+
TOPO.nodes.forEach(function(d) {
|
|
3741
|
+
var cn = centroids[d.layer];
|
|
3742
|
+
if (cn) { d.vx += (cn.x - d.x) * strength; d.vy += (cn.y - d.y) * strength; }
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
// Hulls
|
|
3747
|
+
var hullGroup = g.append('g').attr('class', 'hulls');
|
|
3748
|
+
var hullPaths = {}, hullLabels = {};
|
|
3749
|
+
layers.forEach(function(layer) {
|
|
3750
|
+
hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
|
|
3751
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
|
|
3752
|
+
hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
|
|
3753
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
|
|
3754
|
+
});
|
|
3755
|
+
|
|
3756
|
+
function updateHulls() {
|
|
3757
|
+
layers.forEach(function(layer) {
|
|
3758
|
+
if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
|
|
3759
|
+
var pts = TOPO.nodes.filter(function(d) { return d.layer === layer && layerVisible[d.layer]; }).map(function(d) { return [d.x, d.y]; });
|
|
3760
|
+
if (pts.length < 3) {
|
|
3761
|
+
hullPaths[layer].attr('d', null);
|
|
3762
|
+
if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
|
|
3763
|
+
else hullLabels[layer].attr('x', -9999);
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
var hull = d3.polygonHull(pts);
|
|
3767
|
+
if (!hull) { hullPaths[layer].attr('d', null); return; }
|
|
3768
|
+
var cx = d3.mean(hull, function(p) { return p[0]; });
|
|
3769
|
+
var cy = d3.mean(hull, function(p) { return p[1]; });
|
|
3770
|
+
var padded = hull.map(function(p) {
|
|
3771
|
+
var dx = p[0] - cx, dy = p[1] - cy;
|
|
3772
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3773
|
+
return [p[0] + dx / len * 40, p[1] + dy / len * 40];
|
|
3774
|
+
});
|
|
3775
|
+
hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
|
|
3776
|
+
hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, function(p) { return Math.abs(p[1] - cy); }) - 30);
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// Graph
|
|
3781
|
+
var linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
|
|
3782
|
+
var linkGroup = g.append('g');
|
|
3783
|
+
var nodeGroup = g.append('g');
|
|
3784
|
+
|
|
3785
|
+
function focusTopoNode(d) {
|
|
3786
|
+
if (!d.x || !d.y) return;
|
|
3787
|
+
var w = gW(), h = gH();
|
|
3788
|
+
svgEl.transition().duration(500).call(
|
|
3789
|
+
zoomBehavior.transform,
|
|
3790
|
+
d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
window.focusTopoNode = focusTopoNode;
|
|
3794
|
+
|
|
3795
|
+
function rebuildTopoGraph() {
|
|
3796
|
+
if (sim) sim.stop();
|
|
3797
|
+
|
|
3798
|
+
linkSel = linkGroup.selectAll('line').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
3799
|
+
linkSel.exit().remove();
|
|
3800
|
+
var linkEnter = linkSel.enter().append('line').attr('class', 'link');
|
|
3801
|
+
linkSel = linkEnter.merge(linkSel)
|
|
3802
|
+
.attr('stroke', function(d) { return d.confidence < 0.6 ? '#2a2e35' : '#3d434b'; })
|
|
3803
|
+
.attr('stroke-dasharray', function(d) { return d.confidence < 0.6 ? '4 3' : null; })
|
|
3804
|
+
.attr('stroke-width', function(d) { return d.confidence < 0.6 ? 0.8 : 1.2; })
|
|
3805
|
+
.attr('marker-end', 'url(#arrow)');
|
|
3806
|
+
linkSel.select('title').remove();
|
|
3807
|
+
linkSel.append('title').text(function(d) { return d.relationship + ' (' + Math.round(d.confidence * 100) + '%)\\n' + (d.evidence || ''); });
|
|
3808
|
+
|
|
3809
|
+
linkLabelSel = linkGroup.selectAll('text').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
3810
|
+
linkLabelSel.exit().remove();
|
|
3811
|
+
linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel).text(function(d) { return d.relationship; });
|
|
3812
|
+
|
|
3813
|
+
nodeSel = nodeGroup.selectAll('g').data(TOPO.nodes, function(d) { return d.id; });
|
|
3814
|
+
nodeSel.exit().remove();
|
|
3815
|
+
var nodeEnter = nodeSel.enter().append('g')
|
|
3816
|
+
.call(d3.drag()
|
|
3817
|
+
.on('start', function(e, d) { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
3818
|
+
.on('drag', function(e, d) { d.fx = e.x; d.fy = e.y; })
|
|
3819
|
+
.on('end', function(e, d) { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
3820
|
+
)
|
|
3821
|
+
.on('click', function(e, d) { e.stopPropagation(); selectTopoNode(d); });
|
|
3822
|
+
nodeEnter.append('path').attr('class', 'node-hex');
|
|
3823
|
+
nodeEnter.append('title');
|
|
3824
|
+
nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
|
|
3825
|
+
|
|
3826
|
+
nodeSel = nodeEnter.merge(nodeSel);
|
|
3827
|
+
nodeSel.select('.node-hex')
|
|
3828
|
+
.attr('d', function(d) { return tHexPath(tHexSize(d)); })
|
|
3829
|
+
.attr('fill', function(d) { return TYPE_COLORS[d.type] || '#aaa'; })
|
|
3830
|
+
.attr('stroke', function(d) { var c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
|
|
3831
|
+
.attr('fill-opacity', function(d) { return 0.6 + d.confidence * 0.4; })
|
|
3832
|
+
.classed('selected', function(d) { return d.id === topoSelectedId; });
|
|
3833
|
+
nodeSel.select('title').text(function(d) { return d.name + ' (' + d.type + ')\\nconf: ' + Math.round(d.confidence * 100) + '%'; });
|
|
3834
|
+
nodeLabelSel = nodeSel.select('.node-label')
|
|
3835
|
+
.attr('dy', function(d) { return tHexSize(d) + 13; })
|
|
3836
|
+
.text(function(d) { return d.name.length > 20 ? d.name.substring(0, 18) + '\\u2026' : d.name; });
|
|
3837
|
+
|
|
3838
|
+
sim = d3.forceSimulation(TOPO.nodes)
|
|
3839
|
+
.force('link', d3.forceLink(TOPO.links).id(function(d) { return d.id; }).distance(function(d) { return d.relationship === 'contains' ? 50 : 100; }).strength(0.4))
|
|
3840
|
+
.force('charge', d3.forceManyBody().strength(-280))
|
|
3841
|
+
.force('center', d3.forceCenter(gW() / 2, gH() / 2))
|
|
3842
|
+
.force('collision', d3.forceCollide().radius(function(d) { return tHexSize(d) + 10; }))
|
|
3843
|
+
.force('cluster', clusterForce)
|
|
3844
|
+
.on('tick', function() {
|
|
3845
|
+
updateHulls();
|
|
3846
|
+
linkSel.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; })
|
|
3847
|
+
.attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; });
|
|
3848
|
+
linkLabelSel.attr('x', function(d) { return (d.source.x + d.target.x) / 2; })
|
|
3849
|
+
.attr('y', function(d) { return (d.source.y + d.target.y) / 2 - 4; });
|
|
3850
|
+
nodeSel.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
window.rebuildTopoGraph = rebuildTopoGraph;
|
|
3854
|
+
|
|
3855
|
+
function updateTopoLOD(k) {
|
|
3856
|
+
if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
|
|
3857
|
+
if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
|
|
3858
|
+
d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
function updateTopoVisibility() {
|
|
3862
|
+
if (!nodeSel) return;
|
|
3863
|
+
nodeSel.style('display', function(d) { return layerVisible[d.layer] ? null : 'none'; });
|
|
3864
|
+
linkSel.style('display', function(d) {
|
|
3865
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
3866
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
3867
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
3868
|
+
});
|
|
3869
|
+
linkLabelSel.style('display', function(d) {
|
|
3870
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
3871
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
3872
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
rebuildTopoGraph();
|
|
3877
|
+
buildTopoList();
|
|
3878
|
+
updateTopoLOD(1);
|
|
3879
|
+
|
|
3880
|
+
svgEl.on('click', function() {
|
|
3881
|
+
topoSelectedId = null;
|
|
3882
|
+
d3.selectAll('.node-hex').classed('selected', false);
|
|
3883
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3884
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// Init topology node list (non-D3 part)
|
|
3889
|
+
buildTopoList();
|
|
2992
3890
|
</script>
|
|
2993
3891
|
</body>
|
|
2994
3892
|
</html>`;
|
|
2995
3893
|
}
|
|
3894
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "discovery", "sops"]) {
|
|
3895
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
3896
|
+
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
3897
|
+
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
3898
|
+
const nodes = db.getNodes(sessionId);
|
|
3899
|
+
const edges = db.getEdges(sessionId);
|
|
3900
|
+
if (formats.includes("mermaid")) {
|
|
3901
|
+
writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
3902
|
+
writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
3903
|
+
process.stderr.write("\u2713 topology.mermaid, dependencies.mermaid\n");
|
|
3904
|
+
}
|
|
3905
|
+
if (formats.includes("json")) {
|
|
3906
|
+
writeFileSync(join2(outputDir, "catalog.json"), exportJSON(db, sessionId));
|
|
3907
|
+
process.stderr.write("\u2713 catalog.json\n");
|
|
3908
|
+
}
|
|
3909
|
+
if (formats.includes("yaml")) {
|
|
3910
|
+
writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
3911
|
+
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
3912
|
+
}
|
|
3913
|
+
if (formats.includes("html")) {
|
|
3914
|
+
writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
|
|
3915
|
+
process.stderr.write("\u2713 topology.html\n");
|
|
3916
|
+
}
|
|
3917
|
+
if (formats.includes("map")) {
|
|
3918
|
+
writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
|
|
3919
|
+
process.stderr.write("\u2713 cartography-map.html\n");
|
|
3920
|
+
}
|
|
3921
|
+
if (formats.includes("discovery")) {
|
|
3922
|
+
writeFileSync(join2(outputDir, "discovery.html"), exportDiscoveryApp(nodes, edges));
|
|
3923
|
+
process.stderr.write("\u2713 discovery.html\n");
|
|
3924
|
+
}
|
|
3925
|
+
if (formats.includes("sops")) {
|
|
3926
|
+
const sops = db.getSOPs(sessionId);
|
|
3927
|
+
for (const sop of sops) {
|
|
3928
|
+
const filename = sop.title.toLowerCase().replace(/[^a-z0-9]+/g, "-") + ".md";
|
|
3929
|
+
writeFileSync(join2(outputDir, "sops", filename), exportSOPMarkdown(sop));
|
|
3930
|
+
const wfFilename = `workflow-${sop.workflowId.substring(0, 8)}.mermaid`;
|
|
3931
|
+
writeFileSync(join2(outputDir, "workflows", wfFilename), generateWorkflowMermaid(sop));
|
|
3932
|
+
}
|
|
3933
|
+
if (sops.length > 0) {
|
|
3934
|
+
process.stderr.write(`\u2713 ${sops.length} SOPs + workflow diagrams
|
|
3935
|
+
`);
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
2996
3939
|
|
|
2997
3940
|
// src/cli.ts
|
|
2998
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3941
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2999
3942
|
import { resolve } from "path";
|
|
3000
3943
|
import { createInterface } from "readline";
|
|
3001
3944
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -3011,7 +3954,7 @@ function main() {
|
|
|
3011
3954
|
const program = new Command();
|
|
3012
3955
|
const CMD = "datasynx-cartography";
|
|
3013
3956
|
const VERSION = "0.2.3";
|
|
3014
|
-
program.name(CMD).description("AI-powered Infrastructure Cartography").version(VERSION);
|
|
3957
|
+
program.name(CMD).description("AI-powered Infrastructure Cartography & SOP Generation").version(VERSION);
|
|
3015
3958
|
program.command("discover").description("Scan and map your infrastructure").option("--entry <hosts...>", "Entry points", ["localhost"]).option("--depth <n>", "Max crawl depth", "8").option("--max-turns <n>", "Max agent turns", "50").option("--model <m>", "Agent model", "claude-sonnet-4-5-20250929").option("--org <name>", "Organization name (for Backstage)").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--db <path>", "DB path").option("-v, --verbose", "Show agent reasoning", false).action(async (opts) => {
|
|
3016
3959
|
checkPrerequisites();
|
|
3017
3960
|
const config = defaultConfig({
|
|
@@ -3223,31 +4166,27 @@ function main() {
|
|
|
3223
4166
|
exportAll(db, sessionId, config.outputDir);
|
|
3224
4167
|
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
3225
4168
|
const htmlPath = resolve(config.outputDir, "topology.html");
|
|
3226
|
-
const
|
|
4169
|
+
const mapPath = resolve(config.outputDir, "cartography-map.html");
|
|
4170
|
+
const discoveryPath = resolve(config.outputDir, "discovery.html");
|
|
3227
4171
|
const topoPath = resolve(config.outputDir, "topology.mermaid");
|
|
3228
4172
|
w("\n");
|
|
3229
|
-
if (existsSync2(
|
|
3230
|
-
|
|
3231
|
-
w(` ${green("\u2192")} ${osc8(fileUrl, bold("Open hexmap.html"))} ${dim("\u2190 Data Cartography Map")}
|
|
4173
|
+
if (existsSync2(discoveryPath)) {
|
|
4174
|
+
w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("Open discovery.html"))} ${dim("\u2190 Enterprise Discovery Frontend")}
|
|
3232
4175
|
`);
|
|
3233
|
-
|
|
4176
|
+
}
|
|
4177
|
+
if (existsSync2(mapPath)) {
|
|
4178
|
+
w(` ${green("\u2192")} ${osc8(`file://${mapPath}`, bold("Open cartography-map.html"))} ${dim("\u2190 Hex Map")}
|
|
3234
4179
|
`);
|
|
3235
4180
|
}
|
|
3236
4181
|
if (existsSync2(htmlPath)) {
|
|
3237
|
-
|
|
3238
|
-
w(` ${green("\u2192")} ${osc8(fileUrl, bold("Open topology.html"))}
|
|
3239
|
-
`);
|
|
3240
|
-
w(` ${dim(fileUrl)}
|
|
4182
|
+
w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
|
|
3241
4183
|
`);
|
|
3242
4184
|
}
|
|
3243
4185
|
if (existsSync2(topoPath)) {
|
|
3244
4186
|
try {
|
|
3245
4187
|
const code = readFileSync2(topoPath, "utf8");
|
|
3246
|
-
const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("
|
|
3247
|
-
|
|
3248
|
-
w(` ${cyan("\u2192")} ${osc8(liveUrl, bold("Open in mermaid.live"))}
|
|
3249
|
-
`);
|
|
3250
|
-
w(` ${dim(liveUrl)}
|
|
4188
|
+
const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64");
|
|
4189
|
+
w(` ${cyan("\u2192")} ${osc8(`https://mermaid.live/view#base64:${b64}`, bold("Open in mermaid.live"))}
|
|
3251
4190
|
`);
|
|
3252
4191
|
} catch {
|
|
3253
4192
|
}
|
|
@@ -3302,7 +4241,7 @@ function main() {
|
|
|
3302
4241
|
}
|
|
3303
4242
|
db.close();
|
|
3304
4243
|
});
|
|
3305
|
-
program.command("export [session-id]").description("Generate all output files").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--format <fmt...>", "Formats: mermaid,json,yaml,html,
|
|
4244
|
+
program.command("export [session-id]").description("Generate all output files").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--format <fmt...>", "Formats: mermaid,json,yaml,html,map,sops").action((sessionId, opts) => {
|
|
3306
4245
|
const config = defaultConfig({ outputDir: opts.output });
|
|
3307
4246
|
const db = new CartographyDB(config.dbPath);
|
|
3308
4247
|
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
@@ -3312,34 +4251,33 @@ function main() {
|
|
|
3312
4251
|
process.exitCode = 1;
|
|
3313
4252
|
return;
|
|
3314
4253
|
}
|
|
3315
|
-
const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "
|
|
4254
|
+
const formats = opts.format ?? ["mermaid", "json", "yaml", "html", "map", "sops"];
|
|
3316
4255
|
exportAll(db, session.id, opts.output, formats);
|
|
3317
4256
|
process.stderr.write(`\u2713 Exported to: ${opts.output}
|
|
3318
4257
|
`);
|
|
3319
4258
|
db.close();
|
|
3320
4259
|
});
|
|
3321
|
-
program.command("map [session-id]").description("Open the interactive Data Cartography hex map in your browser").option("-o, --output <dir>", "Output directory", "./datasynx-output").action((sessionId, opts) => {
|
|
4260
|
+
program.command("map [session-id]").description("Open the interactive Data Cartography hex map in your browser").option("-o, --output <dir>", "Output directory", "./datasynx-output").option("--theme <theme>", "Theme: light or dark", "light").action((sessionId, opts) => {
|
|
3322
4261
|
const config = defaultConfig({ outputDir: opts.output });
|
|
3323
4262
|
const db = new CartographyDB(config.dbPath);
|
|
3324
4263
|
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
3325
4264
|
if (!session) {
|
|
3326
|
-
process.stderr.write("
|
|
4265
|
+
process.stderr.write("No session found. Run discover first.\n");
|
|
3327
4266
|
db.close();
|
|
3328
4267
|
process.exitCode = 1;
|
|
3329
4268
|
return;
|
|
3330
4269
|
}
|
|
3331
4270
|
const nodes = db.getNodes(session.id);
|
|
3332
|
-
const
|
|
4271
|
+
const edges = db.getEdges(session.id);
|
|
3333
4272
|
const outDir = resolve(opts.output);
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
});
|
|
3337
|
-
writeFileSync2(outPath, exportHexMap(nodes, connections));
|
|
4273
|
+
mkdirSync3(outDir, { recursive: true });
|
|
4274
|
+
const outPath = resolve(outDir, "cartography-map.html");
|
|
4275
|
+
writeFileSync2(outPath, exportCartographyMap(nodes, edges, { theme: opts.theme }));
|
|
3338
4276
|
db.close();
|
|
3339
4277
|
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
3340
4278
|
const fileUrl = `file://${outPath}`;
|
|
3341
4279
|
process.stderr.write(`
|
|
3342
|
-
${green("
|
|
4280
|
+
${green("OK")} ${osc8(fileUrl, bold("Open cartography-map.html"))}
|
|
3343
4281
|
`);
|
|
3344
4282
|
process.stderr.write(` ${dim(fileUrl)}
|
|
3345
4283
|
|
|
@@ -3592,7 +4530,9 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3592
4530
|
out(dim(" catalog-info.yaml Backstage service catalog\n"));
|
|
3593
4531
|
out(dim(" topology.mermaid Infrastructure topology (graph TB)\n"));
|
|
3594
4532
|
out(dim(" dependencies.mermaid Service dependencies (graph LR)\n"));
|
|
4533
|
+
out(dim(" discovery.html Enterprise discovery frontend (Map + Topology)\n"));
|
|
3595
4534
|
out(dim(" topology.html Interactive D3.js force graph\n"));
|
|
4535
|
+
out(dim(" cartography-map.html Hex grid data cartography map\n"));
|
|
3596
4536
|
out(dim(" sops/ Generated SOPs as Markdown\n"));
|
|
3597
4537
|
out(dim(" workflows/ Workflow flowcharts as Mermaid\n"));
|
|
3598
4538
|
out("\n");
|
|
@@ -3708,9 +4648,6 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3708
4648
|
const port = entry["port"];
|
|
3709
4649
|
const tags = entry["tags"] ?? [];
|
|
3710
4650
|
const metadata = entry["metadata"] ?? {};
|
|
3711
|
-
const domain = entry["domain"];
|
|
3712
|
-
const subDomain = entry["subDomain"];
|
|
3713
|
-
const qualityScore = entry["qualityScore"];
|
|
3714
4651
|
if (!type || !name) {
|
|
3715
4652
|
w(yellow(` \u26A0 Skipped (no type/name): ${JSON.stringify(entry)}
|
|
3716
4653
|
`));
|
|
@@ -3724,10 +4661,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3724
4661
|
discoveredVia: "manual",
|
|
3725
4662
|
confidence: 1,
|
|
3726
4663
|
metadata: { ...metadata, ...host ? { host } : {}, ...port ? { port } : {} },
|
|
3727
|
-
tags
|
|
3728
|
-
domain,
|
|
3729
|
-
subDomain,
|
|
3730
|
-
qualityScore
|
|
4664
|
+
tags
|
|
3731
4665
|
});
|
|
3732
4666
|
out(` ${green("+")} ${cyan(id)} ${dim("(" + type + ")")}
|
|
3733
4667
|
`);
|
|
@@ -3740,7 +4674,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3740
4674
|
`);
|
|
3741
4675
|
return;
|
|
3742
4676
|
}
|
|
3743
|
-
const { NODE_TYPES: NODE_TYPES2 } = await import("./types-
|
|
4677
|
+
const { NODE_TYPES: NODE_TYPES2 } = await import("./types-ZD6G5JKR.js");
|
|
3744
4678
|
if (!process.stdin.isTTY) {
|
|
3745
4679
|
w(red("\n \u2717 Interactive mode requires a terminal (use --file for non-interactive)\n\n"));
|
|
3746
4680
|
process.exitCode = 1;
|
|
@@ -3783,15 +4717,9 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3783
4717
|
const hostRaw = (await ask(` ${cyan("Host / IP")} ${dim("[optional, Enter=skip]")}: `)).trim();
|
|
3784
4718
|
const portRaw = (await ask(` ${cyan("Port")} ${dim("[optional]")}: `)).trim();
|
|
3785
4719
|
const tagsRaw = (await ask(` ${cyan("Tags")} ${dim("[comma-separated, optional]")}: `)).trim();
|
|
3786
|
-
const domainRaw = (await ask(` ${cyan("Domain")} ${dim('[e.g. "Marketing", "Finance", optional]')}: `)).trim();
|
|
3787
|
-
const subDomainRaw = (await ask(` ${cyan("Sub-domain")} ${dim('[e.g. "Forecast client orders", optional]')}: `)).trim();
|
|
3788
|
-
const qualityRaw = (await ask(` ${cyan("Quality score")} ${dim("[0\u2013100, optional]")}: `)).trim();
|
|
3789
4720
|
const host = hostRaw || void 0;
|
|
3790
4721
|
const port = portRaw ? parseInt(portRaw, 10) : void 0;
|
|
3791
4722
|
const tags = tagsRaw ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
3792
|
-
const domain = domainRaw || void 0;
|
|
3793
|
-
const subDomain = subDomainRaw || void 0;
|
|
3794
|
-
const qualityScore = qualityRaw ? Math.max(0, Math.min(100, parseFloat(qualityRaw))) : void 0;
|
|
3795
4723
|
const id = host ? `${nodeType}:${host}${port ? ":" + port : ""}` : `${nodeType}:${name.toLowerCase().replace(/\s+/g, "-")}`;
|
|
3796
4724
|
db.upsertNode(sessionId, {
|
|
3797
4725
|
id,
|
|
@@ -3800,10 +4728,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3800
4728
|
discoveredVia: "manual",
|
|
3801
4729
|
confidence: 1,
|
|
3802
4730
|
metadata: { ...host ? { host } : {}, ...port ? { port } : {} },
|
|
3803
|
-
tags
|
|
3804
|
-
domain,
|
|
3805
|
-
subDomain,
|
|
3806
|
-
qualityScore
|
|
4731
|
+
tags
|
|
3807
4732
|
});
|
|
3808
4733
|
out(` ${green("+")} ${cyan(id)}
|
|
3809
4734
|
`);
|