@datasynx/agentic-ai-cartography 0.3.3 → 0.5.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-FFNOC6HF.js → chunk-A7FTULDM.js} +67 -2
- package/dist/chunk-A7FTULDM.js.map +1 -0
- package/dist/cli.js +1008 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +223 -1
- package/dist/index.js +1073 -11
- package/dist/index.js.map +1 -1
- package/dist/{types-ROE3Z6HY.js → types-ZD6G5JKR.js} +12 -2
- package/package.json +1 -1
- package/dist/chunk-FFNOC6HF.js.map +0 -1
- /package/dist/{types-ROE3Z6HY.js.map → types-ZD6G5JKR.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -27,9 +27,21 @@ CREATE TABLE IF NOT EXISTS nodes (
|
|
|
27
27
|
confidence REAL DEFAULT 0.5,
|
|
28
28
|
metadata TEXT NOT NULL DEFAULT '{}',
|
|
29
29
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
30
|
+
domain TEXT,
|
|
31
|
+
sub_domain TEXT,
|
|
32
|
+
quality_score REAL,
|
|
30
33
|
PRIMARY KEY (id, session_id)
|
|
31
34
|
);
|
|
32
35
|
|
|
36
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
37
|
+
id TEXT PRIMARY KEY,
|
|
38
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
39
|
+
source_asset_id TEXT NOT NULL,
|
|
40
|
+
target_asset_id TEXT NOT NULL,
|
|
41
|
+
type TEXT,
|
|
42
|
+
created_at TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
33
45
|
CREATE TABLE IF NOT EXISTS edges (
|
|
34
46
|
id TEXT PRIMARY KEY,
|
|
35
47
|
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
@@ -104,6 +116,7 @@ CREATE INDEX IF NOT EXISTS idx_edges_session ON edges(session_id);
|
|
|
104
116
|
CREATE INDEX IF NOT EXISTS idx_events_session ON activity_events(session_id);
|
|
105
117
|
CREATE INDEX IF NOT EXISTS idx_events_task ON activity_events(task_id);
|
|
106
118
|
CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
107
120
|
`;
|
|
108
121
|
var CartographyDB = class {
|
|
109
122
|
db;
|
|
@@ -119,7 +132,24 @@ var CartographyDB = class {
|
|
|
119
132
|
const version = this.db.pragma("user_version", { simple: true });
|
|
120
133
|
if (version === 0) {
|
|
121
134
|
this.db.exec(SCHEMA);
|
|
122
|
-
this.db.pragma("user_version =
|
|
135
|
+
this.db.pragma("user_version = 2");
|
|
136
|
+
} else if (version === 1) {
|
|
137
|
+
const cols = this.db.prepare("PRAGMA table_info(nodes)").all().map((c) => c.name);
|
|
138
|
+
if (!cols.includes("domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN domain TEXT");
|
|
139
|
+
if (!cols.includes("sub_domain")) this.db.exec("ALTER TABLE nodes ADD COLUMN sub_domain TEXT");
|
|
140
|
+
if (!cols.includes("quality_score")) this.db.exec("ALTER TABLE nodes ADD COLUMN quality_score REAL");
|
|
141
|
+
this.db.exec(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
143
|
+
id TEXT PRIMARY KEY,
|
|
144
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
145
|
+
source_asset_id TEXT NOT NULL,
|
|
146
|
+
target_asset_id TEXT NOT NULL,
|
|
147
|
+
type TEXT,
|
|
148
|
+
created_at TEXT NOT NULL
|
|
149
|
+
);
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_connections_session ON connections(session_id);
|
|
151
|
+
`);
|
|
152
|
+
this.db.pragma("user_version = 2");
|
|
123
153
|
}
|
|
124
154
|
}
|
|
125
155
|
close() {
|
|
@@ -162,8 +192,9 @@ var CartographyDB = class {
|
|
|
162
192
|
upsertNode(sessionId, node, depth = 0) {
|
|
163
193
|
this.db.prepare(`
|
|
164
194
|
INSERT OR REPLACE INTO nodes
|
|
165
|
-
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags
|
|
166
|
-
|
|
195
|
+
(id, session_id, type, name, discovered_via, discovered_at, depth, confidence, metadata, tags,
|
|
196
|
+
domain, sub_domain, quality_score)
|
|
197
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
167
198
|
`).run(
|
|
168
199
|
node.id,
|
|
169
200
|
sessionId,
|
|
@@ -174,12 +205,18 @@ var CartographyDB = class {
|
|
|
174
205
|
depth,
|
|
175
206
|
node.confidence,
|
|
176
207
|
JSON.stringify(node.metadata ?? {}),
|
|
177
|
-
JSON.stringify(node.tags ?? [])
|
|
208
|
+
JSON.stringify(node.tags ?? []),
|
|
209
|
+
node.domain ?? null,
|
|
210
|
+
node.subDomain ?? null,
|
|
211
|
+
node.qualityScore ?? null
|
|
178
212
|
);
|
|
179
213
|
}
|
|
180
214
|
getNodes(sessionId) {
|
|
181
215
|
const rows = this.db.prepare("SELECT * FROM nodes WHERE session_id = ?").all(sessionId);
|
|
182
|
-
return rows.map((r) => (
|
|
216
|
+
return rows.map((r) => this.mapNode(r));
|
|
217
|
+
}
|
|
218
|
+
mapNode(r) {
|
|
219
|
+
return {
|
|
183
220
|
id: r["id"],
|
|
184
221
|
sessionId: r["session_id"],
|
|
185
222
|
type: r["type"],
|
|
@@ -190,8 +227,11 @@ var CartographyDB = class {
|
|
|
190
227
|
confidence: r["confidence"],
|
|
191
228
|
metadata: JSON.parse(r["metadata"]),
|
|
192
229
|
tags: JSON.parse(r["tags"]),
|
|
193
|
-
pathId: r["path_id"]
|
|
194
|
-
|
|
230
|
+
pathId: r["path_id"],
|
|
231
|
+
domain: r["domain"] ?? void 0,
|
|
232
|
+
subDomain: r["sub_domain"] ?? void 0,
|
|
233
|
+
qualityScore: r["quality_score"] ?? void 0
|
|
234
|
+
};
|
|
195
235
|
}
|
|
196
236
|
deleteNode(sessionId, nodeId) {
|
|
197
237
|
this.db.prepare("DELETE FROM nodes WHERE session_id = ? AND id = ?").run(sessionId, nodeId);
|
|
@@ -403,6 +443,33 @@ var CartographyDB = class {
|
|
|
403
443
|
generatedAt: r["generated_at"]
|
|
404
444
|
}));
|
|
405
445
|
}
|
|
446
|
+
// ── Connections (user-created hex map links) ─────────────────────────────
|
|
447
|
+
upsertConnection(sessionId, conn) {
|
|
448
|
+
const existing = this.db.prepare(
|
|
449
|
+
"SELECT id FROM connections WHERE session_id = ? AND source_asset_id = ? AND target_asset_id = ?"
|
|
450
|
+
).get(sessionId, conn.sourceAssetId, conn.targetAssetId);
|
|
451
|
+
if (existing) return existing.id;
|
|
452
|
+
const id = crypto.randomUUID();
|
|
453
|
+
this.db.prepare(`
|
|
454
|
+
INSERT INTO connections (id, session_id, source_asset_id, target_asset_id, type, created_at)
|
|
455
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
456
|
+
`).run(id, sessionId, conn.sourceAssetId, conn.targetAssetId, conn.type ?? null, (/* @__PURE__ */ new Date()).toISOString());
|
|
457
|
+
return id;
|
|
458
|
+
}
|
|
459
|
+
getConnections(sessionId) {
|
|
460
|
+
const rows = this.db.prepare("SELECT * FROM connections WHERE session_id = ?").all(sessionId);
|
|
461
|
+
return rows.map((r) => ({
|
|
462
|
+
id: r["id"],
|
|
463
|
+
sessionId: r["session_id"],
|
|
464
|
+
sourceAssetId: r["source_asset_id"],
|
|
465
|
+
targetAssetId: r["target_asset_id"],
|
|
466
|
+
type: r["type"] ?? void 0,
|
|
467
|
+
createdAt: r["created_at"]
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
deleteConnection(sessionId, connectionId) {
|
|
471
|
+
this.db.prepare("DELETE FROM connections WHERE session_id = ? AND id = ?").run(sessionId, connectionId);
|
|
472
|
+
}
|
|
406
473
|
// ── Approvals ───────────────────────────
|
|
407
474
|
setApproval(pattern, action) {
|
|
408
475
|
this.db.prepare(`
|
|
@@ -461,7 +528,10 @@ var NodeSchema = z.object({
|
|
|
461
528
|
discoveredVia: z.string(),
|
|
462
529
|
confidence: z.number().min(0).max(1).default(0.5),
|
|
463
530
|
metadata: z.record(z.unknown()).default({}),
|
|
464
|
-
tags: z.array(z.string()).default([])
|
|
531
|
+
tags: z.array(z.string()).default([]),
|
|
532
|
+
domain: z.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
|
|
533
|
+
subDomain: z.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
|
|
534
|
+
qualityScore: z.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
|
|
465
535
|
});
|
|
466
536
|
var EdgeSchema = z.object({
|
|
467
537
|
sourceId: z.string(),
|
|
@@ -486,6 +556,63 @@ var SOPSchema = z.object({
|
|
|
486
556
|
frequency: z.string(),
|
|
487
557
|
confidence: z.number().min(0).max(1)
|
|
488
558
|
});
|
|
559
|
+
var DataAssetSchema = z.object({
|
|
560
|
+
id: z.string(),
|
|
561
|
+
name: z.string(),
|
|
562
|
+
domain: z.string(),
|
|
563
|
+
subDomain: z.string().optional(),
|
|
564
|
+
qualityScore: z.number().min(0).max(100).optional(),
|
|
565
|
+
metadata: z.record(z.unknown()).default({}),
|
|
566
|
+
position: z.object({ q: z.number(), r: z.number() })
|
|
567
|
+
});
|
|
568
|
+
var ClusterSchema = z.object({
|
|
569
|
+
id: z.string(),
|
|
570
|
+
label: z.string(),
|
|
571
|
+
domain: z.string(),
|
|
572
|
+
color: z.string(),
|
|
573
|
+
assetIds: z.array(z.string()),
|
|
574
|
+
centroid: z.object({ x: z.number(), y: z.number() })
|
|
575
|
+
});
|
|
576
|
+
var ConnectionSchema = z.object({
|
|
577
|
+
id: z.string(),
|
|
578
|
+
sourceAssetId: z.string(),
|
|
579
|
+
targetAssetId: z.string(),
|
|
580
|
+
type: z.string().optional()
|
|
581
|
+
});
|
|
582
|
+
var DOMAIN_COLORS = {
|
|
583
|
+
"Quality Control": "#1a2744",
|
|
584
|
+
"Supply Chain": "#1e3a6e",
|
|
585
|
+
"Marketing": "#6a7fb5",
|
|
586
|
+
"Finance": "#3a8a8a",
|
|
587
|
+
"HR": "#2a5a9a",
|
|
588
|
+
"Logistics": "#0e7490",
|
|
589
|
+
"Sales": "#1d4ed8",
|
|
590
|
+
"Engineering": "#4338ca",
|
|
591
|
+
"Operations": "#0891b2",
|
|
592
|
+
"Data Layer": "#1e3352",
|
|
593
|
+
"Web / API": "#1a3a1a",
|
|
594
|
+
"Messaging": "#2a1a3a",
|
|
595
|
+
"Infrastructure": "#0f2a40",
|
|
596
|
+
"Other": "#374151"
|
|
597
|
+
};
|
|
598
|
+
var DOMAIN_PALETTE = [
|
|
599
|
+
"#1a2e5a",
|
|
600
|
+
"#1e3a8a",
|
|
601
|
+
"#1d4ed8",
|
|
602
|
+
"#2563eb",
|
|
603
|
+
"#3b82f6",
|
|
604
|
+
"#6366f1",
|
|
605
|
+
"#818cf8",
|
|
606
|
+
"#7c9fc3",
|
|
607
|
+
"#0e7490",
|
|
608
|
+
"#0891b2",
|
|
609
|
+
"#06b6d4",
|
|
610
|
+
"#22d3ee",
|
|
611
|
+
"#0d9488",
|
|
612
|
+
"#14b8a6",
|
|
613
|
+
"#2dd4bf",
|
|
614
|
+
"#5eead4"
|
|
615
|
+
];
|
|
489
616
|
function defaultConfig(overrides = {}) {
|
|
490
617
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
491
618
|
return {
|
|
@@ -770,7 +897,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
770
897
|
discoveredVia: z2.string(),
|
|
771
898
|
confidence: z2.number().min(0).max(1),
|
|
772
899
|
metadata: z2.record(z2.unknown()).optional(),
|
|
773
|
-
tags: z2.array(z2.string()).optional()
|
|
900
|
+
tags: z2.array(z2.string()).optional(),
|
|
901
|
+
domain: z2.string().optional().describe('Business domain, e.g. "Marketing", "Finance"'),
|
|
902
|
+
subDomain: z2.string().optional().describe('Sub-domain, e.g. "Forecast client orders"'),
|
|
903
|
+
qualityScore: z2.number().min(0).max(100).optional().describe("Data quality score 0\u2013100")
|
|
774
904
|
}, async (args) => {
|
|
775
905
|
const node = {
|
|
776
906
|
id: stripSensitive(args["id"]),
|
|
@@ -779,7 +909,10 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
779
909
|
discoveredVia: args["discoveredVia"],
|
|
780
910
|
confidence: args["confidence"],
|
|
781
911
|
metadata: args["metadata"] ?? {},
|
|
782
|
-
tags: args["tags"] ?? []
|
|
912
|
+
tags: args["tags"] ?? [],
|
|
913
|
+
domain: args["domain"],
|
|
914
|
+
subDomain: args["subDomain"],
|
|
915
|
+
qualityScore: args["qualityScore"]
|
|
783
916
|
};
|
|
784
917
|
db.upsertNode(sessionId, node);
|
|
785
918
|
return { content: [{ type: "text", text: `\u2713 Node: ${node.id}` }] };
|
|
@@ -1411,6 +1544,274 @@ Use ask_user when you need context from the user.`;
|
|
|
1411
1544
|
// src/exporter.ts
|
|
1412
1545
|
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
1413
1546
|
import { join as join2 } from "path";
|
|
1547
|
+
|
|
1548
|
+
// src/hex.ts
|
|
1549
|
+
function hexToPixel(q, r, size) {
|
|
1550
|
+
const x = size * (3 / 2 * q);
|
|
1551
|
+
const y = size * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r);
|
|
1552
|
+
return { x, y };
|
|
1553
|
+
}
|
|
1554
|
+
function pixelToHex(x, y, size) {
|
|
1555
|
+
const q = 2 / 3 * x / size;
|
|
1556
|
+
const r = (-1 / 3 * x + Math.sqrt(3) / 3 * y) / size;
|
|
1557
|
+
return hexRound(q, r);
|
|
1558
|
+
}
|
|
1559
|
+
function hexRound(q, r) {
|
|
1560
|
+
const s = -q - r;
|
|
1561
|
+
let rq = Math.round(q);
|
|
1562
|
+
let rr = Math.round(r);
|
|
1563
|
+
const rs = Math.round(s);
|
|
1564
|
+
const dq = Math.abs(rq - q);
|
|
1565
|
+
const dr = Math.abs(rr - r);
|
|
1566
|
+
const ds = Math.abs(rs - s);
|
|
1567
|
+
if (dq > dr && dq > ds) {
|
|
1568
|
+
rq = -rr - rs;
|
|
1569
|
+
} else if (dr > ds) {
|
|
1570
|
+
rr = -rq - rs;
|
|
1571
|
+
}
|
|
1572
|
+
return { q: rq || 0, r: rr || 0 };
|
|
1573
|
+
}
|
|
1574
|
+
function hexCorners(cx, cy, size) {
|
|
1575
|
+
const corners = [];
|
|
1576
|
+
for (let i = 0; i < 6; i++) {
|
|
1577
|
+
const angle = Math.PI / 180 * (60 * i);
|
|
1578
|
+
corners.push({
|
|
1579
|
+
x: cx + size * Math.cos(angle),
|
|
1580
|
+
y: cy + size * Math.sin(angle)
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return corners;
|
|
1584
|
+
}
|
|
1585
|
+
var HEX_DIRECTIONS = [
|
|
1586
|
+
{ q: 1, r: 0 },
|
|
1587
|
+
{ q: 1, r: -1 },
|
|
1588
|
+
{ q: 0, r: -1 },
|
|
1589
|
+
{ q: -1, r: 0 },
|
|
1590
|
+
{ q: -1, r: 1 },
|
|
1591
|
+
{ q: 0, r: 1 }
|
|
1592
|
+
];
|
|
1593
|
+
function hexNeighbors(q, r) {
|
|
1594
|
+
return HEX_DIRECTIONS.map((d) => ({ q: q + d.q, r: r + d.r }));
|
|
1595
|
+
}
|
|
1596
|
+
function hexDistance(a, b) {
|
|
1597
|
+
return (Math.abs(a.q - b.q) + Math.abs(a.q + a.r - b.q - b.r) + Math.abs(a.r - b.r)) / 2;
|
|
1598
|
+
}
|
|
1599
|
+
function hexRing(center, radius) {
|
|
1600
|
+
if (radius === 0) return [{ q: center.q, r: center.r }];
|
|
1601
|
+
const results = [];
|
|
1602
|
+
let hex = {
|
|
1603
|
+
q: center.q + HEX_DIRECTIONS[4].q * radius,
|
|
1604
|
+
r: center.r + HEX_DIRECTIONS[4].r * radius
|
|
1605
|
+
};
|
|
1606
|
+
for (let side = 0; side < 6; side++) {
|
|
1607
|
+
for (let step = 0; step < radius; step++) {
|
|
1608
|
+
results.push({ q: hex.q, r: hex.r });
|
|
1609
|
+
hex = {
|
|
1610
|
+
q: hex.q + HEX_DIRECTIONS[side].q,
|
|
1611
|
+
r: hex.r + HEX_DIRECTIONS[side].r
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return results;
|
|
1616
|
+
}
|
|
1617
|
+
function hexSpiral(center, count) {
|
|
1618
|
+
const positions = [];
|
|
1619
|
+
let ring = 0;
|
|
1620
|
+
while (positions.length < count) {
|
|
1621
|
+
const ringPositions = hexRing(center, ring);
|
|
1622
|
+
for (const pos of ringPositions) {
|
|
1623
|
+
if (positions.length >= count) break;
|
|
1624
|
+
positions.push(pos);
|
|
1625
|
+
}
|
|
1626
|
+
ring++;
|
|
1627
|
+
}
|
|
1628
|
+
return positions;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/cluster.ts
|
|
1632
|
+
function assignColor(domain, allDomains) {
|
|
1633
|
+
if (DOMAIN_COLORS[domain]) return DOMAIN_COLORS[domain];
|
|
1634
|
+
const idx = allDomains.indexOf(domain);
|
|
1635
|
+
return DOMAIN_PALETTE[idx % DOMAIN_PALETTE.length];
|
|
1636
|
+
}
|
|
1637
|
+
function assignColors(domains) {
|
|
1638
|
+
const result = {};
|
|
1639
|
+
for (const d of domains) {
|
|
1640
|
+
result[d] = assignColor(d, domains);
|
|
1641
|
+
}
|
|
1642
|
+
return result;
|
|
1643
|
+
}
|
|
1644
|
+
function shadeVariant(hex, amount) {
|
|
1645
|
+
const num = parseInt(hex.replace("#", ""), 16);
|
|
1646
|
+
const r = Math.min(255, (num >> 16) + amount);
|
|
1647
|
+
const g = Math.min(255, (num >> 8 & 255) + amount);
|
|
1648
|
+
const b = Math.min(255, (num & 255) + amount);
|
|
1649
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
1650
|
+
}
|
|
1651
|
+
function groupByDomain(assets) {
|
|
1652
|
+
const map = /* @__PURE__ */ new Map();
|
|
1653
|
+
for (const a of assets) {
|
|
1654
|
+
const d = a.domain || "Other";
|
|
1655
|
+
if (!map.has(d)) map.set(d, []);
|
|
1656
|
+
map.get(d).push(a);
|
|
1657
|
+
}
|
|
1658
|
+
return map;
|
|
1659
|
+
}
|
|
1660
|
+
var CLUSTER_GAP = 3;
|
|
1661
|
+
function layoutClusters(groups, hexSize) {
|
|
1662
|
+
const allDomains = Array.from(groups.keys());
|
|
1663
|
+
const colors = assignColors(allDomains);
|
|
1664
|
+
const sorted = Array.from(groups.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
1665
|
+
const occupied = /* @__PURE__ */ new Set();
|
|
1666
|
+
const key = (q, r) => `${q},${r}`;
|
|
1667
|
+
const clusters = [];
|
|
1668
|
+
const allAssets = [];
|
|
1669
|
+
for (const [domain, domainAssets] of sorted) {
|
|
1670
|
+
const origin = clusters.length === 0 ? { q: 0, r: 0 } : findFreeOrigin(occupied, domainAssets.length, CLUSTER_GAP);
|
|
1671
|
+
const positions = hexSpiral(origin, domainAssets.length);
|
|
1672
|
+
const assetIds = [];
|
|
1673
|
+
for (let i = 0; i < domainAssets.length; i++) {
|
|
1674
|
+
const asset = domainAssets[i];
|
|
1675
|
+
asset.position = positions[i];
|
|
1676
|
+
assetIds.push(asset.id);
|
|
1677
|
+
occupied.add(key(positions[i].q, positions[i].r));
|
|
1678
|
+
allAssets.push(asset);
|
|
1679
|
+
}
|
|
1680
|
+
const centroid = computeCentroid(positions, hexSize);
|
|
1681
|
+
clusters.push({
|
|
1682
|
+
id: `cluster:${domain}`,
|
|
1683
|
+
label: domain,
|
|
1684
|
+
domain,
|
|
1685
|
+
color: colors[domain],
|
|
1686
|
+
assetIds,
|
|
1687
|
+
centroid
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
return { clusters, assets: allAssets };
|
|
1691
|
+
}
|
|
1692
|
+
function findFreeOrigin(occupied, count, gap) {
|
|
1693
|
+
const key = (q, r) => `${q},${r}`;
|
|
1694
|
+
for (let searchRadius = 1; searchRadius < 100; searchRadius++) {
|
|
1695
|
+
const candidates = hexSpiral({ q: 0, r: 0 }, 1 + 6 * searchRadius * (searchRadius + 1) / 2);
|
|
1696
|
+
for (const candidate of candidates) {
|
|
1697
|
+
const testPositions = hexSpiral(candidate, count);
|
|
1698
|
+
let fits = true;
|
|
1699
|
+
for (const tp of testPositions) {
|
|
1700
|
+
if (occupied.has(key(tp.q, tp.r))) {
|
|
1701
|
+
fits = false;
|
|
1702
|
+
break;
|
|
1703
|
+
}
|
|
1704
|
+
for (const oKey of occupied) {
|
|
1705
|
+
const [oq, or] = oKey.split(",").map(Number);
|
|
1706
|
+
if (hexDistance(tp, { q: oq, r: or }) < gap) {
|
|
1707
|
+
fits = false;
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (!fits) break;
|
|
1712
|
+
}
|
|
1713
|
+
if (fits) return candidate;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return { q: occupied.size * 5, r: 0 };
|
|
1717
|
+
}
|
|
1718
|
+
function computeCentroid(positions, hexSize) {
|
|
1719
|
+
if (positions.length === 0) return { x: 0, y: 0 };
|
|
1720
|
+
let sx = 0, sy = 0;
|
|
1721
|
+
for (const { q, r } of positions) {
|
|
1722
|
+
const { x, y } = hexToPixel(q, r, hexSize);
|
|
1723
|
+
sx += x;
|
|
1724
|
+
sy += y;
|
|
1725
|
+
}
|
|
1726
|
+
return { x: sx / positions.length, y: sy / positions.length };
|
|
1727
|
+
}
|
|
1728
|
+
function computeClusterBounds(assets, hexSize) {
|
|
1729
|
+
if (assets.length === 0) return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
|
1730
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1731
|
+
for (const a of assets) {
|
|
1732
|
+
const { x, y } = hexToPixel(a.position.q, a.position.r, hexSize);
|
|
1733
|
+
if (x < minX) minX = x;
|
|
1734
|
+
if (y < minY) minY = y;
|
|
1735
|
+
if (x > maxX) maxX = x;
|
|
1736
|
+
if (y > maxY) maxY = y;
|
|
1737
|
+
}
|
|
1738
|
+
return { minX, minY, maxX, maxY };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/mapper.ts
|
|
1742
|
+
var TYPE_TO_DOMAIN = {
|
|
1743
|
+
database_server: "Data Layer",
|
|
1744
|
+
database: "Data Layer",
|
|
1745
|
+
table: "Data Layer",
|
|
1746
|
+
cache_server: "Data Layer",
|
|
1747
|
+
web_service: "Web / API",
|
|
1748
|
+
api_endpoint: "Web / API",
|
|
1749
|
+
message_broker: "Messaging",
|
|
1750
|
+
queue: "Messaging",
|
|
1751
|
+
topic: "Messaging",
|
|
1752
|
+
host: "Infrastructure",
|
|
1753
|
+
container: "Infrastructure",
|
|
1754
|
+
pod: "Infrastructure",
|
|
1755
|
+
k8s_cluster: "Infrastructure",
|
|
1756
|
+
config_file: "Infrastructure",
|
|
1757
|
+
saas_tool: "SaaS Tools"
|
|
1758
|
+
};
|
|
1759
|
+
function resolveDomain(node) {
|
|
1760
|
+
if (node.domain) return node.domain;
|
|
1761
|
+
const meta = node.metadata;
|
|
1762
|
+
if (typeof meta["category"] === "string" && meta["category"].length > 0) {
|
|
1763
|
+
return meta["category"];
|
|
1764
|
+
}
|
|
1765
|
+
for (const tag of node.tags ?? []) {
|
|
1766
|
+
if (tag.length > 2 && tag[0] === tag[0].toUpperCase()) {
|
|
1767
|
+
return tag;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
return TYPE_TO_DOMAIN[node.type] ?? "Other";
|
|
1771
|
+
}
|
|
1772
|
+
function nodesToAssets(nodes) {
|
|
1773
|
+
return nodes.map((n) => ({
|
|
1774
|
+
id: n.id,
|
|
1775
|
+
name: n.name,
|
|
1776
|
+
domain: resolveDomain(n),
|
|
1777
|
+
subDomain: n.subDomain,
|
|
1778
|
+
qualityScore: n.qualityScore ?? Math.round(n.confidence * 100),
|
|
1779
|
+
metadata: n.metadata ?? {},
|
|
1780
|
+
position: { q: 0, r: 0 }
|
|
1781
|
+
// will be assigned by layoutClusters
|
|
1782
|
+
}));
|
|
1783
|
+
}
|
|
1784
|
+
function edgesToConnections(edges) {
|
|
1785
|
+
return edges.map((e) => ({
|
|
1786
|
+
id: e.id,
|
|
1787
|
+
sourceAssetId: e.sourceId,
|
|
1788
|
+
targetAssetId: e.targetId,
|
|
1789
|
+
type: e.relationship
|
|
1790
|
+
}));
|
|
1791
|
+
}
|
|
1792
|
+
var HEX_SIZE = 24;
|
|
1793
|
+
function buildMapData(nodes, edges, options) {
|
|
1794
|
+
const rawAssets = nodesToAssets(nodes);
|
|
1795
|
+
const connections = edgesToConnections(edges);
|
|
1796
|
+
if (rawAssets.length === 0) {
|
|
1797
|
+
return {
|
|
1798
|
+
assets: [],
|
|
1799
|
+
clusters: [],
|
|
1800
|
+
connections,
|
|
1801
|
+
meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
const groups = groupByDomain(rawAssets);
|
|
1805
|
+
const { clusters, assets } = layoutClusters(groups, HEX_SIZE);
|
|
1806
|
+
return {
|
|
1807
|
+
assets,
|
|
1808
|
+
clusters,
|
|
1809
|
+
connections,
|
|
1810
|
+
meta: { exportedAt: (/* @__PURE__ */ new Date()).toISOString(), theme: options?.theme ?? "light" }
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// src/exporter.ts
|
|
1414
1815
|
function nodeLayer(type) {
|
|
1415
1816
|
if (type === "saas_tool") return "saas";
|
|
1416
1817
|
if (["web_service", "api_endpoint"].includes(type)) return "web";
|
|
@@ -2345,7 +2746,647 @@ function toggle(i) {
|
|
|
2345
2746
|
</body>
|
|
2346
2747
|
</html>`;
|
|
2347
2748
|
}
|
|
2348
|
-
function
|
|
2749
|
+
function exportCartographyMap(nodes, edges, options) {
|
|
2750
|
+
const mapData = buildMapData(nodes, edges, options);
|
|
2751
|
+
const { assets, clusters, connections, meta } = mapData;
|
|
2752
|
+
const isEmpty = assets.length === 0;
|
|
2753
|
+
const HEX_SIZE2 = 24;
|
|
2754
|
+
const dataJson = JSON.stringify({
|
|
2755
|
+
assets: assets.map((a) => ({
|
|
2756
|
+
id: a.id,
|
|
2757
|
+
name: a.name,
|
|
2758
|
+
domain: a.domain,
|
|
2759
|
+
subDomain: a.subDomain ?? null,
|
|
2760
|
+
qualityScore: a.qualityScore ?? null,
|
|
2761
|
+
metadata: a.metadata,
|
|
2762
|
+
q: a.position.q,
|
|
2763
|
+
r: a.position.r
|
|
2764
|
+
})),
|
|
2765
|
+
clusters: clusters.map((c) => ({
|
|
2766
|
+
id: c.id,
|
|
2767
|
+
label: c.label,
|
|
2768
|
+
domain: c.domain,
|
|
2769
|
+
color: c.color,
|
|
2770
|
+
assetIds: c.assetIds,
|
|
2771
|
+
centroid: c.centroid
|
|
2772
|
+
})),
|
|
2773
|
+
connections: connections.map((c) => ({
|
|
2774
|
+
id: c.id,
|
|
2775
|
+
sourceAssetId: c.sourceAssetId,
|
|
2776
|
+
targetAssetId: c.targetAssetId,
|
|
2777
|
+
type: c.type ?? "connection"
|
|
2778
|
+
}))
|
|
2779
|
+
});
|
|
2780
|
+
return `<!DOCTYPE html>
|
|
2781
|
+
<html lang="en">
|
|
2782
|
+
<head>
|
|
2783
|
+
<meta charset="UTF-8"/>
|
|
2784
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
2785
|
+
<title>Data Cartography Map</title>
|
|
2786
|
+
<style>
|
|
2787
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2788
|
+
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
2789
|
+
body{display:flex;flex-direction:column;background:${meta.theme === "dark" ? "#0f172a" : "#f8fafc"};color:${meta.theme === "dark" ? "#e2e8f0" : "#1e293b"}}
|
|
2790
|
+
#topbar{
|
|
2791
|
+
height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2792
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};z-index:10;flex-shrink:0;
|
|
2793
|
+
}
|
|
2794
|
+
#topbar h1{font-size:15px;font-weight:600;letter-spacing:-0.01em}
|
|
2795
|
+
#search-box{
|
|
2796
|
+
display:flex;align-items:center;gap:8px;background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"};
|
|
2797
|
+
border-radius:8px;padding:5px 10px;margin-left:auto;
|
|
2798
|
+
}
|
|
2799
|
+
#search-box input{
|
|
2800
|
+
border:none;background:transparent;font-size:13px;outline:none;width:180px;color:inherit;
|
|
2801
|
+
}
|
|
2802
|
+
#search-box input::placeholder{color:#94a3b8}
|
|
2803
|
+
#main{flex:1;display:flex;overflow:hidden;position:relative}
|
|
2804
|
+
#canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2805
|
+
#canvas-wrap.dragging{cursor:grabbing}
|
|
2806
|
+
#canvas-wrap.connecting{cursor:crosshair}
|
|
2807
|
+
canvas{display:block;width:100%;height:100%}
|
|
2808
|
+
/* Detail panel */
|
|
2809
|
+
#detail-panel{
|
|
2810
|
+
width:280px;background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-left:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2811
|
+
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2812
|
+
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2813
|
+
}
|
|
2814
|
+
#detail-panel.open{transform:translateX(0)}
|
|
2815
|
+
#detail-panel .panel-header{
|
|
2816
|
+
padding:16px;border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};display:flex;align-items:center;gap:10px;
|
|
2817
|
+
}
|
|
2818
|
+
#detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2819
|
+
#detail-panel .close-btn{
|
|
2820
|
+
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2821
|
+
color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2822
|
+
}
|
|
2823
|
+
#detail-panel .close-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2824
|
+
#detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2825
|
+
#detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2826
|
+
#detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
|
|
2827
|
+
#detail-panel .meta-value{font-size:13px;word-break:break-all}
|
|
2828
|
+
#detail-panel .quality-bar{height:6px;border-radius:3px;background:${meta.theme === "dark" ? "#334155" : "#e2e8f0"};margin-top:4px}
|
|
2829
|
+
#detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2830
|
+
/* Bottom-left toolbar */
|
|
2831
|
+
#toolbar-left{
|
|
2832
|
+
position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
|
|
2833
|
+
}
|
|
2834
|
+
.tb-btn{
|
|
2835
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2836
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2837
|
+
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2838
|
+
transition:all .15s;color:inherit;
|
|
2839
|
+
}
|
|
2840
|
+
.tb-btn:hover{border-color:#94a3b8}
|
|
2841
|
+
.tb-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6}
|
|
2842
|
+
/* Bottom-right toolbar */
|
|
2843
|
+
#toolbar-right{
|
|
2844
|
+
position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
|
|
2845
|
+
align-items:flex-end;gap:8px;z-index:10;
|
|
2846
|
+
}
|
|
2847
|
+
#zoom-controls{display:flex;align-items:center;gap:6px}
|
|
2848
|
+
.zoom-btn{
|
|
2849
|
+
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2850
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2851
|
+
font-size:18px;color:inherit;display:flex;align-items:center;justify-content:center;
|
|
2852
|
+
}
|
|
2853
|
+
.zoom-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2854
|
+
#zoom-pct{font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center}
|
|
2855
|
+
#detail-selector{display:flex;flex-direction:column;gap:4px}
|
|
2856
|
+
.detail-btn{
|
|
2857
|
+
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2858
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2859
|
+
font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
|
|
2860
|
+
}
|
|
2861
|
+
.detail-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2862
|
+
.detail-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6;color:#2563eb}
|
|
2863
|
+
#connect-btn{
|
|
2864
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2865
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2866
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2867
|
+
}
|
|
2868
|
+
#connect-btn.active{background:#fef3c7;border-color:#f59e0b}
|
|
2869
|
+
/* Tooltip */
|
|
2870
|
+
#tooltip{
|
|
2871
|
+
position:fixed;background:#1e293b;color:#fff;border-radius:8px;
|
|
2872
|
+
padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
|
|
2873
|
+
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
|
2874
|
+
}
|
|
2875
|
+
#tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2876
|
+
#tooltip .tt-domain{color:#94a3b8;font-size:11px}
|
|
2877
|
+
#tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2878
|
+
/* Empty state */
|
|
2879
|
+
#empty-state{
|
|
2880
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2881
|
+
align-items:center;justify-content:center;gap:12px;color:#94a3b8;
|
|
2882
|
+
}
|
|
2883
|
+
#empty-state p{font-size:14px}
|
|
2884
|
+
/* Theme toggle */
|
|
2885
|
+
#theme-btn{
|
|
2886
|
+
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2887
|
+
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2888
|
+
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2889
|
+
}
|
|
2890
|
+
/* Dark mode overrides (toggled via JS) */
|
|
2891
|
+
body.dark{background:#0f172a;color:#e2e8f0}
|
|
2892
|
+
body.dark #topbar{background:#1e293b;border-color:#334155}
|
|
2893
|
+
body.dark #search-box{background:#334155}
|
|
2894
|
+
body.dark #detail-panel{background:#1e293b;border-color:#334155}
|
|
2895
|
+
body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
|
|
2896
|
+
background:#1e293b;border-color:#334155;color:#e2e8f0;
|
|
2897
|
+
}
|
|
2898
|
+
/* Light mode overrides */
|
|
2899
|
+
body.light{background:#f8fafc;color:#1e293b}
|
|
2900
|
+
body.light #topbar{background:#fff;border-color:#e2e8f0}
|
|
2901
|
+
/* Connection hint */
|
|
2902
|
+
#connect-hint{
|
|
2903
|
+
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2904
|
+
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2905
|
+
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2906
|
+
display:none;z-index:20;pointer-events:none;
|
|
2907
|
+
}
|
|
2908
|
+
/* Screen reader only */
|
|
2909
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
|
|
2910
|
+
</style>
|
|
2911
|
+
</head>
|
|
2912
|
+
<body class="${meta.theme}">
|
|
2913
|
+
<!-- Top bar -->
|
|
2914
|
+
<div id="topbar">
|
|
2915
|
+
<h1>Data Cartography Map</h1>
|
|
2916
|
+
<div id="search-box">
|
|
2917
|
+
<span style="color:#94a3b8;font-size:14px">⌕</span>
|
|
2918
|
+
<input id="search-input" type="text" placeholder="Search assets..." aria-label="Search data assets"/>
|
|
2919
|
+
</div>
|
|
2920
|
+
<button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme">${meta.theme === "dark" ? "☼" : "☾"}</button>
|
|
2921
|
+
</div>
|
|
2922
|
+
<!-- SR summary -->
|
|
2923
|
+
<div class="sr-only" role="status" aria-live="polite" id="sr-summary">
|
|
2924
|
+
Data cartography map with ${assets.length} assets in ${clusters.length} clusters.
|
|
2925
|
+
</div>
|
|
2926
|
+
<!-- Main area -->
|
|
2927
|
+
<div id="main">
|
|
2928
|
+
<div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
|
|
2929
|
+
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
2930
|
+
${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>' : ""}
|
|
2931
|
+
</div>
|
|
2932
|
+
<div id="detail-panel" role="complementary" aria-label="Asset details">
|
|
2933
|
+
<div class="panel-header">
|
|
2934
|
+
<h3 id="dp-name">—</h3>
|
|
2935
|
+
<button class="close-btn" id="dp-close" aria-label="Close panel">✕</button>
|
|
2936
|
+
</div>
|
|
2937
|
+
<div class="panel-body" id="dp-body"></div>
|
|
2938
|
+
</div>
|
|
2939
|
+
</div>
|
|
2940
|
+
<!-- Bottom-left toolbar -->
|
|
2941
|
+
<div id="toolbar-left">
|
|
2942
|
+
<button class="tb-btn active" id="btn-labels" title="Show labels" aria-pressed="true" aria-label="Toggle labels">🏷</button>
|
|
2943
|
+
<button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">👁</button>
|
|
2944
|
+
</div>
|
|
2945
|
+
<!-- Bottom-right toolbar -->
|
|
2946
|
+
<div id="toolbar-right">
|
|
2947
|
+
<div id="zoom-controls">
|
|
2948
|
+
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out">−</button>
|
|
2949
|
+
<span id="zoom-pct">100%</span>
|
|
2950
|
+
<button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
|
|
2951
|
+
</div>
|
|
2952
|
+
<div id="detail-selector">
|
|
2953
|
+
<button class="detail-btn" id="dl-1" aria-label="Detail level 1">1</button>
|
|
2954
|
+
<button class="detail-btn active" id="dl-2" aria-label="Detail level 2">2</button>
|
|
2955
|
+
<button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
|
|
2956
|
+
<button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
|
|
2957
|
+
</div>
|
|
2958
|
+
<button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">🔗</button>
|
|
2959
|
+
</div>
|
|
2960
|
+
<!-- Connection hint -->
|
|
2961
|
+
<div id="connect-hint">Click two assets to create a connection</div>
|
|
2962
|
+
<!-- Tooltip -->
|
|
2963
|
+
<div id="tooltip" role="tooltip">
|
|
2964
|
+
<div class="tt-name" id="tt-name"></div>
|
|
2965
|
+
<div class="tt-domain" id="tt-domain"></div>
|
|
2966
|
+
<div class="tt-quality" id="tt-quality"></div>
|
|
2967
|
+
</div>
|
|
2968
|
+
|
|
2969
|
+
<script>
|
|
2970
|
+
(function() {
|
|
2971
|
+
'use strict';
|
|
2972
|
+
|
|
2973
|
+
// \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
|
|
2974
|
+
const MAP = ${dataJson};
|
|
2975
|
+
const HEX_SIZE = ${HEX_SIZE2};
|
|
2976
|
+
const IS_EMPTY = ${isEmpty};
|
|
2977
|
+
|
|
2978
|
+
// Build asset index
|
|
2979
|
+
const assetIndex = new Map();
|
|
2980
|
+
const clusterByAsset = new Map();
|
|
2981
|
+
for (const c of MAP.clusters) {
|
|
2982
|
+
for (const aid of c.assetIds) {
|
|
2983
|
+
clusterByAsset.set(aid, c);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
for (const a of MAP.assets) {
|
|
2987
|
+
assetIndex.set(a.id, a);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// \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
|
|
2991
|
+
const canvas = document.getElementById('hexmap');
|
|
2992
|
+
const ctx = canvas.getContext('2d');
|
|
2993
|
+
const wrap = document.getElementById('canvas-wrap');
|
|
2994
|
+
let W = 0, H = 0;
|
|
2995
|
+
|
|
2996
|
+
function resize() {
|
|
2997
|
+
const dpr = window.devicePixelRatio || 1;
|
|
2998
|
+
W = wrap.clientWidth; H = wrap.clientHeight;
|
|
2999
|
+
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
3000
|
+
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
3001
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3002
|
+
draw();
|
|
3003
|
+
}
|
|
3004
|
+
window.addEventListener('resize', resize);
|
|
3005
|
+
|
|
3006
|
+
// \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
|
|
3007
|
+
let vx = 0, vy = 0, scale = 1;
|
|
3008
|
+
let detailLevel = 2, showLabels = true, showQuality = false;
|
|
3009
|
+
let isDark = document.body.classList.contains('dark');
|
|
3010
|
+
let connectMode = false, connectFirst = null;
|
|
3011
|
+
let hoveredAssetId = null, selectedAssetId = null;
|
|
3012
|
+
let searchQuery = '';
|
|
3013
|
+
let localConnections = [...MAP.connections];
|
|
3014
|
+
|
|
3015
|
+
// Flat-top hex math
|
|
3016
|
+
function htp_x(q, r) { return HEX_SIZE * (3/2 * q); }
|
|
3017
|
+
function htp_y(q, r) { return HEX_SIZE * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r); }
|
|
3018
|
+
function w2s(wx, wy) { return { x: wx*scale+vx, y: wy*scale+vy }; }
|
|
3019
|
+
function s2w(sx, sy) { return { x: (sx-vx)/scale, y: (sy-vy)/scale }; }
|
|
3020
|
+
|
|
3021
|
+
function fitToView() {
|
|
3022
|
+
if (IS_EMPTY || MAP.assets.length === 0) { vx = 0; vy = 0; scale = 1; return; }
|
|
3023
|
+
let mnx=Infinity,mny=Infinity,mxx=-Infinity,mxy=-Infinity;
|
|
3024
|
+
for (const a of MAP.assets) {
|
|
3025
|
+
const px=htp_x(a.q,a.r), py=htp_y(a.q,a.r);
|
|
3026
|
+
if(px<mnx)mnx=px;if(py<mny)mny=py;if(px>mxx)mxx=px;if(py>mxy)mxy=py;
|
|
3027
|
+
}
|
|
3028
|
+
const pw=mxx-mnx+HEX_SIZE*4, ph=mxy-mny+HEX_SIZE*4;
|
|
3029
|
+
scale = Math.min(W/pw, H/ph, 2) * 0.85;
|
|
3030
|
+
vx = W/2 - ((mnx+mxx)/2)*scale;
|
|
3031
|
+
vy = H/2 - ((mny+mxy)/2)*scale;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// \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
|
|
3035
|
+
function hexPath(cx, cy, r) {
|
|
3036
|
+
ctx.beginPath();
|
|
3037
|
+
for (let i=0;i<6;i++) {
|
|
3038
|
+
const angle = Math.PI/180*(60*i);
|
|
3039
|
+
const x=cx+r*Math.cos(angle), y=cy+r*Math.sin(angle);
|
|
3040
|
+
i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
|
|
3041
|
+
}
|
|
3042
|
+
ctx.closePath();
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
function shadeV(hex, amt) {
|
|
3046
|
+
if(!hex||hex.length<7)return hex;
|
|
3047
|
+
const n=parseInt(hex.replace('#',''),16);
|
|
3048
|
+
const r=Math.min(255,(n>>16)+amt), g=Math.min(255,((n>>8)&0xff)+amt), b=Math.min(255,(n&0xff)+amt);
|
|
3049
|
+
return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0');
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
function draw() {
|
|
3053
|
+
ctx.clearRect(0,0,W,H);
|
|
3054
|
+
ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
|
|
3055
|
+
ctx.fillRect(0,0,W,H);
|
|
3056
|
+
if (IS_EMPTY) return;
|
|
3057
|
+
|
|
3058
|
+
const size = HEX_SIZE * scale;
|
|
3059
|
+
const matchedIds = getSearchMatches();
|
|
3060
|
+
const hasSearch = searchQuery.length > 0;
|
|
3061
|
+
|
|
3062
|
+
// Draw connections
|
|
3063
|
+
ctx.save();
|
|
3064
|
+
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
3065
|
+
ctx.lineWidth = 1.5;
|
|
3066
|
+
ctx.setLineDash([4,4]);
|
|
3067
|
+
for (const conn of localConnections) {
|
|
3068
|
+
const src = assetIndex.get(conn.sourceAssetId);
|
|
3069
|
+
const tgt = assetIndex.get(conn.targetAssetId);
|
|
3070
|
+
if (!src||!tgt) continue;
|
|
3071
|
+
const sp=w2s(htp_x(src.q,src.r),htp_y(src.q,src.r));
|
|
3072
|
+
const tp=w2s(htp_x(tgt.q,tgt.r),htp_y(tgt.q,tgt.r));
|
|
3073
|
+
ctx.beginPath();ctx.moveTo(sp.x,sp.y);ctx.lineTo(tp.x,tp.y);ctx.stroke();
|
|
3074
|
+
}
|
|
3075
|
+
ctx.setLineDash([]);
|
|
3076
|
+
ctx.restore();
|
|
3077
|
+
|
|
3078
|
+
// Draw hexagons per cluster
|
|
3079
|
+
for (const cluster of MAP.clusters) {
|
|
3080
|
+
const baseColor = cluster.color;
|
|
3081
|
+
const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
|
|
3082
|
+
const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
|
|
3083
|
+
const clusterDim = hasSearch && !isClusterMatch;
|
|
3084
|
+
|
|
3085
|
+
for (let ai=0; ai<clusterAssets.length; ai++) {
|
|
3086
|
+
const asset = clusterAssets[ai];
|
|
3087
|
+
const wx=htp_x(asset.q,asset.r), wy=htp_y(asset.q,asset.r);
|
|
3088
|
+
const s=w2s(wx,wy);
|
|
3089
|
+
const cx=s.x, cy=s.y;
|
|
3090
|
+
|
|
3091
|
+
// Frustum cull
|
|
3092
|
+
if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
|
|
3093
|
+
|
|
3094
|
+
// Shade variation
|
|
3095
|
+
const shade = ai%3===0?18:ai%3===1?8:0;
|
|
3096
|
+
let fillColor = shadeV(baseColor, shade);
|
|
3097
|
+
|
|
3098
|
+
// Quality overlay
|
|
3099
|
+
if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
3100
|
+
const q = asset.qualityScore;
|
|
3101
|
+
if (q < 40) fillColor = '#ef4444';
|
|
3102
|
+
else if (q < 70) fillColor = '#f97316';
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
const alpha = clusterDim ? 0.18 : 1;
|
|
3106
|
+
const isHovered = asset.id === hoveredAssetId;
|
|
3107
|
+
const isSelected = asset.id === selectedAssetId;
|
|
3108
|
+
const isConnectFirst = asset.id === connectFirst;
|
|
3109
|
+
|
|
3110
|
+
ctx.save();
|
|
3111
|
+
ctx.globalAlpha = alpha;
|
|
3112
|
+
hexPath(cx, cy, size*0.92);
|
|
3113
|
+
|
|
3114
|
+
if (isDark && (isHovered||isSelected||isConnectFirst)) {
|
|
3115
|
+
ctx.shadowColor = fillColor;
|
|
3116
|
+
ctx.shadowBlur = isSelected ? 16 : 8;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
ctx.fillStyle = fillColor;
|
|
3120
|
+
ctx.fill();
|
|
3121
|
+
|
|
3122
|
+
if (isSelected||isConnectFirst) {
|
|
3123
|
+
ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
|
|
3124
|
+
ctx.lineWidth = 2.5;
|
|
3125
|
+
ctx.stroke();
|
|
3126
|
+
} else if (isHovered) {
|
|
3127
|
+
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
|
|
3128
|
+
ctx.lineWidth = 1.5;
|
|
3129
|
+
ctx.stroke();
|
|
3130
|
+
} else {
|
|
3131
|
+
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)';
|
|
3132
|
+
ctx.lineWidth = 1;
|
|
3133
|
+
ctx.stroke();
|
|
3134
|
+
}
|
|
3135
|
+
ctx.restore();
|
|
3136
|
+
|
|
3137
|
+
// Quality dot
|
|
3138
|
+
if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
|
|
3139
|
+
const q = asset.qualityScore;
|
|
3140
|
+
if (q < 70) {
|
|
3141
|
+
ctx.beginPath();
|
|
3142
|
+
ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,size*0.14), 0, Math.PI*2);
|
|
3143
|
+
ctx.fillStyle = q<40?'#ef4444':'#f97316';
|
|
3144
|
+
ctx.fill();
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// Asset labels (detail 4, or 3 at high zoom)
|
|
3149
|
+
const showAssetLabel = showLabels && !clusterDim &&
|
|
3150
|
+
((detailLevel>=4)||(detailLevel===3 && scale>=0.8));
|
|
3151
|
+
if (showAssetLabel && size>14) {
|
|
3152
|
+
const label = asset.name.length>12 ? asset.name.substring(0,11)+'...' : asset.name;
|
|
3153
|
+
ctx.save();
|
|
3154
|
+
ctx.font = Math.max(8,Math.min(11,size*0.38))+'px -apple-system,sans-serif';
|
|
3155
|
+
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
3156
|
+
ctx.textAlign='center';ctx.textBaseline='middle';
|
|
3157
|
+
ctx.fillText(label, cx, cy);
|
|
3158
|
+
ctx.restore();
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// Cluster labels (pill badges)
|
|
3164
|
+
if (showLabels && detailLevel>=1) {
|
|
3165
|
+
for (const cluster of MAP.clusters) {
|
|
3166
|
+
if (cluster.assetIds.length===0) continue;
|
|
3167
|
+
if (hasSearch && !cluster.assetIds.some(id=>matchedIds.has(id))) continue;
|
|
3168
|
+
const s=w2s(cluster.centroid.x, cluster.centroid.y);
|
|
3169
|
+
drawPill(s.x, s.y-size*1.2, cluster.label, cluster.color, 14);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// Sub-domain labels (detail 2+)
|
|
3174
|
+
if (showLabels && detailLevel>=2) {
|
|
3175
|
+
const subGroups = new Map();
|
|
3176
|
+
for (const a of MAP.assets) {
|
|
3177
|
+
if (!a.subDomain) continue;
|
|
3178
|
+
const key = a.domain+'|'+a.subDomain;
|
|
3179
|
+
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
3180
|
+
subGroups.get(key).push(a);
|
|
3181
|
+
}
|
|
3182
|
+
for (const [, group] of subGroups) {
|
|
3183
|
+
let sx=0,sy=0;
|
|
3184
|
+
for (const a of group) { sx+=htp_x(a.q,a.r); sy+=htp_y(a.q,a.r); }
|
|
3185
|
+
const cx=sx/group.length, cy=sy/group.length;
|
|
3186
|
+
const s = w2s(cx, cy);
|
|
3187
|
+
drawPill(s.x, s.y+size*1.5, group[0].subDomain, '#64748b', 11);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
function drawPill(x, y, text, color, fontSize) {
|
|
3193
|
+
if(!text) return;
|
|
3194
|
+
ctx.save();
|
|
3195
|
+
ctx.font = '600 '+fontSize+'px -apple-system,sans-serif';
|
|
3196
|
+
const tw=ctx.measureText(text).width;
|
|
3197
|
+
const ph=fontSize+8, pw=tw+20;
|
|
3198
|
+
ctx.beginPath();
|
|
3199
|
+
if (ctx.roundRect) ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
|
|
3200
|
+
else { ctx.rect(x-pw/2, y-ph/2, pw, ph); }
|
|
3201
|
+
ctx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
3202
|
+
ctx.shadowColor='rgba(0,0,0,0.15)'; ctx.shadowBlur=6;
|
|
3203
|
+
ctx.fill(); ctx.shadowBlur=0;
|
|
3204
|
+
ctx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
3205
|
+
ctx.textAlign='center'; ctx.textBaseline='middle';
|
|
3206
|
+
ctx.fillText(text, x, y);
|
|
3207
|
+
ctx.restore();
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// \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
|
|
3211
|
+
function getAssetAt(sx, sy) {
|
|
3212
|
+
const w=s2w(sx,sy);
|
|
3213
|
+
for (const a of MAP.assets) {
|
|
3214
|
+
const wx=htp_x(a.q,a.r), wy=htp_y(a.q,a.r);
|
|
3215
|
+
const dx=Math.abs(w.x-wx), dy=Math.abs(w.y-wy);
|
|
3216
|
+
if (dx>HEX_SIZE||dy>HEX_SIZE) continue;
|
|
3217
|
+
if (dx*dx+dy*dy < HEX_SIZE*HEX_SIZE) return a;
|
|
3218
|
+
}
|
|
3219
|
+
return null;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
// \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
|
|
3223
|
+
function getSearchMatches() {
|
|
3224
|
+
if(!searchQuery) return new Set();
|
|
3225
|
+
const q=searchQuery.toLowerCase();
|
|
3226
|
+
const m=new Set();
|
|
3227
|
+
for(const a of MAP.assets){
|
|
3228
|
+
if(a.name.toLowerCase().includes(q)||(a.domain&&a.domain.toLowerCase().includes(q))||
|
|
3229
|
+
(a.subDomain&&a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
3230
|
+
}
|
|
3231
|
+
return m;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
// \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
|
|
3235
|
+
let dragging=false, lastMX=0, lastMY=0;
|
|
3236
|
+
|
|
3237
|
+
wrap.addEventListener('mousedown', e=>{
|
|
3238
|
+
if(e.button!==0)return;
|
|
3239
|
+
dragging=true; lastMX=e.clientX; lastMY=e.clientY;
|
|
3240
|
+
wrap.classList.add('dragging');
|
|
3241
|
+
});
|
|
3242
|
+
window.addEventListener('mouseup', ()=>{dragging=false;wrap.classList.remove('dragging');});
|
|
3243
|
+
window.addEventListener('mousemove', e=>{
|
|
3244
|
+
if(dragging){
|
|
3245
|
+
vx+=e.clientX-lastMX; vy+=e.clientY-lastMY;
|
|
3246
|
+
lastMX=e.clientX; lastMY=e.clientY; draw(); return;
|
|
3247
|
+
}
|
|
3248
|
+
const rect=wrap.getBoundingClientRect();
|
|
3249
|
+
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
3250
|
+
const asset=getAssetAt(sx,sy);
|
|
3251
|
+
const newId=asset?asset.id:null;
|
|
3252
|
+
if(newId!==hoveredAssetId){hoveredAssetId=newId;draw();}
|
|
3253
|
+
const tt=document.getElementById('tooltip');
|
|
3254
|
+
if(asset){
|
|
3255
|
+
document.getElementById('tt-name').textContent=asset.name;
|
|
3256
|
+
document.getElementById('tt-domain').textContent=asset.domain+(asset.subDomain?' > '+asset.subDomain:'');
|
|
3257
|
+
document.getElementById('tt-quality').textContent=asset.qualityScore!==null?'Quality: '+asset.qualityScore+'/100':'';
|
|
3258
|
+
tt.style.display='block';tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY-8)+'px';
|
|
3259
|
+
} else { tt.style.display='none'; }
|
|
3260
|
+
});
|
|
3261
|
+
|
|
3262
|
+
wrap.addEventListener('click', e=>{
|
|
3263
|
+
const rect=wrap.getBoundingClientRect();
|
|
3264
|
+
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
3265
|
+
const asset=getAssetAt(sx,sy);
|
|
3266
|
+
if(connectMode){
|
|
3267
|
+
if(!asset) return;
|
|
3268
|
+
if(!connectFirst){connectFirst=asset.id;draw();}
|
|
3269
|
+
else if(connectFirst!==asset.id){
|
|
3270
|
+
localConnections.push({id:crypto.randomUUID(),sourceAssetId:connectFirst,targetAssetId:asset.id,type:'connection'});
|
|
3271
|
+
connectFirst=null;draw();
|
|
3272
|
+
}
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
if(asset){selectedAssetId=asset.id;showDetailPanel(asset);}
|
|
3276
|
+
else{selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');}
|
|
3277
|
+
draw();
|
|
3278
|
+
});
|
|
3279
|
+
|
|
3280
|
+
// Touch
|
|
3281
|
+
let lastTouches=[];
|
|
3282
|
+
wrap.addEventListener('touchstart',e=>{lastTouches=[...e.touches];},{passive:true});
|
|
3283
|
+
wrap.addEventListener('touchmove',e=>{
|
|
3284
|
+
if(e.touches.length===1){
|
|
3285
|
+
vx+=e.touches[0].clientX-lastTouches[0].clientX;
|
|
3286
|
+
vy+=e.touches[0].clientY-lastTouches[0].clientY;draw();
|
|
3287
|
+
} else if(e.touches.length===2){
|
|
3288
|
+
const d0=Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX,lastTouches[0].clientY-lastTouches[1].clientY);
|
|
3289
|
+
const d1=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
|
|
3290
|
+
const mx=(e.touches[0].clientX+e.touches[1].clientX)/2;
|
|
3291
|
+
const my=(e.touches[0].clientY+e.touches[1].clientY)/2;
|
|
3292
|
+
applyZoom(d1/d0,mx,my);
|
|
3293
|
+
}
|
|
3294
|
+
lastTouches=[...e.touches];
|
|
3295
|
+
},{passive:true});
|
|
3296
|
+
|
|
3297
|
+
wrap.addEventListener('wheel',e=>{
|
|
3298
|
+
e.preventDefault();
|
|
3299
|
+
const rect=wrap.getBoundingClientRect();
|
|
3300
|
+
applyZoom(e.deltaY<0?1.12:1/1.12,e.clientX-rect.left,e.clientY-rect.top);
|
|
3301
|
+
},{passive:false});
|
|
3302
|
+
|
|
3303
|
+
function applyZoom(factor,sx,sy){
|
|
3304
|
+
const ns=Math.max(0.05,Math.min(8,scale*factor));
|
|
3305
|
+
const wx=(sx-vx)/scale,wy=(sy-vy)/scale;
|
|
3306
|
+
scale=ns;vx=sx-wx*scale;vy=sy-wy*scale;
|
|
3307
|
+
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';draw();
|
|
3308
|
+
}
|
|
3309
|
+
document.getElementById('zoom-in').addEventListener('click',()=>applyZoom(1.25,W/2,H/2));
|
|
3310
|
+
document.getElementById('zoom-out').addEventListener('click',()=>applyZoom(1/1.25,W/2,H/2));
|
|
3311
|
+
|
|
3312
|
+
// Keyboard
|
|
3313
|
+
wrap.addEventListener('keydown',e=>{
|
|
3314
|
+
const step=40;
|
|
3315
|
+
if(e.key==='ArrowLeft'){vx+=step;draw();}
|
|
3316
|
+
else if(e.key==='ArrowRight'){vx-=step;draw();}
|
|
3317
|
+
else if(e.key==='ArrowUp'){vy+=step;draw();}
|
|
3318
|
+
else if(e.key==='ArrowDown'){vy-=step;draw();}
|
|
3319
|
+
else if(e.key==='+'||e.key==='=')applyZoom(1.2,W/2,H/2);
|
|
3320
|
+
else if(e.key==='-')applyZoom(1/1.2,W/2,H/2);
|
|
3321
|
+
else if(e.key==='Escape'){
|
|
3322
|
+
selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');
|
|
3323
|
+
if(connectMode)toggleConnect();draw();
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
|
|
3327
|
+
// \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
|
|
3328
|
+
function showDetailPanel(asset) {
|
|
3329
|
+
document.getElementById('dp-name').textContent=asset.name;
|
|
3330
|
+
const body=document.getElementById('dp-body');
|
|
3331
|
+
const rows=[['Domain',asset.domain],['Sub-domain',asset.subDomain],
|
|
3332
|
+
['Quality Score',asset.qualityScore!==null?renderQuality(asset.qualityScore):null],
|
|
3333
|
+
...Object.entries(asset.metadata||{}).slice(0,8).map(([k,v])=>[k,String(v)])
|
|
3334
|
+
].filter(([,v])=>v!==null&&v!==undefined&&v!=='');
|
|
3335
|
+
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('');
|
|
3336
|
+
const related=localConnections.filter(c=>c.sourceAssetId===asset.id||c.targetAssetId===asset.id);
|
|
3337
|
+
if(related.length>0){
|
|
3338
|
+
body.innerHTML+='<div class="meta-row"><div class="meta-label">Connections ('+related.length+')</div><div>'+
|
|
3339
|
+
related.map(c=>{const oid=c.sourceAssetId===asset.id?c.targetAssetId:c.sourceAssetId;
|
|
3340
|
+
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>';
|
|
3341
|
+
}
|
|
3342
|
+
document.getElementById('detail-panel').classList.add('open');
|
|
3343
|
+
}
|
|
3344
|
+
function renderQuality(s){
|
|
3345
|
+
const c=s>=70?'#22c55e':s>=40?'#f97316':'#ef4444';
|
|
3346
|
+
return s+'/100 <div class="quality-bar"><div class="quality-fill" style="width:'+s+'%;background:'+c+'"></div></div>';
|
|
3347
|
+
}
|
|
3348
|
+
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
3349
|
+
document.getElementById('dp-close').addEventListener('click',()=>{
|
|
3350
|
+
document.getElementById('detail-panel').classList.remove('open');selectedAssetId=null;draw();
|
|
3351
|
+
});
|
|
3352
|
+
|
|
3353
|
+
// \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
|
|
3354
|
+
[1,2,3,4].forEach(n=>{
|
|
3355
|
+
document.getElementById('dl-'+n).addEventListener('click',()=>{
|
|
3356
|
+
detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
|
|
3357
|
+
document.getElementById('dl-'+n).classList.add('active');draw();
|
|
3358
|
+
});
|
|
3359
|
+
});
|
|
3360
|
+
document.getElementById('btn-labels').addEventListener('click',()=>{
|
|
3361
|
+
showLabels=!showLabels;document.getElementById('btn-labels').classList.toggle('active',showLabels);draw();
|
|
3362
|
+
});
|
|
3363
|
+
document.getElementById('btn-quality').addEventListener('click',()=>{
|
|
3364
|
+
showQuality=!showQuality;document.getElementById('btn-quality').classList.toggle('active',showQuality);draw();
|
|
3365
|
+
});
|
|
3366
|
+
function toggleConnect(){
|
|
3367
|
+
connectMode=!connectMode;connectFirst=null;
|
|
3368
|
+
document.getElementById('connect-btn').classList.toggle('active',connectMode);
|
|
3369
|
+
wrap.classList.toggle('connecting',connectMode);
|
|
3370
|
+
document.getElementById('connect-hint').style.display=connectMode?'block':'none';draw();
|
|
3371
|
+
}
|
|
3372
|
+
document.getElementById('connect-btn').addEventListener('click',toggleConnect);
|
|
3373
|
+
document.getElementById('theme-btn').addEventListener('click',()=>{
|
|
3374
|
+
isDark=!isDark;
|
|
3375
|
+
document.body.classList.toggle('dark',isDark);document.body.classList.toggle('light',!isDark);
|
|
3376
|
+
document.getElementById('theme-btn').innerHTML=isDark?'☼':'☾';draw();
|
|
3377
|
+
});
|
|
3378
|
+
document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
|
|
3379
|
+
|
|
3380
|
+
// \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
|
|
3381
|
+
resize(); fitToView();
|
|
3382
|
+
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';
|
|
3383
|
+
draw();
|
|
3384
|
+
})();
|
|
3385
|
+
</script>
|
|
3386
|
+
</body>
|
|
3387
|
+
</html>`;
|
|
3388
|
+
}
|
|
3389
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "sops"]) {
|
|
2349
3390
|
mkdirSync2(outputDir, { recursive: true });
|
|
2350
3391
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
2351
3392
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
@@ -2368,6 +3409,10 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
2368
3409
|
writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
|
|
2369
3410
|
process.stderr.write("\u2713 topology.html\n");
|
|
2370
3411
|
}
|
|
3412
|
+
if (formats.includes("map")) {
|
|
3413
|
+
writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
|
|
3414
|
+
process.stderr.write("\u2713 cartography-map.html\n");
|
|
3415
|
+
}
|
|
2371
3416
|
if (formats.includes("sops")) {
|
|
2372
3417
|
const sops = db.getSOPs(sessionId);
|
|
2373
3418
|
for (const sop of sops) {
|
|
@@ -2421,12 +3466,18 @@ function checkPrerequisites() {
|
|
|
2421
3466
|
}
|
|
2422
3467
|
export {
|
|
2423
3468
|
CartographyDB,
|
|
3469
|
+
assignColors,
|
|
3470
|
+
buildMapData,
|
|
2424
3471
|
checkPrerequisites,
|
|
3472
|
+
computeCentroid,
|
|
3473
|
+
computeClusterBounds,
|
|
2425
3474
|
createCartographyTools,
|
|
2426
3475
|
CartographyDB as default,
|
|
2427
3476
|
defaultConfig,
|
|
3477
|
+
edgesToConnections,
|
|
2428
3478
|
exportAll,
|
|
2429
3479
|
exportBackstageYAML,
|
|
3480
|
+
exportCartographyMap,
|
|
2430
3481
|
exportHTML,
|
|
2431
3482
|
exportJSON,
|
|
2432
3483
|
exportSOPDashboard,
|
|
@@ -2434,8 +3485,19 @@ export {
|
|
|
2434
3485
|
generateDependencyMermaid,
|
|
2435
3486
|
generateTopologyMermaid,
|
|
2436
3487
|
generateWorkflowMermaid,
|
|
3488
|
+
groupByDomain,
|
|
3489
|
+
hexCorners,
|
|
3490
|
+
hexDistance,
|
|
3491
|
+
hexNeighbors,
|
|
3492
|
+
hexRing,
|
|
3493
|
+
hexSpiral,
|
|
3494
|
+
hexToPixel,
|
|
3495
|
+
layoutClusters,
|
|
3496
|
+
nodesToAssets,
|
|
3497
|
+
pixelToHex,
|
|
2437
3498
|
runDiscovery,
|
|
2438
3499
|
safetyHook,
|
|
3500
|
+
shadeVariant,
|
|
2439
3501
|
stripSensitive
|
|
2440
3502
|
};
|
|
2441
3503
|
//# sourceMappingURL=index.js.map
|