@datasynx/agentic-ai-cartography 0.6.0 → 0.7.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/dist/cli.js +65 -1247
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.js +45 -1174
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -675,14 +675,14 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
675
675
|
tool("scan_local_databases", "Scan for local database files and running DB servers \u2014 PostgreSQL databases, MySQL, SQLite files from installed apps", {
|
|
676
676
|
deep: z.boolean().default(false).optional().describe("Also search home directory recursively for SQLite/DB files (slower)")
|
|
677
677
|
}, async (args) => {
|
|
678
|
-
const { execSync:
|
|
678
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
679
679
|
const { homedir } = await import("os");
|
|
680
680
|
const { existsSync: existsSync3 } = await import("fs");
|
|
681
681
|
const deep = args["deep"] ?? false;
|
|
682
682
|
const HOME = homedir();
|
|
683
683
|
const run = (cmd) => {
|
|
684
684
|
try {
|
|
685
|
-
return
|
|
685
|
+
return execSync2(cmd, { stdio: "pipe", timeout: 1e4, shell: "/bin/sh" }).toString().trim();
|
|
686
686
|
} catch {
|
|
687
687
|
return "";
|
|
688
688
|
}
|
|
@@ -709,12 +709,12 @@ ${v}`).join("\n\n");
|
|
|
709
709
|
tool("scan_k8s_resources", "Scan Kubernetes cluster via kubectl \u2014 100% readonly (get, describe)", {
|
|
710
710
|
namespace: z.string().optional().describe("Filter by namespace \u2014 empty = all namespaces")
|
|
711
711
|
}, async (args) => {
|
|
712
|
-
const { execSync:
|
|
712
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
713
713
|
const ns = args["namespace"];
|
|
714
714
|
const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
|
|
715
715
|
const run = (cmd) => {
|
|
716
716
|
try {
|
|
717
|
-
return
|
|
717
|
+
return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
|
|
718
718
|
} catch (e) {
|
|
719
719
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
720
720
|
}
|
|
@@ -738,7 +738,7 @@ ${run(c)}`).join("\n\n");
|
|
|
738
738
|
region: z.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION or profile"),
|
|
739
739
|
profile: z.string().optional().describe("AWS CLI profile")
|
|
740
740
|
}, async (args) => {
|
|
741
|
-
const { execSync:
|
|
741
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
742
742
|
const region = args["region"];
|
|
743
743
|
const profile = args["profile"];
|
|
744
744
|
const env = { ...process.env };
|
|
@@ -746,7 +746,7 @@ ${run(c)}`).join("\n\n");
|
|
|
746
746
|
const pf = profile ? `--profile ${profile}` : "";
|
|
747
747
|
const run = (cmd) => {
|
|
748
748
|
try {
|
|
749
|
-
return
|
|
749
|
+
return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
|
|
750
750
|
} catch (e) {
|
|
751
751
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
752
752
|
}
|
|
@@ -768,12 +768,12 @@ ${run(c)}`).join("\n\n");
|
|
|
768
768
|
tool("scan_gcp_resources", "Scan Google Cloud Platform via gcloud CLI \u2014 100% readonly (list, describe)", {
|
|
769
769
|
project: z.string().optional().describe("GCP Project ID \u2014 default: current gcloud project")
|
|
770
770
|
}, async (args) => {
|
|
771
|
-
const { execSync:
|
|
771
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
772
772
|
const project = args["project"];
|
|
773
773
|
const pf = project ? `--project ${project}` : "";
|
|
774
774
|
const run = (cmd) => {
|
|
775
775
|
try {
|
|
776
|
-
return
|
|
776
|
+
return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
|
|
777
777
|
} catch (e) {
|
|
778
778
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
779
779
|
}
|
|
@@ -797,14 +797,14 @@ ${run(c)}`).join("\n\n");
|
|
|
797
797
|
subscription: z.string().optional().describe("Azure Subscription ID"),
|
|
798
798
|
resourceGroup: z.string().optional().describe("Filter by resource group")
|
|
799
799
|
}, async (args) => {
|
|
800
|
-
const { execSync:
|
|
800
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
801
801
|
const sub = args["subscription"];
|
|
802
802
|
const rg = args["resourceGroup"];
|
|
803
803
|
const sf = sub ? `--subscription ${sub}` : "";
|
|
804
804
|
const rf = rg ? `--resource-group ${rg}` : "";
|
|
805
805
|
const run = (cmd) => {
|
|
806
806
|
try {
|
|
807
|
-
return
|
|
807
|
+
return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
|
|
808
808
|
} catch (e) {
|
|
809
809
|
return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
|
|
810
810
|
}
|
|
@@ -827,11 +827,11 @@ ${run(c)}`).join("\n\n");
|
|
|
827
827
|
tool("scan_installed_apps", "Scan all installed apps and tools \u2014 IDEs, office, dev tools, business apps, databases", {
|
|
828
828
|
searchHint: z.string().optional().describe('Optional search term to find specific tools (e.g. "hubspot windsurf cursor")')
|
|
829
829
|
}, async (args) => {
|
|
830
|
-
const { execSync:
|
|
830
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
831
831
|
const hint = args["searchHint"];
|
|
832
832
|
const run = (cmd) => {
|
|
833
833
|
try {
|
|
834
|
-
return
|
|
834
|
+
return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
|
|
835
835
|
} catch {
|
|
836
836
|
return "";
|
|
837
837
|
}
|
|
@@ -1630,525 +1630,6 @@ function exportJSON(db, sessionId) {
|
|
|
1630
1630
|
sops
|
|
1631
1631
|
}, null, 2);
|
|
1632
1632
|
}
|
|
1633
|
-
function exportHTML(nodes, edges) {
|
|
1634
|
-
const graphData = JSON.stringify({
|
|
1635
|
-
nodes: nodes.map((n) => ({
|
|
1636
|
-
id: n.id,
|
|
1637
|
-
name: n.name,
|
|
1638
|
-
type: n.type,
|
|
1639
|
-
layer: nodeLayer(n.type),
|
|
1640
|
-
confidence: n.confidence,
|
|
1641
|
-
discoveredVia: n.discoveredVia,
|
|
1642
|
-
discoveredAt: n.discoveredAt,
|
|
1643
|
-
tags: n.tags,
|
|
1644
|
-
metadata: n.metadata
|
|
1645
|
-
})),
|
|
1646
|
-
links: edges.map((e) => ({
|
|
1647
|
-
source: e.sourceId,
|
|
1648
|
-
target: e.targetId,
|
|
1649
|
-
relationship: e.relationship,
|
|
1650
|
-
confidence: e.confidence,
|
|
1651
|
-
evidence: e.evidence
|
|
1652
|
-
}))
|
|
1653
|
-
});
|
|
1654
|
-
return `<!DOCTYPE html>
|
|
1655
|
-
<html lang="en">
|
|
1656
|
-
<head>
|
|
1657
|
-
<meta charset="UTF-8">
|
|
1658
|
-
<title>Cartography \u2014 Infrastructure Map</title>
|
|
1659
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
1660
|
-
<style>
|
|
1661
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1662
|
-
body { background: #0a0e14; color: #e6edf3; font-family: 'SF Mono','Fira Code','Cascadia Code',monospace; display: flex; overflow: hidden; height: 100vh; }
|
|
1663
|
-
|
|
1664
|
-
/* \u2500\u2500 Left node 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 */
|
|
1665
|
-
#node-panel {
|
|
1666
|
-
width: 220px; min-width: 220px; height: 100vh; overflow: hidden;
|
|
1667
|
-
background: #0d1117; border-right: 1px solid #1b2028;
|
|
1668
|
-
display: flex; flex-direction: column;
|
|
1669
|
-
}
|
|
1670
|
-
#node-panel-header {
|
|
1671
|
-
padding: 10px 12px 8px; border-bottom: 1px solid #1b2028;
|
|
1672
|
-
font-size: 11px; color: #6e7681; text-transform: uppercase; letter-spacing: 0.6px;
|
|
1673
|
-
}
|
|
1674
|
-
#node-search {
|
|
1675
|
-
width: calc(100% - 16px); margin: 8px; padding: 5px 8px;
|
|
1676
|
-
background: #161b22; border: 1px solid #30363d; border-radius: 5px;
|
|
1677
|
-
color: #e6edf3; font-size: 11px; font-family: inherit; outline: none;
|
|
1678
|
-
}
|
|
1679
|
-
#node-search:focus { border-color: #58a6ff; }
|
|
1680
|
-
#node-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
|
|
1681
|
-
.node-list-item {
|
|
1682
|
-
padding: 5px 12px; cursor: pointer; font-size: 11px;
|
|
1683
|
-
display: flex; align-items: center; gap: 6px; border-left: 2px solid transparent;
|
|
1684
|
-
}
|
|
1685
|
-
.node-list-item:hover { background: #161b22; }
|
|
1686
|
-
.node-list-item.active { background: #1a2436; border-left-color: #58a6ff; }
|
|
1687
|
-
.node-list-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }
|
|
1688
|
-
.node-list-name { color: #c9d1d9; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
1689
|
-
.node-list-type { color: #484f58; font-size: 9px; flex-shrink: 0; }
|
|
1690
|
-
|
|
1691
|
-
/* \u2500\u2500 Center graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1692
|
-
#graph { flex: 1; height: 100vh; position: relative; }
|
|
1693
|
-
svg { width: 100%; height: 100%; }
|
|
1694
|
-
.hull { opacity: 0.12; stroke-width: 1.5; stroke-opacity: 0.25; }
|
|
1695
|
-
.hull-label { font-size: 13px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; fill-opacity: 0.5; pointer-events: none; }
|
|
1696
|
-
.link { stroke-opacity: 0.4; }
|
|
1697
|
-
.link-label { font-size: 8px; fill: #6e7681; pointer-events: none; opacity: 0; }
|
|
1698
|
-
.node-hex { stroke-width: 1.8; cursor: pointer; transition: opacity 0.15s; }
|
|
1699
|
-
.node-hex:hover { filter: brightness(1.3); stroke-width: 3; }
|
|
1700
|
-
.node-hex.selected { stroke-width: 3.5; filter: brightness(1.5); }
|
|
1701
|
-
.node-label { font-size: 10px; fill: #c9d1d9; pointer-events: none; opacity: 0; }
|
|
1702
|
-
|
|
1703
|
-
/* \u2500\u2500 Right sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1704
|
-
#sidebar {
|
|
1705
|
-
width: 300px; min-width: 300px; height: 100vh; overflow-y: auto;
|
|
1706
|
-
background: #0d1117; border-left: 1px solid #1b2028;
|
|
1707
|
-
padding: 16px; font-size: 12px; line-height: 1.6;
|
|
1708
|
-
}
|
|
1709
|
-
#sidebar h2 { margin: 0 0 8px; font-size: 14px; color: #58a6ff; }
|
|
1710
|
-
#sidebar .meta-table { width: 100%; border-collapse: collapse; }
|
|
1711
|
-
#sidebar .meta-table td { padding: 3px 6px; border-bottom: 1px solid #161b22; vertical-align: top; }
|
|
1712
|
-
#sidebar .meta-table td:first-child { color: #6e7681; white-space: nowrap; width: 90px; }
|
|
1713
|
-
#sidebar .tag { display: inline-block; background: #161b22; border-radius: 3px; padding: 1px 5px; margin: 1px; font-size: 10px; }
|
|
1714
|
-
#sidebar .conf-bar { height: 5px; border-radius: 3px; background: #161b22; margin-top: 3px; }
|
|
1715
|
-
#sidebar .conf-fill { height: 100%; border-radius: 3px; }
|
|
1716
|
-
#sidebar .edges-list { margin-top: 12px; }
|
|
1717
|
-
#sidebar .edge-item { padding: 4px 0; border-bottom: 1px solid #161b22; color: #6e7681; font-size: 11px; }
|
|
1718
|
-
#sidebar .edge-item span { color: #c9d1d9; }
|
|
1719
|
-
#sidebar .action-row { display: flex; gap: 6px; margin-top: 14px; }
|
|
1720
|
-
.btn-delete {
|
|
1721
|
-
flex: 1; padding: 6px 10px; background: transparent; border: 1px solid #6e191d;
|
|
1722
|
-
color: #f85149; border-radius: 5px; font-size: 11px; font-family: inherit;
|
|
1723
|
-
cursor: pointer; text-align: center;
|
|
1724
|
-
}
|
|
1725
|
-
.btn-delete:hover { background: #3d0c0c; }
|
|
1726
|
-
.hint { color: #3d434b; font-size: 11px; margin-top: 8px; }
|
|
1727
|
-
|
|
1728
|
-
/* \u2500\u2500 HUD \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1729
|
-
#hud { position: absolute; top: 10px; left: 10px; background: rgba(10,14,20,0.88);
|
|
1730
|
-
padding: 10px 14px; border-radius: 8px; font-size: 12px; border: 1px solid #1b2028; pointer-events: none; }
|
|
1731
|
-
#hud strong { color: #58a6ff; }
|
|
1732
|
-
#hud .stats { color: #6e7681; }
|
|
1733
|
-
#hud .zoom-level { color: #3d434b; font-size: 10px; margin-top: 2px; }
|
|
1734
|
-
|
|
1735
|
-
/* \u2500\u2500 Toolbar (filters + JGF export) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1736
|
-
#toolbar { position: absolute; top: 10px; right: 10px; display: flex; flex-wrap: wrap; gap: 4px; pointer-events: auto; align-items: center; }
|
|
1737
|
-
.filter-btn {
|
|
1738
|
-
background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
|
|
1739
|
-
color: #c9d1d9; padding: 4px 10px; font-size: 11px; cursor: pointer;
|
|
1740
|
-
font-family: inherit; display: flex; align-items: center; gap: 5px;
|
|
1741
|
-
}
|
|
1742
|
-
.filter-btn:hover { border-color: #30363d; }
|
|
1743
|
-
.filter-btn.off { opacity: 0.35; }
|
|
1744
|
-
.filter-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
|
|
1745
|
-
.export-btn {
|
|
1746
|
-
background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
|
|
1747
|
-
color: #58a6ff; padding: 4px 12px; font-size: 11px; cursor: pointer;
|
|
1748
|
-
font-family: inherit;
|
|
1749
|
-
}
|
|
1750
|
-
.export-btn:hover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
|
|
1751
|
-
</style>
|
|
1752
|
-
</head>
|
|
1753
|
-
<body>
|
|
1754
|
-
|
|
1755
|
-
<!-- Left: node list panel -->
|
|
1756
|
-
<div id="node-panel">
|
|
1757
|
-
<div id="node-panel-header">Nodes (${nodes.length})</div>
|
|
1758
|
-
<input id="node-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false">
|
|
1759
|
-
<div id="node-list"></div>
|
|
1760
|
-
</div>
|
|
1761
|
-
|
|
1762
|
-
<!-- Center: graph -->
|
|
1763
|
-
<div id="graph">
|
|
1764
|
-
<div id="hud">
|
|
1765
|
-
<strong>Cartography</strong>
|
|
1766
|
-
<span class="stats" id="hud-stats">${nodes.length} nodes \xB7 ${edges.length} edges</span><br>
|
|
1767
|
-
<span class="zoom-level">Scroll = zoom \xB7 Drag = pan \xB7 Click = details</span>
|
|
1768
|
-
</div>
|
|
1769
|
-
<div id="toolbar"></div>
|
|
1770
|
-
<svg></svg>
|
|
1771
|
-
</div>
|
|
1772
|
-
|
|
1773
|
-
<!-- Right: detail sidebar -->
|
|
1774
|
-
<div id="sidebar">
|
|
1775
|
-
<h2>Infrastructure Map</h2>
|
|
1776
|
-
<p class="hint">Click a node to view details.</p>
|
|
1777
|
-
</div>
|
|
1778
|
-
|
|
1779
|
-
<script>
|
|
1780
|
-
const data = ${graphData};
|
|
1781
|
-
|
|
1782
|
-
// \u2500\u2500 Color palette per node type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1783
|
-
const TYPE_COLORS = {
|
|
1784
|
-
host: '#4a9eff', database_server: '#ff6b6b', database: '#ff8c42',
|
|
1785
|
-
web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
|
|
1786
|
-
message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
|
|
1787
|
-
container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
|
|
1788
|
-
config_file: '#adb5bd', saas_tool: '#c084fc', table: '#f97316', unknown: '#6c757d',
|
|
1789
|
-
};
|
|
1790
|
-
|
|
1791
|
-
const LAYER_COLORS = {
|
|
1792
|
-
saas: '#c084fc', web: '#6bcb77', data: '#ff6b6b',
|
|
1793
|
-
messaging: '#c77dff', infra: '#4a9eff', config: '#adb5bd', other: '#6c757d',
|
|
1794
|
-
};
|
|
1795
|
-
const LAYER_NAMES = {
|
|
1796
|
-
saas: 'SaaS Tools', web: 'Web / API', data: 'Data Layer',
|
|
1797
|
-
messaging: 'Messaging', infra: 'Infrastructure', config: 'Config', other: 'Other',
|
|
1798
|
-
};
|
|
1799
|
-
|
|
1800
|
-
// \u2500\u2500 Hexagon path \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1801
|
-
const HEX_SIZE = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
|
|
1802
|
-
function hexSize(d) { return HEX_SIZE[d.type] || HEX_SIZE.default; }
|
|
1803
|
-
function hexPath(size) {
|
|
1804
|
-
const pts = [];
|
|
1805
|
-
for (let i = 0; i < 6; i++) {
|
|
1806
|
-
const angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
1807
|
-
pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
|
|
1808
|
-
}
|
|
1809
|
-
return 'M' + pts.map(p => p.join(',')).join('L') + 'Z';
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// \u2500\u2500 Left 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
|
|
1813
|
-
const nodeListEl = document.getElementById('node-list');
|
|
1814
|
-
const nodeSearchEl = document.getElementById('node-search');
|
|
1815
|
-
let selectedNodeId = null;
|
|
1816
|
-
|
|
1817
|
-
function buildNodeList(filter) {
|
|
1818
|
-
const q = (filter || '').toLowerCase();
|
|
1819
|
-
nodeListEl.innerHTML = '';
|
|
1820
|
-
const sorted = [...data.nodes].sort((a, b) => a.name.localeCompare(b.name));
|
|
1821
|
-
for (const d of sorted) {
|
|
1822
|
-
if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
|
|
1823
|
-
const item = document.createElement('div');
|
|
1824
|
-
item.className = 'node-list-item' + (d.id === selectedNodeId ? ' active' : '');
|
|
1825
|
-
item.dataset.id = d.id;
|
|
1826
|
-
const color = TYPE_COLORS[d.type] || '#aaa';
|
|
1827
|
-
item.innerHTML = \`<span class="node-list-dot" style="background:\${color}"></span>
|
|
1828
|
-
<span class="node-list-name" title="\${d.id}">\${d.name}</span>
|
|
1829
|
-
<span class="node-list-type">\${d.type.replace(/_/g,' ')}</span>\`;
|
|
1830
|
-
item.onclick = () => { selectNode(d); focusNode(d); };
|
|
1831
|
-
nodeListEl.appendChild(item);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
nodeSearchEl.addEventListener('input', e => buildNodeList(e.target.value));
|
|
1836
|
-
|
|
1837
|
-
// \u2500\u2500 Sidebar detail view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1838
|
-
const sidebar = document.getElementById('sidebar');
|
|
1839
|
-
|
|
1840
|
-
function selectNode(d) {
|
|
1841
|
-
selectedNodeId = d.id;
|
|
1842
|
-
buildNodeList(nodeSearchEl.value);
|
|
1843
|
-
showNode(d);
|
|
1844
|
-
// highlight hex
|
|
1845
|
-
d3.selectAll('.node-hex').classed('selected', nd => nd.id === d.id);
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
function showNode(d) {
|
|
1849
|
-
const c = TYPE_COLORS[d.type] || '#aaa';
|
|
1850
|
-
const confPct = Math.round(d.confidence * 100);
|
|
1851
|
-
const tags = (d.tags || []).map(t => \`<span class="tag">\${t}</span>\`).join('');
|
|
1852
|
-
const metaRows = Object.entries(d.metadata || {})
|
|
1853
|
-
.filter(([,v]) => v !== null && v !== undefined && String(v).length > 0)
|
|
1854
|
-
.map(([k,v]) => \`<tr><td>\${k}</td><td>\${JSON.stringify(v)}</td></tr>\`)
|
|
1855
|
-
.join('');
|
|
1856
|
-
const related = data.links.filter(l =>
|
|
1857
|
-
(l.source.id||l.source) === d.id || (l.target.id||l.target) === d.id
|
|
1858
|
-
);
|
|
1859
|
-
const edgeItems = related.map(l => {
|
|
1860
|
-
const isOut = (l.source.id||l.source) === d.id;
|
|
1861
|
-
const other = isOut ? (l.target.id||l.target) : (l.source.id||l.source);
|
|
1862
|
-
return \`<div class="edge-item">\${isOut ? '\u2192' : '\u2190'} <span>\${other}</span> <small>[\${l.relationship}]</small></div>\`;
|
|
1863
|
-
}).join('');
|
|
1864
|
-
|
|
1865
|
-
sidebar.innerHTML = \`
|
|
1866
|
-
<h2>\${d.name}</h2>
|
|
1867
|
-
<table class="meta-table">
|
|
1868
|
-
<tr><td>ID</td><td style="font-size:10px;word-break:break-all">\${d.id}</td></tr>
|
|
1869
|
-
<tr><td>Type</td><td><span style="color:\${c}">\${d.type}</span></td></tr>
|
|
1870
|
-
<tr><td>Layer</td><td>\${d.layer}</td></tr>
|
|
1871
|
-
<tr><td>Confidence</td><td>
|
|
1872
|
-
\${confPct}%
|
|
1873
|
-
<div class="conf-bar"><div class="conf-fill" style="width:\${confPct}%;background:\${c}"></div></div>
|
|
1874
|
-
</td></tr>
|
|
1875
|
-
<tr><td>Discovered via</td><td>\${d.discoveredVia || '\u2014'}</td></tr>
|
|
1876
|
-
<tr><td>Timestamp</td><td>\${d.discoveredAt ? d.discoveredAt.substring(0,19).replace('T',' ') : '\u2014'}</td></tr>
|
|
1877
|
-
\${tags ? '<tr><td>Tags</td><td>'+tags+'</td></tr>' : ''}
|
|
1878
|
-
\${metaRows}
|
|
1879
|
-
</table>
|
|
1880
|
-
\${related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>'+edgeItems+'</div>' : ''}
|
|
1881
|
-
<div class="action-row">
|
|
1882
|
-
<button class="btn-delete" onclick="deleteNode('\${d.id}')">\u{1F5D1} Delete node</button>
|
|
1883
|
-
</div>
|
|
1884
|
-
\`;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
// \u2500\u2500 Delete node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1888
|
-
function deleteNode(id) {
|
|
1889
|
-
const idx = data.nodes.findIndex(n => n.id === id);
|
|
1890
|
-
if (idx === -1) return;
|
|
1891
|
-
data.nodes.splice(idx, 1);
|
|
1892
|
-
data.links = data.links.filter(l =>
|
|
1893
|
-
(l.source.id || l.source) !== id && (l.target.id || l.target) !== id
|
|
1894
|
-
);
|
|
1895
|
-
selectedNodeId = null;
|
|
1896
|
-
sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
|
|
1897
|
-
document.getElementById('hud-stats').textContent =
|
|
1898
|
-
data.nodes.length + ' nodes \xB7 ' + data.links.length + ' edges';
|
|
1899
|
-
rebuildGraph();
|
|
1900
|
-
buildNodeList(nodeSearchEl.value);
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
// \u2500\u2500 SVG setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1904
|
-
const svgEl = d3.select('svg');
|
|
1905
|
-
const graphDiv = document.getElementById('graph');
|
|
1906
|
-
const W = () => graphDiv.clientWidth;
|
|
1907
|
-
const H = () => graphDiv.clientHeight;
|
|
1908
|
-
const g = svgEl.append('g');
|
|
1909
|
-
|
|
1910
|
-
svgEl.append('defs').append('marker')
|
|
1911
|
-
.attr('id', 'arrow').attr('viewBox', '0 0 10 6')
|
|
1912
|
-
.attr('refX', 10).attr('refY', 3)
|
|
1913
|
-
.attr('markerWidth', 8).attr('markerHeight', 6)
|
|
1914
|
-
.attr('orient', 'auto')
|
|
1915
|
-
.append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
|
|
1916
|
-
|
|
1917
|
-
let currentZoom = 1;
|
|
1918
|
-
const zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', e => {
|
|
1919
|
-
g.attr('transform', e.transform);
|
|
1920
|
-
currentZoom = e.transform.k;
|
|
1921
|
-
updateLOD(currentZoom);
|
|
1922
|
-
});
|
|
1923
|
-
svgEl.call(zoomBehavior);
|
|
1924
|
-
|
|
1925
|
-
// \u2500\u2500 Layer filter state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1926
|
-
const layers = [...new Set(data.nodes.map(d => d.layer))];
|
|
1927
|
-
const layerVisible = {};
|
|
1928
|
-
layers.forEach(l => layerVisible[l] = true);
|
|
1929
|
-
|
|
1930
|
-
const toolbarEl = document.getElementById('toolbar');
|
|
1931
|
-
|
|
1932
|
-
// Filter buttons
|
|
1933
|
-
layers.forEach(layer => {
|
|
1934
|
-
const btn = document.createElement('button');
|
|
1935
|
-
btn.className = 'filter-btn';
|
|
1936
|
-
btn.innerHTML = \`<span class="filter-dot" style="background:\${LAYER_COLORS[layer]||'#666'}"></span>\${LAYER_NAMES[layer]||layer}\`;
|
|
1937
|
-
btn.onclick = () => {
|
|
1938
|
-
layerVisible[layer] = !layerVisible[layer];
|
|
1939
|
-
btn.classList.toggle('off', !layerVisible[layer]);
|
|
1940
|
-
updateVisibility();
|
|
1941
|
-
};
|
|
1942
|
-
toolbarEl.appendChild(btn);
|
|
1943
|
-
});
|
|
1944
|
-
|
|
1945
|
-
// JGF export button
|
|
1946
|
-
const jgfBtn = document.createElement('button');
|
|
1947
|
-
jgfBtn.className = 'export-btn';
|
|
1948
|
-
jgfBtn.textContent = '\u2193 JGF';
|
|
1949
|
-
jgfBtn.title = 'Export JSON Graph Format';
|
|
1950
|
-
jgfBtn.onclick = exportJGF;
|
|
1951
|
-
toolbarEl.appendChild(jgfBtn);
|
|
1952
|
-
|
|
1953
|
-
// \u2500\u2500 JGF export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1954
|
-
function exportJGF() {
|
|
1955
|
-
const jgf = {
|
|
1956
|
-
graph: {
|
|
1957
|
-
directed: true,
|
|
1958
|
-
type: 'cartography',
|
|
1959
|
-
label: 'Infrastructure Map',
|
|
1960
|
-
metadata: { exportedAt: new Date().toISOString() },
|
|
1961
|
-
nodes: Object.fromEntries(data.nodes.map(n => [n.id, {
|
|
1962
|
-
label: n.name,
|
|
1963
|
-
metadata: { type: n.type, layer: n.layer, confidence: n.confidence,
|
|
1964
|
-
discoveredVia: n.discoveredVia, discoveredAt: n.discoveredAt,
|
|
1965
|
-
tags: n.tags, ...n.metadata }
|
|
1966
|
-
}])),
|
|
1967
|
-
edges: data.links.map(l => ({
|
|
1968
|
-
source: l.source.id || l.source,
|
|
1969
|
-
target: l.target.id || l.target,
|
|
1970
|
-
relation: l.relationship,
|
|
1971
|
-
metadata: { confidence: l.confidence, evidence: l.evidence }
|
|
1972
|
-
})),
|
|
1973
|
-
}
|
|
1974
|
-
};
|
|
1975
|
-
const blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
|
|
1976
|
-
const url = URL.createObjectURL(blob);
|
|
1977
|
-
const a = document.createElement('a');
|
|
1978
|
-
a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
|
|
1979
|
-
URL.revokeObjectURL(url);
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
// \u2500\u2500 Cluster force \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1983
|
-
function clusterForce(alpha) {
|
|
1984
|
-
const centroids = {};
|
|
1985
|
-
const counts = {};
|
|
1986
|
-
data.nodes.forEach(d => {
|
|
1987
|
-
if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
|
|
1988
|
-
centroids[d.layer].x += d.x || 0;
|
|
1989
|
-
centroids[d.layer].y += d.y || 0;
|
|
1990
|
-
counts[d.layer]++;
|
|
1991
|
-
});
|
|
1992
|
-
for (const l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
|
|
1993
|
-
const strength = alpha * 0.15;
|
|
1994
|
-
data.nodes.forEach(d => {
|
|
1995
|
-
const c = centroids[d.layer];
|
|
1996
|
-
if (c) { d.vx += (c.x - d.x) * strength; d.vy += (c.y - d.y) * strength; }
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// \u2500\u2500 Hull group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2001
|
-
const hullGroup = g.append('g').attr('class', 'hulls');
|
|
2002
|
-
const hullPaths = {};
|
|
2003
|
-
const hullLabels = {};
|
|
2004
|
-
layers.forEach(layer => {
|
|
2005
|
-
hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
|
|
2006
|
-
.attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
|
|
2007
|
-
hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
|
|
2008
|
-
.attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
|
|
2009
|
-
});
|
|
2010
|
-
|
|
2011
|
-
function updateHulls() {
|
|
2012
|
-
layers.forEach(layer => {
|
|
2013
|
-
if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
|
|
2014
|
-
const pts = data.nodes.filter(d => d.layer === layer && layerVisible[d.layer]).map(d => [d.x, d.y]);
|
|
2015
|
-
if (pts.length < 3) {
|
|
2016
|
-
hullPaths[layer].attr('d', null);
|
|
2017
|
-
if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
|
|
2018
|
-
else hullLabels[layer].attr('x', -9999);
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
const hull = d3.polygonHull(pts);
|
|
2022
|
-
if (!hull) { hullPaths[layer].attr('d', null); return; }
|
|
2023
|
-
const cx = d3.mean(hull, p => p[0]);
|
|
2024
|
-
const cy = d3.mean(hull, p => p[1]);
|
|
2025
|
-
const padded = hull.map(p => {
|
|
2026
|
-
const dx = p[0] - cx, dy = p[1] - cy;
|
|
2027
|
-
const len = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
2028
|
-
return [p[0] + dx/len * 40, p[1] + dy/len * 40];
|
|
2029
|
-
});
|
|
2030
|
-
hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
|
|
2031
|
-
hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, p => Math.abs(p[1] - cy)) - 30);
|
|
2032
|
-
});
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
// \u2500\u2500 Graph rendering (rebuildable after delete) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2036
|
-
let linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
|
|
2037
|
-
const linkGroup = g.append('g');
|
|
2038
|
-
const nodeGroup = g.append('g');
|
|
2039
|
-
|
|
2040
|
-
function focusNode(d) {
|
|
2041
|
-
if (!d.x || !d.y) return;
|
|
2042
|
-
const w = W(), h = H();
|
|
2043
|
-
svgEl.transition().duration(500).call(
|
|
2044
|
-
zoomBehavior.transform,
|
|
2045
|
-
d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
|
|
2046
|
-
);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
function rebuildGraph() {
|
|
2050
|
-
if (sim) sim.stop();
|
|
2051
|
-
|
|
2052
|
-
// Links
|
|
2053
|
-
linkSel = linkGroup.selectAll('line').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
|
|
2054
|
-
linkSel.exit().remove();
|
|
2055
|
-
const linkEnter = linkSel.enter().append('line').attr('class', 'link');
|
|
2056
|
-
linkSel = linkEnter.merge(linkSel)
|
|
2057
|
-
.attr('stroke', d => d.confidence < 0.6 ? '#2a2e35' : '#3d434b')
|
|
2058
|
-
.attr('stroke-dasharray', d => d.confidence < 0.6 ? '4 3' : null)
|
|
2059
|
-
.attr('stroke-width', d => d.confidence < 0.6 ? 0.8 : 1.2)
|
|
2060
|
-
.attr('marker-end', 'url(#arrow)');
|
|
2061
|
-
linkSel.select('title').remove();
|
|
2062
|
-
linkSel.append('title').text(d => \`\${d.relationship} (\${Math.round(d.confidence*100)}%)
|
|
2063
|
-
\${d.evidence||''}\`);
|
|
2064
|
-
|
|
2065
|
-
// Link labels
|
|
2066
|
-
linkLabelSel = linkGroup.selectAll('text').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
|
|
2067
|
-
linkLabelSel.exit().remove();
|
|
2068
|
-
linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel)
|
|
2069
|
-
.text(d => d.relationship);
|
|
2070
|
-
|
|
2071
|
-
// Nodes
|
|
2072
|
-
nodeSel = nodeGroup.selectAll('g').data(data.nodes, d => d.id);
|
|
2073
|
-
nodeSel.exit().remove();
|
|
2074
|
-
const nodeEnter = nodeSel.enter().append('g')
|
|
2075
|
-
.call(d3.drag()
|
|
2076
|
-
.on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
2077
|
-
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
2078
|
-
.on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
2079
|
-
)
|
|
2080
|
-
.on('click', (e, d) => { e.stopPropagation(); selectNode(d); });
|
|
2081
|
-
nodeEnter.append('path').attr('class', 'node-hex');
|
|
2082
|
-
nodeEnter.append('title');
|
|
2083
|
-
nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
|
|
2084
|
-
|
|
2085
|
-
nodeSel = nodeEnter.merge(nodeSel);
|
|
2086
|
-
nodeSel.select('.node-hex')
|
|
2087
|
-
.attr('d', d => hexPath(hexSize(d)))
|
|
2088
|
-
.attr('fill', d => TYPE_COLORS[d.type] || '#aaa')
|
|
2089
|
-
.attr('stroke', d => { const c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
|
|
2090
|
-
.attr('fill-opacity', d => 0.6 + d.confidence * 0.4)
|
|
2091
|
-
.classed('selected', d => d.id === selectedNodeId);
|
|
2092
|
-
nodeSel.select('title').text(d => \`\${d.name} (\${d.type})
|
|
2093
|
-
conf: \${Math.round(d.confidence*100)}%\`);
|
|
2094
|
-
nodeLabelSel = nodeSel.select('.node-label')
|
|
2095
|
-
.attr('dy', d => hexSize(d) + 13)
|
|
2096
|
-
.text(d => d.name.length > 20 ? d.name.substring(0, 18) + '\u2026' : d.name);
|
|
2097
|
-
|
|
2098
|
-
// Simulation
|
|
2099
|
-
sim = d3.forceSimulation(data.nodes)
|
|
2100
|
-
.force('link', d3.forceLink(data.links).id(d => d.id).distance(d => d.relationship === 'contains' ? 50 : 100).strength(0.4))
|
|
2101
|
-
.force('charge', d3.forceManyBody().strength(-280))
|
|
2102
|
-
.force('center', d3.forceCenter(W() / 2, H() / 2))
|
|
2103
|
-
.force('collision', d3.forceCollide().radius(d => hexSize(d) + 10))
|
|
2104
|
-
.force('cluster', clusterForce)
|
|
2105
|
-
.on('tick', () => {
|
|
2106
|
-
updateHulls();
|
|
2107
|
-
linkSel.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
2108
|
-
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
2109
|
-
linkLabelSel.attr('x', d => (d.source.x + d.target.x) / 2)
|
|
2110
|
-
.attr('y', d => (d.source.y + d.target.y) / 2 - 4);
|
|
2111
|
-
nodeSel.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
// \u2500\u2500 LOD & visibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2116
|
-
function updateLOD(k) {
|
|
2117
|
-
if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
|
|
2118
|
-
if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
|
|
2119
|
-
d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
function updateVisibility() {
|
|
2123
|
-
if (!nodeSel) return;
|
|
2124
|
-
nodeSel.style('display', d => layerVisible[d.layer] ? null : 'none');
|
|
2125
|
-
linkSel.style('display', d => {
|
|
2126
|
-
const s = data.nodes.find(n => n.id === (d.source.id||d.source));
|
|
2127
|
-
const t = data.nodes.find(n => n.id === (d.target.id||d.target));
|
|
2128
|
-
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2129
|
-
});
|
|
2130
|
-
linkLabelSel.style('display', d => {
|
|
2131
|
-
const s = data.nodes.find(n => n.id === (d.source.id||d.source));
|
|
2132
|
-
const t = data.nodes.find(n => n.id === (d.target.id||d.target));
|
|
2133
|
-
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2134
|
-
});
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
// \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
|
|
2138
|
-
rebuildGraph();
|
|
2139
|
-
buildNodeList();
|
|
2140
|
-
updateLOD(1);
|
|
2141
|
-
|
|
2142
|
-
svgEl.on('click', () => {
|
|
2143
|
-
selectedNodeId = null;
|
|
2144
|
-
d3.selectAll('.node-hex').classed('selected', false);
|
|
2145
|
-
buildNodeList(nodeSearchEl.value);
|
|
2146
|
-
sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
|
|
2147
|
-
});
|
|
2148
|
-
</script>
|
|
2149
|
-
</body>
|
|
2150
|
-
</html>`;
|
|
2151
|
-
}
|
|
2152
1633
|
function exportSOPMarkdown(sop) {
|
|
2153
1634
|
const lines = [
|
|
2154
1635
|
`# ${sop.title}`,
|
|
@@ -2170,646 +1651,6 @@ function exportSOPMarkdown(sop) {
|
|
|
2170
1651
|
}
|
|
2171
1652
|
return lines.join("\n");
|
|
2172
1653
|
}
|
|
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) => ({
|
|
2180
|
-
id: a.id,
|
|
2181
|
-
name: a.name,
|
|
2182
|
-
domain: a.domain,
|
|
2183
|
-
subDomain: a.subDomain ?? null,
|
|
2184
|
-
qualityScore: a.qualityScore ?? null,
|
|
2185
|
-
metadata: a.metadata,
|
|
2186
|
-
q: a.position.q,
|
|
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"
|
|
2202
|
-
}))
|
|
2203
|
-
});
|
|
2204
|
-
return `<!DOCTYPE html>
|
|
2205
|
-
<html lang="en">
|
|
2206
|
-
<head>
|
|
2207
|
-
<meta charset="UTF-8"/>
|
|
2208
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
2209
|
-
<title>Data Cartography Map</title>
|
|
2210
|
-
<style>
|
|
2211
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
2212
|
-
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
2213
|
-
body{display:flex;flex-direction:column;background:${meta.theme === "dark" ? "#0f172a" : "#f8fafc"};color:${meta.theme === "dark" ? "#e2e8f0" : "#1e293b"}}
|
|
2214
|
-
#topbar{
|
|
2215
|
-
height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2216
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};z-index:10;flex-shrink:0;
|
|
2217
|
-
}
|
|
2218
|
-
#topbar h1{font-size:15px;font-weight:600;letter-spacing:-0.01em}
|
|
2219
|
-
#search-box{
|
|
2220
|
-
display:flex;align-items:center;gap:8px;background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"};
|
|
2221
|
-
border-radius:8px;padding:5px 10px;margin-left:auto;
|
|
2222
|
-
}
|
|
2223
|
-
#search-box input{
|
|
2224
|
-
border:none;background:transparent;font-size:13px;outline:none;width:180px;color:inherit;
|
|
2225
|
-
}
|
|
2226
|
-
#search-box input::placeholder{color:#94a3b8}
|
|
2227
|
-
#main{flex:1;display:flex;overflow:hidden;position:relative}
|
|
2228
|
-
#canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2229
|
-
#canvas-wrap.dragging{cursor:grabbing}
|
|
2230
|
-
#canvas-wrap.connecting{cursor:crosshair}
|
|
2231
|
-
canvas{display:block;width:100%;height:100%}
|
|
2232
|
-
/* Detail panel */
|
|
2233
|
-
#detail-panel{
|
|
2234
|
-
width:280px;background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-left:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2235
|
-
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2236
|
-
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2237
|
-
}
|
|
2238
|
-
#detail-panel.open{transform:translateX(0)}
|
|
2239
|
-
#detail-panel .panel-header{
|
|
2240
|
-
padding:16px;border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};display:flex;align-items:center;gap:10px;
|
|
2241
|
-
}
|
|
2242
|
-
#detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2243
|
-
#detail-panel .close-btn{
|
|
2244
|
-
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2245
|
-
color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2246
|
-
}
|
|
2247
|
-
#detail-panel .close-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2248
|
-
#detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2249
|
-
#detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2250
|
-
#detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
|
|
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}
|
|
2253
|
-
#detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2254
|
-
/* Bottom-left toolbar */
|
|
2255
|
-
#toolbar-left{
|
|
2256
|
-
position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
|
|
2257
|
-
}
|
|
2258
|
-
.tb-btn{
|
|
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;
|
|
2261
|
-
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2262
|
-
transition:all .15s;color:inherit;
|
|
2263
|
-
}
|
|
2264
|
-
.tb-btn:hover{border-color:#94a3b8}
|
|
2265
|
-
.tb-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6}
|
|
2266
|
-
/* Bottom-right toolbar */
|
|
2267
|
-
#toolbar-right{
|
|
2268
|
-
position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
|
|
2269
|
-
align-items:flex-end;gap:8px;z-index:10;
|
|
2270
|
-
}
|
|
2271
|
-
#zoom-controls{display:flex;align-items:center;gap:6px}
|
|
2272
|
-
.zoom-btn{
|
|
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;
|
|
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}
|
|
2279
|
-
#detail-selector{display:flex;flex-direction:column;gap:4px}
|
|
2280
|
-
.detail-btn{
|
|
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;
|
|
2283
|
-
font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
|
|
2284
|
-
}
|
|
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}
|
|
2287
|
-
#connect-btn{
|
|
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;
|
|
2291
|
-
}
|
|
2292
|
-
#connect-btn.active{background:#fef3c7;border-color:#f59e0b}
|
|
2293
|
-
/* Tooltip */
|
|
2294
|
-
#tooltip{
|
|
2295
|
-
position:fixed;background:#1e293b;color:#fff;border-radius:8px;
|
|
2296
|
-
padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
|
|
2297
|
-
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
|
2298
|
-
}
|
|
2299
|
-
#tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2300
|
-
#tooltip .tt-domain{color:#94a3b8;font-size:11px}
|
|
2301
|
-
#tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2302
|
-
/* Empty state */
|
|
2303
|
-
#empty-state{
|
|
2304
|
-
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2305
|
-
align-items:center;justify-content:center;gap:12px;color:#94a3b8;
|
|
2306
|
-
}
|
|
2307
|
-
#empty-state p{font-size:14px}
|
|
2308
|
-
/* Theme toggle */
|
|
2309
|
-
#theme-btn{
|
|
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;
|
|
2313
|
-
}
|
|
2314
|
-
/* Dark mode overrides (toggled via JS) */
|
|
2315
|
-
body.dark{background:#0f172a;color:#e2e8f0}
|
|
2316
|
-
body.dark #topbar{background:#1e293b;border-color:#334155}
|
|
2317
|
-
body.dark #search-box{background:#334155}
|
|
2318
|
-
body.dark #detail-panel{background:#1e293b;border-color:#334155}
|
|
2319
|
-
body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
|
|
2320
|
-
background:#1e293b;border-color:#334155;color:#e2e8f0;
|
|
2321
|
-
}
|
|
2322
|
-
/* Light mode overrides */
|
|
2323
|
-
body.light{background:#f8fafc;color:#1e293b}
|
|
2324
|
-
body.light #topbar{background:#fff;border-color:#e2e8f0}
|
|
2325
|
-
/* Connection hint */
|
|
2326
|
-
#connect-hint{
|
|
2327
|
-
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2328
|
-
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2329
|
-
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2330
|
-
display:none;z-index:20;pointer-events:none;
|
|
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}
|
|
2334
|
-
</style>
|
|
2335
|
-
</head>
|
|
2336
|
-
<body class="${meta.theme}">
|
|
2337
|
-
<!-- Top bar -->
|
|
2338
|
-
<div id="topbar">
|
|
2339
|
-
<h1>Data Cartography Map</h1>
|
|
2340
|
-
<div id="search-box">
|
|
2341
|
-
<span style="color:#94a3b8;font-size:14px">⌕</span>
|
|
2342
|
-
<input id="search-input" type="text" placeholder="Search assets..." aria-label="Search data assets"/>
|
|
2343
|
-
</div>
|
|
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.
|
|
2349
|
-
</div>
|
|
2350
|
-
<!-- Main area -->
|
|
2351
|
-
<div id="main">
|
|
2352
|
-
<div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
|
|
2353
|
-
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
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>' : ""}
|
|
2355
|
-
</div>
|
|
2356
|
-
<div id="detail-panel" role="complementary" aria-label="Asset details">
|
|
2357
|
-
<div class="panel-header">
|
|
2358
|
-
<h3 id="dp-name">—</h3>
|
|
2359
|
-
<button class="close-btn" id="dp-close" aria-label="Close panel">✕</button>
|
|
2360
|
-
</div>
|
|
2361
|
-
<div class="panel-body" id="dp-body"></div>
|
|
2362
|
-
</div>
|
|
2363
|
-
</div>
|
|
2364
|
-
<!-- Bottom-left toolbar -->
|
|
2365
|
-
<div id="toolbar-left">
|
|
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>
|
|
2368
|
-
</div>
|
|
2369
|
-
<!-- Bottom-right toolbar -->
|
|
2370
|
-
<div id="toolbar-right">
|
|
2371
|
-
<div id="zoom-controls">
|
|
2372
|
-
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out">−</button>
|
|
2373
|
-
<span id="zoom-pct">100%</span>
|
|
2374
|
-
<button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
|
|
2375
|
-
</div>
|
|
2376
|
-
<div id="detail-selector">
|
|
2377
|
-
<button class="detail-btn" id="dl-1" aria-label="Detail level 1">1</button>
|
|
2378
|
-
<button class="detail-btn active" id="dl-2" aria-label="Detail level 2">2</button>
|
|
2379
|
-
<button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
|
|
2380
|
-
<button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
|
|
2381
|
-
</div>
|
|
2382
|
-
<button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">🔗</button>
|
|
2383
|
-
</div>
|
|
2384
|
-
<!-- Connection hint -->
|
|
2385
|
-
<div id="connect-hint">Click two assets to create a connection</div>
|
|
2386
|
-
<!-- Tooltip -->
|
|
2387
|
-
<div id="tooltip" role="tooltip">
|
|
2388
|
-
<div class="tt-name" id="tt-name"></div>
|
|
2389
|
-
<div class="tt-domain" id="tt-domain"></div>
|
|
2390
|
-
<div class="tt-quality" id="tt-quality"></div>
|
|
2391
|
-
</div>
|
|
2392
|
-
|
|
2393
|
-
<script>
|
|
2394
|
-
(function() {
|
|
2395
|
-
'use strict';
|
|
2396
|
-
|
|
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
|
|
2398
|
-
const MAP = ${dataJson};
|
|
2399
|
-
const HEX_SIZE = ${HEX_SIZE2};
|
|
2400
|
-
const IS_EMPTY = ${isEmpty};
|
|
2401
|
-
|
|
2402
|
-
// Build asset index
|
|
2403
|
-
const assetIndex = new Map();
|
|
2404
|
-
const clusterByAsset = new Map();
|
|
2405
|
-
for (const c of MAP.clusters) {
|
|
2406
|
-
for (const aid of c.assetIds) {
|
|
2407
|
-
clusterByAsset.set(aid, c);
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
for (const a of MAP.assets) {
|
|
2411
|
-
assetIndex.set(a.id, a);
|
|
2412
|
-
}
|
|
2413
|
-
|
|
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
|
|
2415
|
-
const canvas = document.getElementById('hexmap');
|
|
2416
|
-
const ctx = canvas.getContext('2d');
|
|
2417
|
-
const wrap = document.getElementById('canvas-wrap');
|
|
2418
|
-
let W = 0, H = 0;
|
|
2419
|
-
|
|
2420
|
-
function resize() {
|
|
2421
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2422
|
-
W = wrap.clientWidth; H = wrap.clientHeight;
|
|
2423
|
-
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
2424
|
-
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
2425
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2426
|
-
draw();
|
|
2427
|
-
}
|
|
2428
|
-
window.addEventListener('resize', resize);
|
|
2429
|
-
|
|
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
|
|
2431
|
-
let vx = 0, vy = 0, scale = 1;
|
|
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;
|
|
2436
|
-
let searchQuery = '';
|
|
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 }; }
|
|
2444
|
-
|
|
2445
|
-
function fitToView() {
|
|
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;
|
|
2451
|
-
}
|
|
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;
|
|
2456
|
-
}
|
|
2457
|
-
|
|
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
|
|
2459
|
-
function hexPath(cx, cy, r) {
|
|
2460
|
-
ctx.beginPath();
|
|
2461
|
-
for (let i=0;i<6;i++) {
|
|
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);
|
|
2465
|
-
}
|
|
2466
|
-
ctx.closePath();
|
|
2467
|
-
}
|
|
2468
|
-
|
|
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
|
-
}
|
|
2475
|
-
|
|
2476
|
-
function draw() {
|
|
2477
|
-
ctx.clearRect(0,0,W,H);
|
|
2478
|
-
ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
|
|
2479
|
-
ctx.fillRect(0,0,W,H);
|
|
2480
|
-
if (IS_EMPTY) return;
|
|
2481
|
-
|
|
2482
|
-
const size = HEX_SIZE * scale;
|
|
2483
|
-
const matchedIds = getSearchMatches();
|
|
2484
|
-
const hasSearch = searchQuery.length > 0;
|
|
2485
|
-
|
|
2486
|
-
// Draw connections
|
|
2487
|
-
ctx.save();
|
|
2488
|
-
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
2489
|
-
ctx.lineWidth = 1.5;
|
|
2490
|
-
ctx.setLineDash([4,4]);
|
|
2491
|
-
for (const conn of localConnections) {
|
|
2492
|
-
const src = assetIndex.get(conn.sourceAssetId);
|
|
2493
|
-
const tgt = assetIndex.get(conn.targetAssetId);
|
|
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();
|
|
2498
|
-
}
|
|
2499
|
-
ctx.setLineDash([]);
|
|
2500
|
-
ctx.restore();
|
|
2501
|
-
|
|
2502
|
-
// Draw hexagons per cluster
|
|
2503
|
-
for (const cluster of MAP.clusters) {
|
|
2504
|
-
const baseColor = cluster.color;
|
|
2505
|
-
const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
|
|
2506
|
-
const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
|
|
2507
|
-
const clusterDim = hasSearch && !isClusterMatch;
|
|
2508
|
-
|
|
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;
|
|
2514
|
-
|
|
2515
|
-
// Frustum cull
|
|
2516
|
-
if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
|
|
2517
|
-
|
|
2518
|
-
// Shade variation
|
|
2519
|
-
const shade = ai%3===0?18:ai%3===1?8:0;
|
|
2520
|
-
let fillColor = shadeV(baseColor, shade);
|
|
2521
|
-
|
|
2522
|
-
// Quality overlay
|
|
2523
|
-
if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
2524
|
-
const q = asset.qualityScore;
|
|
2525
|
-
if (q < 40) fillColor = '#ef4444';
|
|
2526
|
-
else if (q < 70) fillColor = '#f97316';
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
const alpha = clusterDim ? 0.18 : 1;
|
|
2530
|
-
const isHovered = asset.id === hoveredAssetId;
|
|
2531
|
-
const isSelected = asset.id === selectedAssetId;
|
|
2532
|
-
const isConnectFirst = asset.id === connectFirst;
|
|
2533
|
-
|
|
2534
|
-
ctx.save();
|
|
2535
|
-
ctx.globalAlpha = alpha;
|
|
2536
|
-
hexPath(cx, cy, size*0.92);
|
|
2537
|
-
|
|
2538
|
-
if (isDark && (isHovered||isSelected||isConnectFirst)) {
|
|
2539
|
-
ctx.shadowColor = fillColor;
|
|
2540
|
-
ctx.shadowBlur = isSelected ? 16 : 8;
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
ctx.fillStyle = fillColor;
|
|
2544
|
-
ctx.fill();
|
|
2545
|
-
|
|
2546
|
-
if (isSelected||isConnectFirst) {
|
|
2547
|
-
ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
|
|
2548
|
-
ctx.lineWidth = 2.5;
|
|
2549
|
-
ctx.stroke();
|
|
2550
|
-
} else if (isHovered) {
|
|
2551
|
-
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
|
|
2552
|
-
ctx.lineWidth = 1.5;
|
|
2553
|
-
ctx.stroke();
|
|
2554
|
-
} else {
|
|
2555
|
-
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)';
|
|
2556
|
-
ctx.lineWidth = 1;
|
|
2557
|
-
ctx.stroke();
|
|
2558
|
-
}
|
|
2559
|
-
ctx.restore();
|
|
2560
|
-
|
|
2561
|
-
// Quality dot
|
|
2562
|
-
if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
|
|
2563
|
-
const q = asset.qualityScore;
|
|
2564
|
-
if (q < 70) {
|
|
2565
|
-
ctx.beginPath();
|
|
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';
|
|
2568
|
-
ctx.fill();
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
|
|
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;
|
|
2577
|
-
ctx.save();
|
|
2578
|
-
ctx.font = Math.max(8,Math.min(11,size*0.38))+'px -apple-system,sans-serif';
|
|
2579
|
-
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
2580
|
-
ctx.textAlign='center';ctx.textBaseline='middle';
|
|
2581
|
-
ctx.fillText(label, cx, cy);
|
|
2582
|
-
ctx.restore();
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
|
|
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);
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
|
|
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);
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
|
|
2616
|
-
function drawPill(x, y, text, color, fontSize) {
|
|
2617
|
-
if(!text) return;
|
|
2618
|
-
ctx.save();
|
|
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;
|
|
2622
|
-
ctx.beginPath();
|
|
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';
|
|
2630
|
-
ctx.fillText(text, x, y);
|
|
2631
|
-
ctx.restore();
|
|
2632
|
-
}
|
|
2633
|
-
|
|
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
|
|
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;
|
|
2642
|
-
}
|
|
2643
|
-
return null;
|
|
2644
|
-
}
|
|
2645
|
-
|
|
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
|
|
2647
|
-
function getSearchMatches() {
|
|
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);
|
|
2654
|
-
}
|
|
2655
|
-
return m;
|
|
2656
|
-
}
|
|
2657
|
-
|
|
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
|
|
2659
|
-
let dragging=false, lastMX=0, lastMY=0;
|
|
2660
|
-
|
|
2661
|
-
wrap.addEventListener('mousedown', e=>{
|
|
2662
|
-
if(e.button!==0)return;
|
|
2663
|
-
dragging=true; lastMX=e.clientX; lastMY=e.clientY;
|
|
2664
|
-
wrap.classList.add('dragging');
|
|
2665
|
-
});
|
|
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;
|
|
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'; }
|
|
2684
|
-
});
|
|
2685
|
-
|
|
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();
|
|
2696
|
-
}
|
|
2697
|
-
return;
|
|
2698
|
-
}
|
|
2699
|
-
if(asset){selectedAssetId=asset.id;showDetailPanel(asset);}
|
|
2700
|
-
else{selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');}
|
|
2701
|
-
draw();
|
|
2702
|
-
});
|
|
2703
|
-
|
|
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=>{
|
|
2722
|
-
e.preventDefault();
|
|
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();
|
|
2732
|
-
}
|
|
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();
|
|
2748
|
-
}
|
|
2749
|
-
});
|
|
2750
|
-
|
|
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
|
|
2752
|
-
function showDetailPanel(asset) {
|
|
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>';
|
|
2765
|
-
}
|
|
2766
|
-
document.getElementById('detail-panel').classList.add('open');
|
|
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
|
-
});
|
|
2776
|
-
|
|
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
1654
|
function exportDiscoveryApp(nodes, edges, options) {
|
|
2814
1655
|
const theme = options?.theme ?? "dark";
|
|
2815
1656
|
const graphData = JSON.stringify({
|
|
@@ -2970,8 +1811,9 @@ body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--te
|
|
|
2970
1811
|
width:40px;height:40px;border-radius:10px;border:1px solid var(--border);
|
|
2971
1812
|
background:var(--bg-surface);box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2972
1813
|
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2973
|
-
transition:all .15s;color:var(--text);
|
|
1814
|
+
transition:all .15s;color:var(--text);font-family:-apple-system,sans-serif;
|
|
2974
1815
|
}
|
|
1816
|
+
#btn-all-labels{font-size:14px;font-weight:700;letter-spacing:-.02em}
|
|
2975
1817
|
.tb-tool:hover{border-color:var(--text-muted)}
|
|
2976
1818
|
.tb-tool.active{background:var(--accent-dim);border-color:var(--accent)}
|
|
2977
1819
|
.map-zoom{display:flex;align-items:center;gap:6px}
|
|
@@ -3143,6 +1985,7 @@ body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--te
|
|
|
3143
1985
|
</div>
|
|
3144
1986
|
<div id="map-tb-left">
|
|
3145
1987
|
<button class="tb-tool active" id="btn-labels" title="Toggle labels">🏷</button>
|
|
1988
|
+
<button class="tb-tool" id="btn-all-labels" title="Show all hex labels">Aa</button>
|
|
3146
1989
|
<button class="tb-tool" id="btn-quality" title="Quality layer">👁</button>
|
|
3147
1990
|
<button class="tb-tool" id="btn-connect" title="Connection tool">🔗</button>
|
|
3148
1991
|
</div>
|
|
@@ -3239,7 +2082,7 @@ var mapCtx = mapCanvas.getContext('2d');
|
|
|
3239
2082
|
var mapWrap = document.getElementById('map-wrap');
|
|
3240
2083
|
var mW = 0, mH = 0;
|
|
3241
2084
|
var mvx = 0, mvy = 0, mScale = 1;
|
|
3242
|
-
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false;
|
|
2085
|
+
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false, mShowAllLabels = false;
|
|
3243
2086
|
var mConnectMode = false, mConnectFirst = null;
|
|
3244
2087
|
var mHoveredId = null, mSelectedId = null;
|
|
3245
2088
|
var mSearchQuery = '';
|
|
@@ -3384,8 +2227,8 @@ function drawMap() {
|
|
|
3384
2227
|
mapCtx.fillStyle = asset.qualityScore < 40 ? '#ef4444' : '#f97316'; mapCtx.fill();
|
|
3385
2228
|
}
|
|
3386
2229
|
|
|
3387
|
-
var showAssetLabel = mShowLabels && !clusterDim && ((mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
3388
|
-
if (showAssetLabel && size >
|
|
2230
|
+
var showAssetLabel = mShowLabels && !clusterDim && (mShowAllLabels || (mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
2231
|
+
if (showAssetLabel && size > 6) {
|
|
3389
2232
|
var label = asset.name.length > 12 ? asset.name.substring(0, 11) + '...' : asset.name;
|
|
3390
2233
|
mapCtx.save();
|
|
3391
2234
|
mapCtx.font = Math.max(8, Math.min(11, size * 0.38)) + 'px -apple-system,sans-serif';
|
|
@@ -3537,6 +2380,11 @@ document.getElementById('md-close').addEventListener('click', function() {
|
|
|
3537
2380
|
document.getElementById('btn-labels').addEventListener('click', function() {
|
|
3538
2381
|
mShowLabels = !mShowLabels; this.classList.toggle('active', mShowLabels); drawMap();
|
|
3539
2382
|
});
|
|
2383
|
+
document.getElementById('btn-all-labels').addEventListener('click', function() {
|
|
2384
|
+
mShowAllLabels = !mShowAllLabels; this.classList.toggle('active', mShowAllLabels);
|
|
2385
|
+
if (mShowAllLabels && !mShowLabels) { mShowLabels = true; document.getElementById('btn-labels').classList.add('active'); }
|
|
2386
|
+
drawMap();
|
|
2387
|
+
});
|
|
3540
2388
|
document.getElementById('btn-quality').addEventListener('click', function() {
|
|
3541
2389
|
mShowQuality = !mShowQuality; this.classList.toggle('active', mShowQuality); drawMap();
|
|
3542
2390
|
});
|
|
@@ -3891,12 +2739,44 @@ buildTopoList();
|
|
|
3891
2739
|
</body>
|
|
3892
2740
|
</html>`;
|
|
3893
2741
|
}
|
|
2742
|
+
function exportJGF(nodes, edges) {
|
|
2743
|
+
const jgf = {
|
|
2744
|
+
graph: {
|
|
2745
|
+
directed: true,
|
|
2746
|
+
type: "cartography",
|
|
2747
|
+
label: "Infrastructure Map",
|
|
2748
|
+
metadata: { exportedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2749
|
+
nodes: Object.fromEntries(nodes.map((n) => [n.id, {
|
|
2750
|
+
label: n.name,
|
|
2751
|
+
metadata: {
|
|
2752
|
+
type: n.type,
|
|
2753
|
+
layer: nodeLayer(n.type),
|
|
2754
|
+
confidence: n.confidence,
|
|
2755
|
+
discoveredVia: n.discoveredVia,
|
|
2756
|
+
discoveredAt: n.discoveredAt,
|
|
2757
|
+
tags: n.tags,
|
|
2758
|
+
metadata: n.metadata
|
|
2759
|
+
}
|
|
2760
|
+
}])),
|
|
2761
|
+
edges: edges.map((e) => ({
|
|
2762
|
+
source: e.sourceId,
|
|
2763
|
+
target: e.targetId,
|
|
2764
|
+
relation: e.relationship,
|
|
2765
|
+
metadata: { confidence: e.confidence, evidence: e.evidence }
|
|
2766
|
+
}))
|
|
2767
|
+
}
|
|
2768
|
+
};
|
|
2769
|
+
return JSON.stringify(jgf, null, 2);
|
|
2770
|
+
}
|
|
3894
2771
|
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "discovery", "sops"]) {
|
|
3895
2772
|
mkdirSync2(outputDir, { recursive: true });
|
|
3896
2773
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
3897
2774
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
3898
2775
|
const nodes = db.getNodes(sessionId);
|
|
3899
2776
|
const edges = db.getEdges(sessionId);
|
|
2777
|
+
const jgfPath = join2(outputDir, "cartography-graph.jgf.json");
|
|
2778
|
+
writeFileSync(jgfPath, exportJGF(nodes, edges));
|
|
2779
|
+
process.stderr.write("\u2713 cartography-graph.jgf.json\n");
|
|
3900
2780
|
if (formats.includes("mermaid")) {
|
|
3901
2781
|
writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
3902
2782
|
writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
@@ -3910,15 +2790,7 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
3910
2790
|
writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
3911
2791
|
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
3912
2792
|
}
|
|
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")) {
|
|
2793
|
+
if (formats.includes("html") || formats.includes("map") || formats.includes("discovery")) {
|
|
3922
2794
|
writeFileSync(join2(outputDir, "discovery.html"), exportDiscoveryApp(nodes, edges));
|
|
3923
2795
|
process.stderr.write("\u2713 discovery.html\n");
|
|
3924
2796
|
}
|
|
@@ -3938,10 +2810,9 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
3938
2810
|
}
|
|
3939
2811
|
|
|
3940
2812
|
// src/cli.ts
|
|
3941
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2
|
|
2813
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
3942
2814
|
import { resolve } from "path";
|
|
3943
2815
|
import { createInterface } from "readline";
|
|
3944
|
-
import { execSync as execSync2 } from "child_process";
|
|
3945
2816
|
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
3946
2817
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
3947
2818
|
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
@@ -4165,32 +3036,12 @@ function main() {
|
|
|
4165
3036
|
}
|
|
4166
3037
|
exportAll(db, sessionId, config.outputDir);
|
|
4167
3038
|
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
4168
|
-
const htmlPath = resolve(config.outputDir, "topology.html");
|
|
4169
|
-
const mapPath = resolve(config.outputDir, "cartography-map.html");
|
|
4170
3039
|
const discoveryPath = resolve(config.outputDir, "discovery.html");
|
|
4171
|
-
const topoPath = resolve(config.outputDir, "topology.mermaid");
|
|
4172
3040
|
w("\n");
|
|
4173
3041
|
if (existsSync2(discoveryPath)) {
|
|
4174
|
-
w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("Open discovery.html"))} ${dim("\u2190
|
|
3042
|
+
w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("Open discovery.html"))} ${dim("\u2190 Map + Topology")}
|
|
4175
3043
|
`);
|
|
4176
3044
|
}
|
|
4177
|
-
if (existsSync2(mapPath)) {
|
|
4178
|
-
w(` ${green("\u2192")} ${osc8(`file://${mapPath}`, bold("Open cartography-map.html"))} ${dim("\u2190 Hex Map")}
|
|
4179
|
-
`);
|
|
4180
|
-
}
|
|
4181
|
-
if (existsSync2(htmlPath)) {
|
|
4182
|
-
w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
|
|
4183
|
-
`);
|
|
4184
|
-
}
|
|
4185
|
-
if (existsSync2(topoPath)) {
|
|
4186
|
-
try {
|
|
4187
|
-
const code = readFileSync2(topoPath, "utf8");
|
|
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"))}
|
|
4190
|
-
`);
|
|
4191
|
-
} catch {
|
|
4192
|
-
}
|
|
4193
|
-
}
|
|
4194
3045
|
w("\n");
|
|
4195
3046
|
if (process.stdin.isTTY) {
|
|
4196
3047
|
w(dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
@@ -4232,8 +3083,8 @@ function main() {
|
|
|
4232
3083
|
`);
|
|
4233
3084
|
w("\n");
|
|
4234
3085
|
exportAll(db, sessionId, config.outputDir);
|
|
4235
|
-
if (existsSync2(
|
|
4236
|
-
w(` ${green("\u2192")} ${osc8(`file://${
|
|
3086
|
+
if (existsSync2(discoveryPath)) {
|
|
3087
|
+
w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("discovery.html updated"))}
|
|
4237
3088
|
`);
|
|
4238
3089
|
}
|
|
4239
3090
|
w("\n");
|
|
@@ -4257,37 +3108,6 @@ function main() {
|
|
|
4257
3108
|
`);
|
|
4258
3109
|
db.close();
|
|
4259
3110
|
});
|
|
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) => {
|
|
4261
|
-
const config = defaultConfig({ outputDir: opts.output });
|
|
4262
|
-
const db = new CartographyDB(config.dbPath);
|
|
4263
|
-
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
4264
|
-
if (!session) {
|
|
4265
|
-
process.stderr.write("No session found. Run discover first.\n");
|
|
4266
|
-
db.close();
|
|
4267
|
-
process.exitCode = 1;
|
|
4268
|
-
return;
|
|
4269
|
-
}
|
|
4270
|
-
const nodes = db.getNodes(session.id);
|
|
4271
|
-
const edges = db.getEdges(session.id);
|
|
4272
|
-
const outDir = resolve(opts.output);
|
|
4273
|
-
mkdirSync3(outDir, { recursive: true });
|
|
4274
|
-
const outPath = resolve(outDir, "cartography-map.html");
|
|
4275
|
-
writeFileSync2(outPath, exportCartographyMap(nodes, edges, { theme: opts.theme }));
|
|
4276
|
-
db.close();
|
|
4277
|
-
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
4278
|
-
const fileUrl = `file://${outPath}`;
|
|
4279
|
-
process.stderr.write(`
|
|
4280
|
-
${green("OK")} ${osc8(fileUrl, bold("Open cartography-map.html"))}
|
|
4281
|
-
`);
|
|
4282
|
-
process.stderr.write(` ${dim(fileUrl)}
|
|
4283
|
-
|
|
4284
|
-
`);
|
|
4285
|
-
try {
|
|
4286
|
-
const cmd = process.platform === "darwin" ? `open "${outPath}"` : process.platform === "win32" ? `start "" "${outPath}"` : `xdg-open "${outPath}"`;
|
|
4287
|
-
execSync2(cmd, { stdio: "ignore" });
|
|
4288
|
-
} catch {
|
|
4289
|
-
}
|
|
4290
|
-
});
|
|
4291
3111
|
program.command("show [session-id]").description("Show session details").action((sessionId) => {
|
|
4292
3112
|
const config = defaultConfig();
|
|
4293
3113
|
const db = new CartographyDB(config.dbPath);
|
|
@@ -4531,8 +3351,6 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
4531
3351
|
out(dim(" topology.mermaid Infrastructure topology (graph TB)\n"));
|
|
4532
3352
|
out(dim(" dependencies.mermaid Service dependencies (graph LR)\n"));
|
|
4533
3353
|
out(dim(" discovery.html Enterprise discovery frontend (Map + Topology)\n"));
|
|
4534
|
-
out(dim(" topology.html Interactive D3.js force graph\n"));
|
|
4535
|
-
out(dim(" cartography-map.html Hex grid data cartography map\n"));
|
|
4536
3354
|
out(dim(" sops/ Generated SOPs as Markdown\n"));
|
|
4537
3355
|
out(dim(" workflows/ Workflow flowcharts as Mermaid\n"));
|
|
4538
3356
|
out("\n");
|
|
@@ -4749,7 +3567,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
4749
3567
|
`);
|
|
4750
3568
|
});
|
|
4751
3569
|
program.command("doctor").description("Check all requirements and cloud CLIs").action(async () => {
|
|
4752
|
-
const { execSync:
|
|
3570
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
4753
3571
|
const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
|
|
4754
3572
|
const { join: join3 } = await import("path");
|
|
4755
3573
|
const out = (s) => process.stdout.write(s);
|
|
@@ -4772,7 +3590,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
4772
3590
|
allGood = false;
|
|
4773
3591
|
}
|
|
4774
3592
|
try {
|
|
4775
|
-
const v =
|
|
3593
|
+
const v = execSync2("claude --version", { stdio: "pipe" }).toString().trim();
|
|
4776
3594
|
ok(`Claude CLI ${dim2(v)}`);
|
|
4777
3595
|
} catch {
|
|
4778
3596
|
err("Claude CLI not found \u2014 npm i -g @anthropic-ai/claude-code");
|
|
@@ -4796,7 +3614,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
4796
3614
|
allGood = false;
|
|
4797
3615
|
}
|
|
4798
3616
|
try {
|
|
4799
|
-
const v =
|
|
3617
|
+
const v = execSync2("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
|
|
4800
3618
|
ok(`kubectl ${dim2(v || "(client OK)")}`);
|
|
4801
3619
|
} catch {
|
|
4802
3620
|
warn(`kubectl not found ${dim2("\u2014 install: https://kubernetes.io/docs/tasks/tools/")}`);
|
|
@@ -4808,7 +3626,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
4808
3626
|
];
|
|
4809
3627
|
for (const [name, cmd, hint] of cloudClis) {
|
|
4810
3628
|
try {
|
|
4811
|
-
|
|
3629
|
+
execSync2(cmd, { stdio: "pipe" });
|
|
4812
3630
|
ok(`${name} ${dim2("(cloud scanning available)")}`);
|
|
4813
3631
|
} catch {
|
|
4814
3632
|
warn(`${name} not found ${dim2("\u2014 cloud scan skipped | " + hint)}`);
|
|
@@ -4820,7 +3638,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
4820
3638
|
];
|
|
4821
3639
|
for (const [name, cmd] of localTools) {
|
|
4822
3640
|
try {
|
|
4823
|
-
|
|
3641
|
+
execSync2(cmd, { stdio: "pipe" });
|
|
4824
3642
|
ok(`${name} ${dim2("(discovery tool)")}`);
|
|
4825
3643
|
} catch {
|
|
4826
3644
|
warn(`${name} not found ${dim2("\u2014 discovery without " + name + " will be limited")}`);
|