@datasynx/agentic-ai-cartography 0.5.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 +1045 -1136
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1022 -1065
- 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,12 +1651,32 @@ function exportSOPMarkdown(sop) {
|
|
|
2170
1651
|
}
|
|
2171
1652
|
return lines.join("\n");
|
|
2172
1653
|
}
|
|
2173
|
-
function
|
|
2174
|
-
const
|
|
2175
|
-
const
|
|
1654
|
+
function exportDiscoveryApp(nodes, edges, options) {
|
|
1655
|
+
const theme = options?.theme ?? "dark";
|
|
1656
|
+
const graphData = JSON.stringify({
|
|
1657
|
+
nodes: nodes.map((n) => ({
|
|
1658
|
+
id: n.id,
|
|
1659
|
+
name: n.name,
|
|
1660
|
+
type: n.type,
|
|
1661
|
+
layer: nodeLayer(n.type),
|
|
1662
|
+
confidence: n.confidence,
|
|
1663
|
+
discoveredVia: n.discoveredVia,
|
|
1664
|
+
discoveredAt: n.discoveredAt,
|
|
1665
|
+
tags: n.tags,
|
|
1666
|
+
metadata: n.metadata
|
|
1667
|
+
})),
|
|
1668
|
+
links: edges.map((e) => ({
|
|
1669
|
+
source: e.sourceId,
|
|
1670
|
+
target: e.targetId,
|
|
1671
|
+
relationship: e.relationship,
|
|
1672
|
+
confidence: e.confidence,
|
|
1673
|
+
evidence: e.evidence
|
|
1674
|
+
}))
|
|
1675
|
+
});
|
|
1676
|
+
const { assets, clusters, connections } = buildMapData(nodes, edges, { theme });
|
|
2176
1677
|
const isEmpty = assets.length === 0;
|
|
2177
1678
|
const HEX_SIZE2 = 24;
|
|
2178
|
-
const
|
|
1679
|
+
const mapJson = JSON.stringify({
|
|
2179
1680
|
assets: assets.map((a) => ({
|
|
2180
1681
|
id: a.id,
|
|
2181
1682
|
name: a.name,
|
|
@@ -2201,621 +1702,1081 @@ function exportCartographyMap(nodes, edges, options) {
|
|
|
2201
1702
|
type: c.type ?? "connection"
|
|
2202
1703
|
}))
|
|
2203
1704
|
});
|
|
1705
|
+
const nodeCount = nodes.length;
|
|
1706
|
+
const edgeCount = edges.length;
|
|
1707
|
+
const assetCount = assets.length;
|
|
1708
|
+
const clusterCount = clusters.length;
|
|
2204
1709
|
return `<!DOCTYPE html>
|
|
2205
|
-
<html lang="en">
|
|
1710
|
+
<html lang="en" data-theme="${theme}">
|
|
2206
1711
|
<head>
|
|
2207
1712
|
<meta charset="UTF-8"/>
|
|
2208
|
-
<meta name="viewport" content="width=device-width,
|
|
2209
|
-
<title>
|
|
1713
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
1714
|
+
<title>Cartography \u2014 Datasynx Discovery</title>
|
|
1715
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
2210
1716
|
<style>
|
|
1717
|
+
/* \u2500\u2500 CSS Custom Properties \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1718
|
+
:root{
|
|
1719
|
+
--bg-base:#0f172a;--bg-surface:#1e293b;--bg-elevated:#273148;
|
|
1720
|
+
--border:#334155;--border-dim:#1e293b;
|
|
1721
|
+
--text:#e2e8f0;--text-muted:#94a3b8;--text-dim:#475569;
|
|
1722
|
+
--accent:#3b82f6;--accent-hover:#2563eb;--accent-dim:rgba(59,130,246,.12);
|
|
1723
|
+
}
|
|
1724
|
+
[data-theme="light"]{
|
|
1725
|
+
--bg-base:#f8fafc;--bg-surface:#ffffff;--bg-elevated:#f1f5f9;
|
|
1726
|
+
--border:#e2e8f0;--border-dim:#f1f5f9;
|
|
1727
|
+
--text:#0f172a;--text-muted:#64748b;--text-dim:#94a3b8;
|
|
1728
|
+
--accent:#2563eb;--accent-hover:#1d4ed8;--accent-dim:rgba(37,99,235,.08);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/* \u2500\u2500 Reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2211
1732
|
*{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
|
|
1733
|
+
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',sans-serif}
|
|
1734
|
+
body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--text)}
|
|
1735
|
+
|
|
1736
|
+
/* \u2500\u2500 Topbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2214
1737
|
#topbar{
|
|
2215
|
-
height:
|
|
2216
|
-
background
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
1738
|
+
height:56px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
1739
|
+
background:var(--bg-surface);border-bottom:1px solid var(--border);z-index:100;flex-shrink:0;
|
|
1740
|
+
}
|
|
1741
|
+
.tb-left{display:flex;align-items:center;gap:10px}
|
|
1742
|
+
.brand-logo{flex-shrink:0}
|
|
1743
|
+
.brand-name{font-size:15px;font-weight:700;color:var(--accent);letter-spacing:-.02em}
|
|
1744
|
+
.brand-product{font-size:14px;font-weight:500;color:var(--text-muted);margin-left:2px}
|
|
1745
|
+
.brand-sep{width:1px;height:24px;background:var(--border);margin:0 6px}
|
|
1746
|
+
.tb-center{display:flex;align-items:center;gap:2px;margin-left:auto;
|
|
1747
|
+
background:var(--bg-elevated);border-radius:8px;padding:3px}
|
|
1748
|
+
.tab-btn{
|
|
1749
|
+
padding:6px 16px;border:none;border-radius:6px;font-size:13px;font-weight:500;
|
|
1750
|
+
cursor:pointer;color:var(--text-muted);background:transparent;font-family:inherit;
|
|
1751
|
+
transition:all .15s;
|
|
1752
|
+
}
|
|
1753
|
+
.tab-btn:hover{color:var(--text)}
|
|
1754
|
+
.tab-btn.active{background:var(--accent);color:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2)}
|
|
1755
|
+
.tb-right{display:flex;align-items:center;gap:8px;margin-left:auto}
|
|
1756
|
+
.tb-search{
|
|
1757
|
+
display:flex;align-items:center;gap:6px;background:var(--bg-elevated);
|
|
1758
|
+
border:1px solid var(--border);border-radius:8px;padding:5px 10px;
|
|
1759
|
+
}
|
|
1760
|
+
.tb-search input{
|
|
1761
|
+
border:none;background:transparent;font-size:13px;outline:none;width:160px;
|
|
1762
|
+
color:var(--text);font-family:inherit;
|
|
1763
|
+
}
|
|
1764
|
+
.tb-search input::placeholder{color:var(--text-dim)}
|
|
1765
|
+
.tb-search svg{flex-shrink:0;color:var(--text-dim)}
|
|
1766
|
+
.icon-btn{
|
|
1767
|
+
width:36px;height:36px;border-radius:8px;border:1px solid var(--border);
|
|
1768
|
+
background:var(--bg-surface);cursor:pointer;display:flex;align-items:center;
|
|
1769
|
+
justify-content:center;color:var(--text-muted);text-decoration:none;transition:all .15s;font-size:16px;
|
|
1770
|
+
}
|
|
1771
|
+
.icon-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
|
|
1772
|
+
.tb-stats{font-size:11px;color:var(--text-dim);white-space:nowrap}
|
|
1773
|
+
|
|
1774
|
+
/* \u2500\u2500 Views \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1775
|
+
.view{flex:1;display:none;overflow:hidden;position:relative}
|
|
1776
|
+
.view.active{display:flex}
|
|
1777
|
+
|
|
1778
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1779
|
+
MAP VIEW
|
|
1780
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
1781
|
+
#map-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
1782
|
+
#map-wrap.dragging{cursor:grabbing}
|
|
1783
|
+
#map-wrap.connecting{cursor:crosshair}
|
|
1784
|
+
#map-wrap canvas{display:block;width:100%;height:100%}
|
|
1785
|
+
#map-detail{
|
|
1786
|
+
width:280px;background:var(--bg-surface);border-left:1px solid var(--border);
|
|
2235
1787
|
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2236
1788
|
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2237
1789
|
}
|
|
2238
|
-
#detail
|
|
2239
|
-
#detail
|
|
2240
|
-
padding:16px;border-bottom:1px solid
|
|
1790
|
+
#map-detail.open{transform:translateX(0)}
|
|
1791
|
+
#map-detail .panel-header{
|
|
1792
|
+
padding:16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;
|
|
2241
1793
|
}
|
|
2242
|
-
#detail
|
|
2243
|
-
|
|
1794
|
+
#map-detail .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
1795
|
+
.close-btn{
|
|
2244
1796
|
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2245
|
-
color
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
}
|
|
2258
|
-
.tb-
|
|
2259
|
-
width:40px;height:40px;border-radius:10px;border:1px solid
|
|
2260
|
-
background
|
|
1797
|
+
color:var(--text-muted);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
1798
|
+
}
|
|
1799
|
+
.close-btn:hover{background:var(--bg-elevated)}
|
|
1800
|
+
.panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
1801
|
+
.meta-row{display:flex;flex-direction:column;gap:3px}
|
|
1802
|
+
.meta-label{font-size:11px;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
|
|
1803
|
+
.meta-value{font-size:13px;word-break:break-all}
|
|
1804
|
+
.quality-bar{height:6px;border-radius:3px;background:var(--bg-elevated);margin-top:4px}
|
|
1805
|
+
.quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
1806
|
+
|
|
1807
|
+
/* Map toolbars */
|
|
1808
|
+
#map-tb-left{position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10}
|
|
1809
|
+
#map-tb-right{position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;align-items:flex-end;gap:8px;z-index:10}
|
|
1810
|
+
.tb-tool{
|
|
1811
|
+
width:40px;height:40px;border-radius:10px;border:1px solid var(--border);
|
|
1812
|
+
background:var(--bg-surface);box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2261
1813
|
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2262
|
-
transition:all .15s;color:
|
|
1814
|
+
transition:all .15s;color:var(--text);font-family:-apple-system,sans-serif;
|
|
2263
1815
|
}
|
|
2264
|
-
|
|
2265
|
-
.tb-
|
|
2266
|
-
|
|
2267
|
-
|
|
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}
|
|
1816
|
+
#btn-all-labels{font-size:14px;font-weight:700;letter-spacing:-.02em}
|
|
1817
|
+
.tb-tool:hover{border-color:var(--text-muted)}
|
|
1818
|
+
.tb-tool.active{background:var(--accent-dim);border-color:var(--accent)}
|
|
1819
|
+
.map-zoom{display:flex;align-items:center;gap:6px}
|
|
2272
1820
|
.zoom-btn{
|
|
2273
|
-
width:34px;height:34px;border-radius:8px;border:1px solid
|
|
2274
|
-
background
|
|
2275
|
-
|
|
2276
|
-
}
|
|
2277
|
-
.zoom-btn:hover{background
|
|
2278
|
-
#zoom-pct{font-size:12px;font-weight:500;color
|
|
2279
|
-
#
|
|
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{
|
|
1821
|
+
width:34px;height:34px;border-radius:8px;border:1px solid var(--border);
|
|
1822
|
+
background:var(--bg-surface);cursor:pointer;font-size:18px;color:var(--text);
|
|
1823
|
+
display:flex;align-items:center;justify-content:center;
|
|
1824
|
+
}
|
|
1825
|
+
.zoom-btn:hover{background:var(--bg-elevated)}
|
|
1826
|
+
#map-zoom-pct{font-size:12px;font-weight:500;color:var(--text-dim);min-width:38px;text-align:center}
|
|
1827
|
+
#map-connect-hint{
|
|
2327
1828
|
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2328
1829
|
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2329
1830
|
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2330
1831
|
display:none;z-index:20;pointer-events:none;
|
|
2331
1832
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
1833
|
+
#map-tooltip{
|
|
1834
|
+
position:fixed;background:var(--bg-surface);color:var(--text);border-radius:8px;
|
|
1835
|
+
padding:8px 12px;font-size:12px;pointer-events:none;z-index:200;
|
|
1836
|
+
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.25);border:1px solid var(--border);
|
|
1837
|
+
}
|
|
1838
|
+
#map-tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
1839
|
+
#map-tooltip .tt-domain{color:var(--text-muted);font-size:11px}
|
|
1840
|
+
#map-tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
1841
|
+
#map-empty{
|
|
1842
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
1843
|
+
align-items:center;justify-content:center;gap:12px;color:var(--text-muted);
|
|
1844
|
+
}
|
|
1845
|
+
#map-empty p{font-size:14px}
|
|
1846
|
+
|
|
1847
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1848
|
+
TOPOLOGY VIEW
|
|
1849
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
1850
|
+
#topo-panel{
|
|
1851
|
+
width:220px;min-width:220px;height:100%;overflow:hidden;
|
|
1852
|
+
background:var(--bg-surface);border-right:1px solid var(--border);
|
|
1853
|
+
display:flex;flex-direction:column;
|
|
1854
|
+
}
|
|
1855
|
+
#topo-panel-header{
|
|
1856
|
+
padding:10px 12px 8px;border-bottom:1px solid var(--border);
|
|
1857
|
+
font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.6px;
|
|
1858
|
+
}
|
|
1859
|
+
#topo-search{
|
|
1860
|
+
width:calc(100% - 16px);margin:8px;padding:5px 8px;
|
|
1861
|
+
background:var(--bg-elevated);border:1px solid var(--border);border-radius:5px;
|
|
1862
|
+
color:var(--text);font-size:11px;font-family:inherit;outline:none;
|
|
1863
|
+
}
|
|
1864
|
+
#topo-search:focus{border-color:var(--accent)}
|
|
1865
|
+
#topo-list{flex:1;overflow-y:auto;padding-bottom:8px}
|
|
1866
|
+
.topo-item{
|
|
1867
|
+
padding:5px 12px;cursor:pointer;font-size:11px;
|
|
1868
|
+
display:flex;align-items:center;gap:6px;border-left:2px solid transparent;
|
|
1869
|
+
}
|
|
1870
|
+
.topo-item:hover{background:var(--bg-elevated)}
|
|
1871
|
+
.topo-item.active{background:var(--accent-dim);border-left-color:var(--accent)}
|
|
1872
|
+
.topo-dot{width:7px;height:7px;border-radius:2px;flex-shrink:0}
|
|
1873
|
+
.topo-name{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
1874
|
+
.topo-type{color:var(--text-dim);font-size:9px;flex-shrink:0}
|
|
1875
|
+
|
|
1876
|
+
#topo-graph{flex:1;height:100%;position:relative}
|
|
1877
|
+
#topo-graph svg{width:100%;height:100%}
|
|
1878
|
+
.hull{opacity:.12;stroke-width:1.5;stroke-opacity:.25}
|
|
1879
|
+
.hull-label{font-size:13px;font-weight:700;letter-spacing:1px;text-transform:uppercase;fill-opacity:.5;pointer-events:none}
|
|
1880
|
+
.link{stroke-opacity:.4}
|
|
1881
|
+
.link-label{font-size:8px;fill:var(--text-dim);pointer-events:none;opacity:0}
|
|
1882
|
+
.node-hex{stroke-width:1.8;cursor:pointer;transition:opacity .15s}
|
|
1883
|
+
.node-hex:hover{filter:brightness(1.3);stroke-width:3}
|
|
1884
|
+
.node-hex.selected{stroke-width:3.5;filter:brightness(1.5)}
|
|
1885
|
+
.node-label{font-size:10px;fill:var(--text);pointer-events:none;opacity:0}
|
|
1886
|
+
|
|
1887
|
+
#topo-sidebar{
|
|
1888
|
+
width:300px;min-width:300px;height:100%;overflow-y:auto;
|
|
1889
|
+
background:var(--bg-surface);border-left:1px solid var(--border);
|
|
1890
|
+
padding:16px;font-size:12px;line-height:1.6;
|
|
1891
|
+
}
|
|
1892
|
+
#topo-sidebar h2{margin:0 0 8px;font-size:14px;color:var(--accent)}
|
|
1893
|
+
#topo-sidebar .meta-table{width:100%;border-collapse:collapse}
|
|
1894
|
+
#topo-sidebar .meta-table td{padding:3px 6px;border-bottom:1px solid var(--border-dim);vertical-align:top}
|
|
1895
|
+
#topo-sidebar .meta-table td:first-child{color:var(--text-dim);white-space:nowrap;width:90px}
|
|
1896
|
+
#topo-sidebar .tag{display:inline-block;background:var(--bg-elevated);border-radius:3px;padding:1px 5px;margin:1px;font-size:10px}
|
|
1897
|
+
#topo-sidebar .conf-bar{height:5px;border-radius:3px;background:var(--bg-elevated);margin-top:3px}
|
|
1898
|
+
#topo-sidebar .conf-fill{height:100%;border-radius:3px}
|
|
1899
|
+
#topo-sidebar .edges-list{margin-top:12px}
|
|
1900
|
+
#topo-sidebar .edge-item{padding:4px 0;border-bottom:1px solid var(--border-dim);color:var(--text-dim);font-size:11px}
|
|
1901
|
+
#topo-sidebar .edge-item span{color:var(--text)}
|
|
1902
|
+
.hint{color:var(--text-dim);font-size:11px;margin-top:8px}
|
|
1903
|
+
|
|
1904
|
+
#topo-hud{
|
|
1905
|
+
position:absolute;top:10px;left:10px;background:rgba(15,23,42,.88);
|
|
1906
|
+
padding:10px 14px;border-radius:8px;font-size:12px;border:1px solid var(--border);pointer-events:none;
|
|
1907
|
+
}
|
|
1908
|
+
#topo-hud strong{color:var(--accent)}
|
|
1909
|
+
#topo-hud .stats{color:var(--text-dim)}
|
|
1910
|
+
#topo-hud .zoom-level{color:var(--text-dim);font-size:10px;margin-top:2px}
|
|
1911
|
+
|
|
1912
|
+
#topo-toolbar{position:absolute;top:10px;right:10px;display:flex;flex-wrap:wrap;gap:4px;pointer-events:auto;align-items:center}
|
|
1913
|
+
.filter-btn{
|
|
1914
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
1915
|
+
color:var(--text);padding:4px 10px;font-size:11px;cursor:pointer;
|
|
1916
|
+
font-family:inherit;display:flex;align-items:center;gap:5px;
|
|
1917
|
+
}
|
|
1918
|
+
.filter-btn:hover{border-color:var(--text-dim)}
|
|
1919
|
+
.filter-btn.off{opacity:.35}
|
|
1920
|
+
.filter-dot{width:8px;height:8px;border-radius:2px;display:inline-block}
|
|
1921
|
+
.export-btn{
|
|
1922
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
1923
|
+
color:var(--accent);padding:4px 12px;font-size:11px;cursor:pointer;font-family:inherit;
|
|
1924
|
+
}
|
|
1925
|
+
.export-btn:hover{border-color:var(--accent);background:var(--accent-dim)}
|
|
2334
1926
|
</style>
|
|
2335
1927
|
</head>
|
|
2336
|
-
<body
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
1928
|
+
<body>
|
|
1929
|
+
|
|
1930
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1931
|
+
TOPBAR
|
|
1932
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
1933
|
+
<header id="topbar">
|
|
1934
|
+
<div class="tb-left">
|
|
1935
|
+
<svg class="brand-logo" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
1936
|
+
<path d="M16 1.5L29.5 8.75V23.25L16 30.5L2.5 23.25V8.75L16 1.5Z" fill="#0F2347" stroke="#2563EB" stroke-width="1.2"/>
|
|
1937
|
+
<circle cx="10" cy="16" r="2.8" fill="#60A5FA"/><circle cx="22" cy="10.5" r="2.2" fill="#38BDF8"/>
|
|
1938
|
+
<circle cx="22" cy="21.5" r="2.2" fill="#38BDF8"/>
|
|
1939
|
+
<line x1="12.5" y1="14.8" x2="19.8" y2="11.2" stroke="#93C5FD" stroke-width="1.2"/>
|
|
1940
|
+
<line x1="12.5" y1="17.2" x2="19.8" y2="20.8" stroke="#93C5FD" stroke-width="1.2"/>
|
|
1941
|
+
<line x1="22" y1="12.7" x2="22" y2="19.3" stroke="#93C5FD" stroke-width="1" stroke-dasharray="2 1.5"/>
|
|
1942
|
+
</svg>
|
|
1943
|
+
<span class="brand-name">datasynx</span>
|
|
1944
|
+
<span class="brand-sep"></span>
|
|
1945
|
+
<span class="brand-product">Cartography</span>
|
|
2343
1946
|
</div>
|
|
2344
|
-
<
|
|
2345
|
-
</
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
</
|
|
2350
|
-
|
|
2351
|
-
<
|
|
2352
|
-
|
|
1947
|
+
<div class="tb-center">
|
|
1948
|
+
<button class="tab-btn active" id="tab-map-btn" data-tab="map">Map</button>
|
|
1949
|
+
<button class="tab-btn" id="tab-topo-btn" data-tab="topo">Topology</button>
|
|
1950
|
+
</div>
|
|
1951
|
+
<div class="tb-right">
|
|
1952
|
+
<span class="tb-stats">${nodeCount} nodes · ${edgeCount} edges</span>
|
|
1953
|
+
<div class="tb-search">
|
|
1954
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1955
|
+
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
|
|
1956
|
+
</svg>
|
|
1957
|
+
<input id="global-search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false"/>
|
|
1958
|
+
</div>
|
|
1959
|
+
<a href="https://www.linkedin.com/company/datasynx-ai/" target="_blank" rel="noopener noreferrer"
|
|
1960
|
+
class="icon-btn" title="Datasynx on LinkedIn" aria-label="LinkedIn">
|
|
1961
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
|
1962
|
+
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
|
1963
|
+
</svg>
|
|
1964
|
+
</a>
|
|
1965
|
+
<button id="theme-btn" class="icon-btn" title="Toggle theme" aria-label="Toggle theme">
|
|
1966
|
+
${theme === "dark" ? "☼" : "☾"}
|
|
1967
|
+
</button>
|
|
1968
|
+
</div>
|
|
1969
|
+
</header>
|
|
1970
|
+
|
|
1971
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1972
|
+
MAP VIEW
|
|
1973
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
1974
|
+
<div id="view-map" class="view active">
|
|
1975
|
+
<div id="map-wrap" tabindex="0" aria-label="Data cartography hex map">
|
|
2353
1976
|
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
2354
|
-
${isEmpty ? '<div id="empty
|
|
1977
|
+
${isEmpty ? '<div id="map-empty"><p style="font-size:48px">🗺</p><p>No data assets discovered yet</p><p style="font-size:12px">Run <code>datasynx-cartography discover</code> to populate the map</p></div>' : ""}
|
|
2355
1978
|
</div>
|
|
2356
|
-
<div id="detail
|
|
1979
|
+
<div id="map-detail">
|
|
2357
1980
|
<div class="panel-header">
|
|
2358
|
-
<h3 id="
|
|
2359
|
-
<button class="close-btn" id="
|
|
1981
|
+
<h3 id="md-name">—</h3>
|
|
1982
|
+
<button class="close-btn" id="md-close" aria-label="Close">✕</button>
|
|
2360
1983
|
</div>
|
|
2361
|
-
<div class="panel-body" id="
|
|
1984
|
+
<div class="panel-body" id="md-body"></div>
|
|
2362
1985
|
</div>
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
<
|
|
2366
|
-
|
|
2367
|
-
|
|
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>
|
|
1986
|
+
<div id="map-tb-left">
|
|
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>
|
|
1989
|
+
<button class="tb-tool" id="btn-quality" title="Quality layer">👁</button>
|
|
1990
|
+
<button class="tb-tool" id="btn-connect" title="Connection tool">🔗</button>
|
|
2375
1991
|
</div>
|
|
2376
|
-
<div id="
|
|
2377
|
-
<
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
1992
|
+
<div id="map-tb-right">
|
|
1993
|
+
<div class="map-zoom">
|
|
1994
|
+
<button class="zoom-btn" id="mz-out">−</button>
|
|
1995
|
+
<span id="map-zoom-pct">100%</span>
|
|
1996
|
+
<button class="zoom-btn" id="mz-in">+</button>
|
|
1997
|
+
</div>
|
|
2381
1998
|
</div>
|
|
2382
|
-
<
|
|
1999
|
+
<div id="map-connect-hint">Click two assets to create a connection</div>
|
|
2000
|
+
<div id="map-tooltip"><div class="tt-name" id="mtt-name"></div><div class="tt-domain" id="mtt-domain"></div><div class="tt-quality" id="mtt-quality"></div></div>
|
|
2383
2001
|
</div>
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
<div
|
|
2390
|
-
|
|
2002
|
+
|
|
2003
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2004
|
+
TOPOLOGY VIEW
|
|
2005
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
2006
|
+
<div id="view-topo" class="view">
|
|
2007
|
+
<div id="topo-panel">
|
|
2008
|
+
<div id="topo-panel-header">Nodes (${nodeCount})</div>
|
|
2009
|
+
<input id="topo-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false"/>
|
|
2010
|
+
<div id="topo-list"></div>
|
|
2011
|
+
</div>
|
|
2012
|
+
<div id="topo-graph">
|
|
2013
|
+
<div id="topo-hud">
|
|
2014
|
+
<strong>Topology</strong>
|
|
2015
|
+
<span class="stats">${nodeCount} nodes · ${edgeCount} edges</span><br/>
|
|
2016
|
+
<span class="zoom-level">Scroll = zoom · Drag = pan · Click = details</span>
|
|
2017
|
+
</div>
|
|
2018
|
+
<div id="topo-toolbar"></div>
|
|
2019
|
+
<svg></svg>
|
|
2020
|
+
</div>
|
|
2021
|
+
<div id="topo-sidebar">
|
|
2022
|
+
<h2>Infrastructure Map</h2>
|
|
2023
|
+
<p class="hint">Click a node to view details.</p>
|
|
2024
|
+
</div>
|
|
2391
2025
|
</div>
|
|
2392
2026
|
|
|
2393
2027
|
<script>
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
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
|
-
}
|
|
2028
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2029
|
+
// SHARED STATE
|
|
2030
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2031
|
+
let isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
2032
|
+
let currentTab = 'map';
|
|
2033
|
+
let topoInited = false;
|
|
2034
|
+
|
|
2035
|
+
// \u2500\u2500 Theme toggle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2036
|
+
document.getElementById('theme-btn').addEventListener('click', function() {
|
|
2037
|
+
isDark = !isDark;
|
|
2038
|
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
2039
|
+
this.innerHTML = isDark ? '\\u2606' : '\\u263E';
|
|
2040
|
+
if (typeof drawMap === 'function') drawMap();
|
|
2041
|
+
});
|
|
2528
2042
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2043
|
+
// \u2500\u2500 Tab switching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2044
|
+
document.querySelectorAll('.tab-btn').forEach(function(btn) {
|
|
2045
|
+
btn.addEventListener('click', function() {
|
|
2046
|
+
var tab = this.getAttribute('data-tab');
|
|
2047
|
+
if (tab === currentTab) return;
|
|
2048
|
+
currentTab = tab;
|
|
2049
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
2050
|
+
this.classList.add('active');
|
|
2051
|
+
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
2052
|
+
document.getElementById('view-' + tab).classList.add('active');
|
|
2053
|
+
if (tab === 'topo' && !topoInited) { initTopology(); topoInited = true; }
|
|
2054
|
+
if (tab === 'map' && typeof drawMap === 'function') { resizeMap(); }
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2533
2057
|
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2058
|
+
// \u2500\u2500 Global search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2059
|
+
document.getElementById('global-search').addEventListener('input', function(e) {
|
|
2060
|
+
var q = e.target.value.trim();
|
|
2061
|
+
if (typeof setMapSearch === 'function') setMapSearch(q);
|
|
2062
|
+
if (typeof setTopoSearch === 'function') setTopoSearch(q);
|
|
2063
|
+
});
|
|
2537
2064
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2065
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2066
|
+
// MAP VIEW
|
|
2067
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2068
|
+
var MAP = ${mapJson};
|
|
2069
|
+
var MAP_HEX = ${HEX_SIZE2};
|
|
2070
|
+
var MAP_EMPTY = ${isEmpty};
|
|
2071
|
+
|
|
2072
|
+
var mapAssetIndex = new Map();
|
|
2073
|
+
var mapClusterByAsset = new Map();
|
|
2074
|
+
for (var ci = 0; ci < MAP.clusters.length; ci++) {
|
|
2075
|
+
var c = MAP.clusters[ci];
|
|
2076
|
+
for (var ai = 0; ai < c.assetIds.length; ai++) mapClusterByAsset.set(c.assetIds[ai], c);
|
|
2077
|
+
}
|
|
2078
|
+
for (var ni = 0; ni < MAP.assets.length; ni++) mapAssetIndex.set(MAP.assets[ni].id, MAP.assets[ni]);
|
|
2079
|
+
|
|
2080
|
+
var mapCanvas = document.getElementById('hexmap');
|
|
2081
|
+
var mapCtx = mapCanvas.getContext('2d');
|
|
2082
|
+
var mapWrap = document.getElementById('map-wrap');
|
|
2083
|
+
var mW = 0, mH = 0;
|
|
2084
|
+
var mvx = 0, mvy = 0, mScale = 1;
|
|
2085
|
+
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false, mShowAllLabels = false;
|
|
2086
|
+
var mConnectMode = false, mConnectFirst = null;
|
|
2087
|
+
var mHoveredId = null, mSelectedId = null;
|
|
2088
|
+
var mSearchQuery = '';
|
|
2089
|
+
var mLocalConns = MAP.connections.slice();
|
|
2090
|
+
|
|
2091
|
+
function setMapSearch(q) { mSearchQuery = q; drawMap(); }
|
|
2092
|
+
|
|
2093
|
+
function resizeMap() {
|
|
2094
|
+
var dpr = window.devicePixelRatio || 1;
|
|
2095
|
+
mW = mapWrap.clientWidth; mH = mapWrap.clientHeight;
|
|
2096
|
+
mapCanvas.width = mW * dpr; mapCanvas.height = mH * dpr;
|
|
2097
|
+
mapCanvas.style.width = mW + 'px'; mapCanvas.style.height = mH + 'px';
|
|
2098
|
+
mapCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2099
|
+
drawMap();
|
|
2100
|
+
}
|
|
2101
|
+
window.addEventListener('resize', function() { if (currentTab === 'map') resizeMap(); });
|
|
2102
|
+
|
|
2103
|
+
function mHtp_x(q, r) { return MAP_HEX * (1.5 * q); }
|
|
2104
|
+
function mHtp_y(q, r) { return MAP_HEX * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r); }
|
|
2105
|
+
function mW2s(wx, wy) { return { x: wx * mScale + mvx, y: wy * mScale + mvy }; }
|
|
2106
|
+
function mS2w(sx, sy) { return { x: (sx - mvx) / mScale, y: (sy - mvy) / mScale }; }
|
|
2107
|
+
|
|
2108
|
+
function mapFitToView() {
|
|
2109
|
+
if (MAP_EMPTY || MAP.assets.length === 0) { mvx = 0; mvy = 0; mScale = 1; return; }
|
|
2110
|
+
var mnx = Infinity, mny = Infinity, mxx = -Infinity, mxy = -Infinity;
|
|
2111
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
2112
|
+
var a = MAP.assets[i], px = mHtp_x(a.q, a.r), py = mHtp_y(a.q, a.r);
|
|
2113
|
+
if (px < mnx) mnx = px; if (py < mny) mny = py; if (px > mxx) mxx = px; if (py > mxy) mxy = py;
|
|
2114
|
+
}
|
|
2115
|
+
var pw = mxx - mnx + MAP_HEX * 4, ph = mxy - mny + MAP_HEX * 4;
|
|
2116
|
+
mScale = Math.min(mW / pw, mH / ph, 2) * 0.85;
|
|
2117
|
+
mvx = mW / 2 - ((mnx + mxx) / 2) * mScale;
|
|
2118
|
+
mvy = mH / 2 - ((mny + mxy) / 2) * mScale;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function mHexPath(cx, cy, r) {
|
|
2122
|
+
mapCtx.beginPath();
|
|
2123
|
+
for (var i = 0; i < 6; i++) {
|
|
2124
|
+
var angle = Math.PI / 180 * (60 * i);
|
|
2125
|
+
var x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle);
|
|
2126
|
+
i === 0 ? mapCtx.moveTo(x, y) : mapCtx.lineTo(x, y);
|
|
2127
|
+
}
|
|
2128
|
+
mapCtx.closePath();
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function mShadeV(hex, amt) {
|
|
2132
|
+
if (!hex || hex.length < 7) return hex;
|
|
2133
|
+
var n = parseInt(hex.replace('#', ''), 16);
|
|
2134
|
+
var r = Math.min(255, (n >> 16) + amt), g = Math.min(255, ((n >> 8) & 0xff) + amt), b = Math.min(255, (n & 0xff) + amt);
|
|
2135
|
+
return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
function mGetSearchMatches() {
|
|
2139
|
+
if (!mSearchQuery) return new Set();
|
|
2140
|
+
var q = mSearchQuery.toLowerCase(), m = new Set();
|
|
2141
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
2142
|
+
var a = MAP.assets[i];
|
|
2143
|
+
if (a.name.toLowerCase().includes(q) || (a.domain && a.domain.toLowerCase().includes(q)) ||
|
|
2144
|
+
(a.subDomain && a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
2145
|
+
}
|
|
2146
|
+
return m;
|
|
2147
|
+
}
|
|
2542
2148
|
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2149
|
+
function mDrawPill(x, y, text, color, fontSize) {
|
|
2150
|
+
if (!text) return;
|
|
2151
|
+
mapCtx.save();
|
|
2152
|
+
mapCtx.font = '600 ' + fontSize + 'px -apple-system,sans-serif';
|
|
2153
|
+
var tw = mapCtx.measureText(text).width;
|
|
2154
|
+
var ph = fontSize + 8, pw = tw + 20;
|
|
2155
|
+
mapCtx.beginPath();
|
|
2156
|
+
if (mapCtx.roundRect) mapCtx.roundRect(x - pw / 2, y - ph / 2, pw, ph, ph / 2);
|
|
2157
|
+
else mapCtx.rect(x - pw / 2, y - ph / 2, pw, ph);
|
|
2158
|
+
mapCtx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
2159
|
+
mapCtx.shadowColor = 'rgba(0,0,0,0.15)'; mapCtx.shadowBlur = 6;
|
|
2160
|
+
mapCtx.fill(); mapCtx.shadowBlur = 0;
|
|
2161
|
+
mapCtx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
2162
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
2163
|
+
mapCtx.fillText(text, x, y);
|
|
2164
|
+
mapCtx.restore();
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function drawMap() {
|
|
2168
|
+
mapCtx.clearRect(0, 0, mW, mH);
|
|
2169
|
+
var bg = getComputedStyle(document.documentElement).getPropertyValue('--bg-base').trim();
|
|
2170
|
+
mapCtx.fillStyle = bg || (isDark ? '#0f172a' : '#f8fafc');
|
|
2171
|
+
mapCtx.fillRect(0, 0, mW, mH);
|
|
2172
|
+
if (MAP_EMPTY) return;
|
|
2173
|
+
|
|
2174
|
+
var size = MAP_HEX * mScale;
|
|
2175
|
+
var matchedIds = mGetSearchMatches();
|
|
2176
|
+
var hasSearch = mSearchQuery.length > 0;
|
|
2177
|
+
|
|
2178
|
+
// Connections
|
|
2179
|
+
mapCtx.save();
|
|
2180
|
+
mapCtx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
2181
|
+
mapCtx.lineWidth = 1.5; mapCtx.setLineDash([4, 4]);
|
|
2182
|
+
for (var ci = 0; ci < mLocalConns.length; ci++) {
|
|
2183
|
+
var conn = mLocalConns[ci];
|
|
2184
|
+
var src = mapAssetIndex.get(conn.sourceAssetId), tgt = mapAssetIndex.get(conn.targetAssetId);
|
|
2185
|
+
if (!src || !tgt) continue;
|
|
2186
|
+
var sp = mW2s(mHtp_x(src.q, src.r), mHtp_y(src.q, src.r));
|
|
2187
|
+
var tp = mW2s(mHtp_x(tgt.q, tgt.r), mHtp_y(tgt.q, tgt.r));
|
|
2188
|
+
mapCtx.beginPath(); mapCtx.moveTo(sp.x, sp.y); mapCtx.lineTo(tp.x, tp.y); mapCtx.stroke();
|
|
2189
|
+
}
|
|
2190
|
+
mapCtx.setLineDash([]); mapCtx.restore();
|
|
2191
|
+
|
|
2192
|
+
// Hexagons per cluster
|
|
2193
|
+
for (var cli = 0; cli < MAP.clusters.length; cli++) {
|
|
2194
|
+
var cluster = MAP.clusters[cli];
|
|
2195
|
+
var baseColor = cluster.color;
|
|
2196
|
+
var clusterAssets = cluster.assetIds.map(function(id) { return mapAssetIndex.get(id); }).filter(Boolean);
|
|
2197
|
+
var isClusterMatch = !hasSearch || clusterAssets.some(function(a) { return matchedIds.has(a.id); });
|
|
2198
|
+
var clusterDim = hasSearch && !isClusterMatch;
|
|
2199
|
+
|
|
2200
|
+
for (var ai = 0; ai < clusterAssets.length; ai++) {
|
|
2201
|
+
var asset = clusterAssets[ai];
|
|
2202
|
+
var wx = mHtp_x(asset.q, asset.r), wy = mHtp_y(asset.q, asset.r);
|
|
2203
|
+
var s = mW2s(wx, wy), cx = s.x, cy = s.y;
|
|
2204
|
+
if (cx + size < 0 || cx - size > mW || cy + size < 0 || cy - size > mH) continue;
|
|
2205
|
+
|
|
2206
|
+
var shade = ai % 3 === 0 ? 18 : ai % 3 === 1 ? 8 : 0;
|
|
2207
|
+
var fillColor = mShadeV(baseColor, shade);
|
|
2208
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
2209
|
+
if (asset.qualityScore < 40) fillColor = '#ef4444';
|
|
2210
|
+
else if (asset.qualityScore < 70) fillColor = '#f97316';
|
|
2558
2211
|
}
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2212
|
+
|
|
2213
|
+
var alpha = clusterDim ? 0.18 : 1;
|
|
2214
|
+
var isHov = asset.id === mHoveredId, isSel = asset.id === mSelectedId, isCF = asset.id === mConnectFirst;
|
|
2215
|
+
|
|
2216
|
+
mapCtx.save(); mapCtx.globalAlpha = alpha;
|
|
2217
|
+
mHexPath(cx, cy, size * 0.92);
|
|
2218
|
+
if (isDark && (isHov || isSel || isCF)) { mapCtx.shadowColor = fillColor; mapCtx.shadowBlur = isSel ? 16 : 8; }
|
|
2219
|
+
mapCtx.fillStyle = fillColor; mapCtx.fill();
|
|
2220
|
+
if (isSel || isCF) { mapCtx.strokeStyle = isCF ? '#f59e0b' : '#fff'; mapCtx.lineWidth = 2.5; mapCtx.stroke(); }
|
|
2221
|
+
else if (isHov) { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)'; mapCtx.lineWidth = 1.5; mapCtx.stroke(); }
|
|
2222
|
+
else { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)'; mapCtx.lineWidth = 1; mapCtx.stroke(); }
|
|
2223
|
+
mapCtx.restore();
|
|
2224
|
+
|
|
2225
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined && size > 8 && asset.qualityScore < 70) {
|
|
2226
|
+
mapCtx.beginPath(); mapCtx.arc(cx + size * 0.4, cy - size * 0.4, Math.max(3, size * 0.14), 0, Math.PI * 2);
|
|
2227
|
+
mapCtx.fillStyle = asset.qualityScore < 40 ? '#ef4444' : '#f97316'; mapCtx.fill();
|
|
2570
2228
|
}
|
|
2571
2229
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
(
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
ctx.textAlign='center';ctx.textBaseline='middle';
|
|
2581
|
-
ctx.fillText(label, cx, cy);
|
|
2582
|
-
ctx.restore();
|
|
2230
|
+
var showAssetLabel = mShowLabels && !clusterDim && (mShowAllLabels || (mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
2231
|
+
if (showAssetLabel && size > 6) {
|
|
2232
|
+
var label = asset.name.length > 12 ? asset.name.substring(0, 11) + '...' : asset.name;
|
|
2233
|
+
mapCtx.save();
|
|
2234
|
+
mapCtx.font = Math.max(8, Math.min(11, size * 0.38)) + 'px -apple-system,sans-serif';
|
|
2235
|
+
mapCtx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
2236
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
2237
|
+
mapCtx.fillText(label, cx, cy); mapCtx.restore();
|
|
2583
2238
|
}
|
|
2584
2239
|
}
|
|
2585
2240
|
}
|
|
2586
2241
|
|
|
2587
|
-
// Cluster labels
|
|
2588
|
-
if (
|
|
2589
|
-
for (
|
|
2590
|
-
|
|
2591
|
-
if (
|
|
2592
|
-
|
|
2593
|
-
|
|
2242
|
+
// Cluster labels
|
|
2243
|
+
if (mShowLabels && mDetailLevel >= 1) {
|
|
2244
|
+
for (var cli2 = 0; cli2 < MAP.clusters.length; cli2++) {
|
|
2245
|
+
var cl = MAP.clusters[cli2];
|
|
2246
|
+
if (cl.assetIds.length === 0) continue;
|
|
2247
|
+
if (hasSearch && !cl.assetIds.some(function(id) { return matchedIds.has(id); })) continue;
|
|
2248
|
+
var sc = mW2s(cl.centroid.x, cl.centroid.y);
|
|
2249
|
+
mDrawPill(sc.x, sc.y - size * 1.2, cl.label, cl.color, 14);
|
|
2594
2250
|
}
|
|
2595
2251
|
}
|
|
2596
2252
|
|
|
2597
|
-
// Sub-domain labels
|
|
2598
|
-
if (
|
|
2599
|
-
|
|
2600
|
-
for (
|
|
2601
|
-
|
|
2602
|
-
|
|
2253
|
+
// Sub-domain labels
|
|
2254
|
+
if (mShowLabels && mDetailLevel >= 2) {
|
|
2255
|
+
var subGroups = new Map();
|
|
2256
|
+
for (var si = 0; si < MAP.assets.length; si++) {
|
|
2257
|
+
var sa = MAP.assets[si];
|
|
2258
|
+
if (!sa.subDomain) continue;
|
|
2259
|
+
var key = sa.domain + '|' + sa.subDomain;
|
|
2603
2260
|
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
2604
|
-
subGroups.get(key).push(
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
for (
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
}
|
|
2261
|
+
subGroups.get(key).push(sa);
|
|
2262
|
+
}
|
|
2263
|
+
subGroups.forEach(function(group) {
|
|
2264
|
+
var sx = 0, sy = 0;
|
|
2265
|
+
for (var gi = 0; gi < group.length; gi++) { sx += mHtp_x(group[gi].q, group[gi].r); sy += mHtp_y(group[gi].q, group[gi].r); }
|
|
2266
|
+
var cxs = sx / group.length, cys = sy / group.length;
|
|
2267
|
+
var spt = mW2s(cxs, cys);
|
|
2268
|
+
mDrawPill(spt.x, spt.y + size * 1.5, group[0].subDomain, '#64748b', 11);
|
|
2269
|
+
});
|
|
2613
2270
|
}
|
|
2614
2271
|
}
|
|
2615
2272
|
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
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;
|
|
2273
|
+
// \u2500\u2500 Map hit test \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2274
|
+
function mGetAssetAt(sx, sy) {
|
|
2275
|
+
var w = mS2w(sx, sy);
|
|
2276
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
2277
|
+
var a = MAP.assets[i], wx = mHtp_x(a.q, a.r), wy = mHtp_y(a.q, a.r);
|
|
2278
|
+
var dx = Math.abs(w.x - wx), dy = Math.abs(w.y - wy);
|
|
2279
|
+
if (dx > MAP_HEX || dy > MAP_HEX) continue;
|
|
2280
|
+
if (dx * dx + dy * dy < MAP_HEX * MAP_HEX) return a;
|
|
2642
2281
|
}
|
|
2643
2282
|
return null;
|
|
2644
2283
|
}
|
|
2645
2284
|
|
|
2646
|
-
// \u2500\u2500
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
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');
|
|
2285
|
+
// \u2500\u2500 Map pan / zoom \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2286
|
+
var mDragging = false, mLastMX = 0, mLastMY = 0;
|
|
2287
|
+
mapWrap.addEventListener('mousedown', function(e) {
|
|
2288
|
+
if (e.button !== 0) return;
|
|
2289
|
+
mDragging = true; mLastMX = e.clientX; mLastMY = e.clientY;
|
|
2290
|
+
mapWrap.classList.add('dragging');
|
|
2665
2291
|
});
|
|
2666
|
-
window.addEventListener('mouseup', ()
|
|
2667
|
-
window.addEventListener('mousemove', e
|
|
2668
|
-
if(
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
document.getElementById('
|
|
2681
|
-
document.getElementById('
|
|
2682
|
-
|
|
2683
|
-
|
|
2292
|
+
window.addEventListener('mouseup', function() { mDragging = false; mapWrap.classList.remove('dragging'); });
|
|
2293
|
+
window.addEventListener('mousemove', function(e) {
|
|
2294
|
+
if (currentTab !== 'map') return;
|
|
2295
|
+
if (mDragging) {
|
|
2296
|
+
mvx += e.clientX - mLastMX; mvy += e.clientY - mLastMY;
|
|
2297
|
+
mLastMX = e.clientX; mLastMY = e.clientY; drawMap(); return;
|
|
2298
|
+
}
|
|
2299
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
2300
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
2301
|
+
var asset = mGetAssetAt(sx, sy);
|
|
2302
|
+
var newId = asset ? asset.id : null;
|
|
2303
|
+
if (newId !== mHoveredId) { mHoveredId = newId; drawMap(); }
|
|
2304
|
+
var tt = document.getElementById('map-tooltip');
|
|
2305
|
+
if (asset) {
|
|
2306
|
+
document.getElementById('mtt-name').textContent = asset.name;
|
|
2307
|
+
document.getElementById('mtt-domain').textContent = asset.domain + (asset.subDomain ? ' > ' + asset.subDomain : '');
|
|
2308
|
+
document.getElementById('mtt-quality').textContent = asset.qualityScore !== null ? 'Quality: ' + asset.qualityScore + '/100' : '';
|
|
2309
|
+
tt.style.display = 'block'; tt.style.left = (e.clientX + 12) + 'px'; tt.style.top = (e.clientY - 8) + 'px';
|
|
2310
|
+
} else { tt.style.display = 'none'; }
|
|
2684
2311
|
});
|
|
2685
2312
|
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
if(
|
|
2691
|
-
if(!asset) return;
|
|
2692
|
-
if(!
|
|
2693
|
-
else if(
|
|
2694
|
-
|
|
2695
|
-
|
|
2313
|
+
mapWrap.addEventListener('click', function(e) {
|
|
2314
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
2315
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
2316
|
+
var asset = mGetAssetAt(sx, sy);
|
|
2317
|
+
if (mConnectMode) {
|
|
2318
|
+
if (!asset) return;
|
|
2319
|
+
if (!mConnectFirst) { mConnectFirst = asset.id; drawMap(); }
|
|
2320
|
+
else if (mConnectFirst !== asset.id) {
|
|
2321
|
+
mLocalConns.push({ id: crypto.randomUUID(), sourceAssetId: mConnectFirst, targetAssetId: asset.id, type: 'connection' });
|
|
2322
|
+
mConnectFirst = null; drawMap();
|
|
2696
2323
|
}
|
|
2697
2324
|
return;
|
|
2698
2325
|
}
|
|
2699
|
-
if(asset){
|
|
2700
|
-
else{
|
|
2701
|
-
|
|
2326
|
+
if (asset) { mSelectedId = asset.id; mShowDetail(asset); }
|
|
2327
|
+
else { mSelectedId = null; document.getElementById('map-detail').classList.remove('open'); }
|
|
2328
|
+
drawMap();
|
|
2702
2329
|
});
|
|
2703
2330
|
|
|
2704
|
-
|
|
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=>{
|
|
2331
|
+
mapWrap.addEventListener('wheel', function(e) {
|
|
2722
2332
|
e.preventDefault();
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
},{passive:false});
|
|
2726
|
-
|
|
2727
|
-
function
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
document.getElementById('zoom-pct').textContent=Math.round(
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2333
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
2334
|
+
mApplyZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, e.clientX - rect.left, e.clientY - rect.top);
|
|
2335
|
+
}, { passive: false });
|
|
2336
|
+
|
|
2337
|
+
function mApplyZoom(factor, sx, sy) {
|
|
2338
|
+
var ns = Math.max(0.05, Math.min(8, mScale * factor));
|
|
2339
|
+
var wx = (sx - mvx) / mScale, wy = (sy - mvy) / mScale;
|
|
2340
|
+
mScale = ns; mvx = sx - wx * mScale; mvy = sy - wy * mScale;
|
|
2341
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
2342
|
+
drawMap();
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
document.getElementById('mz-in').addEventListener('click', function() { mApplyZoom(1.25, mW / 2, mH / 2); });
|
|
2346
|
+
document.getElementById('mz-out').addEventListener('click', function() { mApplyZoom(1 / 1.25, mW / 2, mH / 2); });
|
|
2347
|
+
|
|
2348
|
+
// \u2500\u2500 Map detail panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2349
|
+
function mEsc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
2350
|
+
function mRenderQ(s) {
|
|
2351
|
+
var c = s >= 70 ? '#22c55e' : s >= 40 ? '#f97316' : '#ef4444';
|
|
2352
|
+
return s + '/100 <div class="quality-bar"><div class="quality-fill" style="width:' + s + '%;background:' + c + '"></div></div>';
|
|
2353
|
+
}
|
|
2354
|
+
function mShowDetail(asset) {
|
|
2355
|
+
document.getElementById('md-name').textContent = asset.name;
|
|
2356
|
+
var body = document.getElementById('md-body');
|
|
2357
|
+
var rows = [['Domain', asset.domain], ['Sub-domain', asset.subDomain],
|
|
2358
|
+
['Quality', asset.qualityScore !== null ? mRenderQ(asset.qualityScore) : null]
|
|
2359
|
+
].concat(Object.entries(asset.metadata || {}).slice(0, 8).map(function(kv) { return [kv[0], String(kv[1])]; }))
|
|
2360
|
+
.filter(function(r) { return r[1] !== null && r[1] !== undefined && r[1] !== ''; });
|
|
2361
|
+
body.innerHTML = rows.map(function(r) {
|
|
2362
|
+
return '<div class="meta-row"><div class="meta-label">' + mEsc(String(r[0])) + '</div><div class="meta-value">' + r[1] + '</div></div>';
|
|
2363
|
+
}).join('');
|
|
2364
|
+
var related = mLocalConns.filter(function(cn) { return cn.sourceAssetId === asset.id || cn.targetAssetId === asset.id; });
|
|
2365
|
+
if (related.length > 0) {
|
|
2366
|
+
body.innerHTML += '<div class="meta-row"><div class="meta-label">Connections (' + related.length + ')</div><div>' +
|
|
2367
|
+
related.map(function(cn) {
|
|
2368
|
+
var oid = cn.sourceAssetId === asset.id ? cn.targetAssetId : cn.sourceAssetId;
|
|
2369
|
+
var o = mapAssetIndex.get(oid);
|
|
2370
|
+
return '<div class="meta-value" style="margin-top:4px;font-size:12px">' + (o ? mEsc(o.name) : oid) + '</div>';
|
|
2371
|
+
}).join('') + '</div></div>';
|
|
2372
|
+
}
|
|
2373
|
+
document.getElementById('map-detail').classList.add('open');
|
|
2374
|
+
}
|
|
2375
|
+
document.getElementById('md-close').addEventListener('click', function() {
|
|
2376
|
+
document.getElementById('map-detail').classList.remove('open'); mSelectedId = null; drawMap();
|
|
2749
2377
|
});
|
|
2750
2378
|
|
|
2751
|
-
// \u2500\u2500
|
|
2752
|
-
function
|
|
2753
|
-
|
|
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();
|
|
2379
|
+
// \u2500\u2500 Map toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2380
|
+
document.getElementById('btn-labels').addEventListener('click', function() {
|
|
2381
|
+
mShowLabels = !mShowLabels; this.classList.toggle('active', mShowLabels); drawMap();
|
|
2775
2382
|
});
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
|
|
2781
|
-
document.getElementById('dl-'+n).classList.add('active');draw();
|
|
2782
|
-
});
|
|
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();
|
|
2783
2387
|
});
|
|
2784
|
-
document.getElementById('btn-
|
|
2785
|
-
|
|
2388
|
+
document.getElementById('btn-quality').addEventListener('click', function() {
|
|
2389
|
+
mShowQuality = !mShowQuality; this.classList.toggle('active', mShowQuality); drawMap();
|
|
2786
2390
|
});
|
|
2787
|
-
document.getElementById('btn-
|
|
2788
|
-
|
|
2391
|
+
document.getElementById('btn-connect').addEventListener('click', function() {
|
|
2392
|
+
mConnectMode = !mConnectMode; mConnectFirst = null;
|
|
2393
|
+
this.classList.toggle('active', mConnectMode);
|
|
2394
|
+
mapWrap.classList.toggle('connecting', mConnectMode);
|
|
2395
|
+
document.getElementById('map-connect-hint').style.display = mConnectMode ? 'block' : 'none'; drawMap();
|
|
2789
2396
|
});
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2397
|
+
|
|
2398
|
+
// Map keyboard
|
|
2399
|
+
mapWrap.addEventListener('keydown', function(e) {
|
|
2400
|
+
if (e.key === 'ArrowLeft') { mvx += 40; drawMap(); }
|
|
2401
|
+
else if (e.key === 'ArrowRight') { mvx -= 40; drawMap(); }
|
|
2402
|
+
else if (e.key === 'ArrowUp') { mvy += 40; drawMap(); }
|
|
2403
|
+
else if (e.key === 'ArrowDown') { mvy -= 40; drawMap(); }
|
|
2404
|
+
else if (e.key === '+' || e.key === '=') mApplyZoom(1.2, mW / 2, mH / 2);
|
|
2405
|
+
else if (e.key === '-') mApplyZoom(1 / 1.2, mW / 2, mH / 2);
|
|
2406
|
+
else if (e.key === 'Escape') {
|
|
2407
|
+
mSelectedId = null; document.getElementById('map-detail').classList.remove('open');
|
|
2408
|
+
if (mConnectMode) { mConnectMode = false; mConnectFirst = null; mapWrap.classList.remove('connecting'); document.getElementById('map-connect-hint').style.display = 'none'; document.getElementById('btn-connect').classList.remove('active'); }
|
|
2409
|
+
drawMap();
|
|
2410
|
+
}
|
|
2801
2411
|
});
|
|
2802
|
-
document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
|
|
2803
2412
|
|
|
2804
|
-
//
|
|
2805
|
-
|
|
2806
|
-
document.getElementById('zoom-pct').textContent=Math.round(
|
|
2807
|
-
|
|
2808
|
-
|
|
2413
|
+
// Map init
|
|
2414
|
+
resizeMap(); mapFitToView();
|
|
2415
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
2416
|
+
drawMap();
|
|
2417
|
+
|
|
2418
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2419
|
+
// TOPOLOGY VIEW (lazy init)
|
|
2420
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2421
|
+
var TOPO = ${graphData};
|
|
2422
|
+
|
|
2423
|
+
var TYPE_COLORS = {
|
|
2424
|
+
host:'#4a9eff',database_server:'#ff6b6b',database:'#ff8c42',
|
|
2425
|
+
web_service:'#6bcb77',api_endpoint:'#4d96ff',cache_server:'#ffd93d',
|
|
2426
|
+
message_broker:'#c77dff',queue:'#e0aaff',topic:'#9d4edd',
|
|
2427
|
+
container:'#48cae4',pod:'#00b4d8',k8s_cluster:'#0077b6',
|
|
2428
|
+
config_file:'#adb5bd',saas_tool:'#c084fc',table:'#f97316',unknown:'#6c757d'
|
|
2429
|
+
};
|
|
2430
|
+
var LAYER_COLORS = { saas:'#c084fc',web:'#6bcb77',data:'#ff6b6b',messaging:'#c77dff',infra:'#4a9eff',config:'#adb5bd',other:'#6c757d' };
|
|
2431
|
+
var LAYER_NAMES = { saas:'SaaS Tools',web:'Web / API',data:'Data Layer',messaging:'Messaging',infra:'Infrastructure',config:'Config',other:'Other' };
|
|
2432
|
+
|
|
2433
|
+
var topoSelectedId = null;
|
|
2434
|
+
|
|
2435
|
+
function setTopoSearch(q) {
|
|
2436
|
+
var el = document.getElementById('topo-search');
|
|
2437
|
+
if (el) { el.value = q; buildTopoList(q); }
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
function buildTopoList(filter) {
|
|
2441
|
+
var listEl = document.getElementById('topo-list');
|
|
2442
|
+
var q = (filter || '').toLowerCase();
|
|
2443
|
+
listEl.innerHTML = '';
|
|
2444
|
+
var sorted = TOPO.nodes.slice().sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
2445
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
2446
|
+
var d = sorted[i];
|
|
2447
|
+
if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
|
|
2448
|
+
var item = document.createElement('div');
|
|
2449
|
+
item.className = 'topo-item' + (d.id === topoSelectedId ? ' active' : '');
|
|
2450
|
+
item.dataset.id = d.id;
|
|
2451
|
+
var color = TYPE_COLORS[d.type] || '#aaa';
|
|
2452
|
+
item.innerHTML = '<span class="topo-dot" style="background:' + color + '"></span>' +
|
|
2453
|
+
'<span class="topo-name" title="' + d.id + '">' + d.name + '</span>' +
|
|
2454
|
+
'<span class="topo-type">' + d.type.replace(/_/g, ' ') + '</span>';
|
|
2455
|
+
(function(dd) { item.onclick = function() { selectTopoNode(dd); focusTopoNode(dd); }; })(d);
|
|
2456
|
+
listEl.appendChild(item);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
document.getElementById('topo-search').addEventListener('input', function(e) { buildTopoList(e.target.value); });
|
|
2461
|
+
|
|
2462
|
+
var topoSidebar = document.getElementById('topo-sidebar');
|
|
2463
|
+
|
|
2464
|
+
function selectTopoNode(d) {
|
|
2465
|
+
topoSelectedId = d.id;
|
|
2466
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
2467
|
+
showTopoNode(d);
|
|
2468
|
+
if (typeof d3 !== 'undefined') d3.selectAll('.node-hex').classed('selected', function(nd) { return nd.id === d.id; });
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function showTopoNode(d) {
|
|
2472
|
+
var c = TYPE_COLORS[d.type] || '#aaa';
|
|
2473
|
+
var confPct = Math.round(d.confidence * 100);
|
|
2474
|
+
var tags = (d.tags || []).map(function(t) { return '<span class="tag">' + t + '</span>'; }).join('');
|
|
2475
|
+
var metaRows = Object.entries(d.metadata || {})
|
|
2476
|
+
.filter(function(kv) { return kv[1] !== null && kv[1] !== undefined && String(kv[1]).length > 0; })
|
|
2477
|
+
.map(function(kv) { return '<tr><td>' + kv[0] + '</td><td>' + JSON.stringify(kv[1]) + '</td></tr>'; }).join('');
|
|
2478
|
+
var related = TOPO.links.filter(function(l) {
|
|
2479
|
+
return (l.source.id || l.source) === d.id || (l.target.id || l.target) === d.id;
|
|
2480
|
+
});
|
|
2481
|
+
var edgeItems = related.map(function(l) {
|
|
2482
|
+
var isOut = (l.source.id || l.source) === d.id;
|
|
2483
|
+
var other = isOut ? (l.target.id || l.target) : (l.source.id || l.source);
|
|
2484
|
+
return '<div class="edge-item">' + (isOut ? '\\u2192' : '\\u2190') + ' <span>' + other + '</span> <small>[' + l.relationship + ']</small></div>';
|
|
2485
|
+
}).join('');
|
|
2486
|
+
|
|
2487
|
+
topoSidebar.innerHTML =
|
|
2488
|
+
'<h2>' + d.name + '</h2>' +
|
|
2489
|
+
'<table class="meta-table">' +
|
|
2490
|
+
'<tr><td>ID</td><td style="font-size:10px;word-break:break-all">' + d.id + '</td></tr>' +
|
|
2491
|
+
'<tr><td>Type</td><td><span style="color:' + c + '">' + d.type + '</span></td></tr>' +
|
|
2492
|
+
'<tr><td>Layer</td><td>' + d.layer + '</td></tr>' +
|
|
2493
|
+
'<tr><td>Confidence</td><td>' + confPct + '% <div class="conf-bar"><div class="conf-fill" style="width:' + confPct + '%;background:' + c + '"></div></div></td></tr>' +
|
|
2494
|
+
'<tr><td>Via</td><td>' + (d.discoveredVia || '\\u2014') + '</td></tr>' +
|
|
2495
|
+
'<tr><td>Timestamp</td><td>' + (d.discoveredAt ? d.discoveredAt.substring(0, 19).replace('T', ' ') : '\\u2014') + '</td></tr>' +
|
|
2496
|
+
(tags ? '<tr><td>Tags</td><td>' + tags + '</td></tr>' : '') +
|
|
2497
|
+
metaRows + '</table>' +
|
|
2498
|
+
(related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>' + edgeItems + '</div>' : '') +
|
|
2499
|
+
'<div style="margin-top:14px"><button class="export-btn" style="width:100%" onclick="deleteTopoNode(\\'' + d.id.replace(/'/g, "\\\\'") + '\\')">Delete node</button></div>';
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function deleteTopoNode(id) {
|
|
2503
|
+
var idx = TOPO.nodes.findIndex(function(n) { return n.id === id; });
|
|
2504
|
+
if (idx === -1) return;
|
|
2505
|
+
TOPO.nodes.splice(idx, 1);
|
|
2506
|
+
TOPO.links = TOPO.links.filter(function(l) {
|
|
2507
|
+
return (l.source.id || l.source) !== id && (l.target.id || l.target) !== id;
|
|
2508
|
+
});
|
|
2509
|
+
topoSelectedId = null;
|
|
2510
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
|
|
2511
|
+
if (typeof rebuildTopoGraph === 'function') rebuildTopoGraph();
|
|
2512
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
function initTopology() {
|
|
2516
|
+
if (typeof d3 === 'undefined') return;
|
|
2517
|
+
|
|
2518
|
+
var svgEl = d3.select('#topo-graph svg');
|
|
2519
|
+
var graphDiv = document.getElementById('topo-graph');
|
|
2520
|
+
var gW = function() { return graphDiv.clientWidth; };
|
|
2521
|
+
var gH = function() { return graphDiv.clientHeight; };
|
|
2522
|
+
var g = svgEl.append('g');
|
|
2523
|
+
|
|
2524
|
+
svgEl.append('defs').append('marker')
|
|
2525
|
+
.attr('id', 'arrow').attr('viewBox', '0 0 10 6')
|
|
2526
|
+
.attr('refX', 10).attr('refY', 3)
|
|
2527
|
+
.attr('markerWidth', 8).attr('markerHeight', 6)
|
|
2528
|
+
.attr('orient', 'auto')
|
|
2529
|
+
.append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
|
|
2530
|
+
|
|
2531
|
+
var currentZoom = 1;
|
|
2532
|
+
var zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', function(e) {
|
|
2533
|
+
g.attr('transform', e.transform); currentZoom = e.transform.k; updateTopoLOD(currentZoom);
|
|
2534
|
+
});
|
|
2535
|
+
svgEl.call(zoomBehavior);
|
|
2536
|
+
|
|
2537
|
+
// Layer filters
|
|
2538
|
+
var layers = Array.from(new Set(TOPO.nodes.map(function(d) { return d.layer; })));
|
|
2539
|
+
var layerVisible = {};
|
|
2540
|
+
layers.forEach(function(l) { layerVisible[l] = true; });
|
|
2541
|
+
|
|
2542
|
+
var toolbarEl = document.getElementById('topo-toolbar');
|
|
2543
|
+
layers.forEach(function(layer) {
|
|
2544
|
+
var btn = document.createElement('button');
|
|
2545
|
+
btn.className = 'filter-btn';
|
|
2546
|
+
btn.innerHTML = '<span class="filter-dot" style="background:' + (LAYER_COLORS[layer] || '#666') + '"></span>' + (LAYER_NAMES[layer] || layer);
|
|
2547
|
+
btn.onclick = function() { layerVisible[layer] = !layerVisible[layer]; btn.classList.toggle('off', !layerVisible[layer]); updateTopoVisibility(); };
|
|
2548
|
+
toolbarEl.appendChild(btn);
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
// JGF export button
|
|
2552
|
+
var jgfBtn = document.createElement('button');
|
|
2553
|
+
jgfBtn.className = 'export-btn'; jgfBtn.textContent = '\\u2193 JGF'; jgfBtn.title = 'Export JSON Graph Format';
|
|
2554
|
+
jgfBtn.onclick = function() {
|
|
2555
|
+
var jgf = { graph: { directed: true, type: 'cartography', label: 'Infrastructure Map',
|
|
2556
|
+
metadata: { exportedAt: new Date().toISOString() },
|
|
2557
|
+
nodes: Object.fromEntries(TOPO.nodes.map(function(n) { return [n.id, { label: n.name, metadata: { type: n.type, layer: n.layer, confidence: n.confidence, discoveredVia: n.discoveredVia, discoveredAt: n.discoveredAt, tags: n.tags } }]; })),
|
|
2558
|
+
edges: TOPO.links.map(function(l) { return { source: l.source.id || l.source, target: l.target.id || l.target, relation: l.relationship, metadata: { confidence: l.confidence, evidence: l.evidence } }; })
|
|
2559
|
+
}};
|
|
2560
|
+
var blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
|
|
2561
|
+
var url = URL.createObjectURL(blob);
|
|
2562
|
+
var a = document.createElement('a'); a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
|
|
2563
|
+
URL.revokeObjectURL(url);
|
|
2564
|
+
};
|
|
2565
|
+
toolbarEl.appendChild(jgfBtn);
|
|
2566
|
+
|
|
2567
|
+
// Hex helpers
|
|
2568
|
+
var T_HEX = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
|
|
2569
|
+
function tHexSize(d) { return T_HEX[d.type] || T_HEX.default; }
|
|
2570
|
+
function tHexPath(size) {
|
|
2571
|
+
var pts = [];
|
|
2572
|
+
for (var i = 0; i < 6; i++) {
|
|
2573
|
+
var angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
2574
|
+
pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
|
|
2575
|
+
}
|
|
2576
|
+
return 'M' + pts.map(function(p) { return p.join(','); }).join('L') + 'Z';
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// Cluster force
|
|
2580
|
+
function clusterForce(alpha) {
|
|
2581
|
+
var centroids = {}, counts = {};
|
|
2582
|
+
TOPO.nodes.forEach(function(d) {
|
|
2583
|
+
if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
|
|
2584
|
+
centroids[d.layer].x += d.x || 0; centroids[d.layer].y += d.y || 0; counts[d.layer]++;
|
|
2585
|
+
});
|
|
2586
|
+
for (var l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
|
|
2587
|
+
var strength = alpha * 0.15;
|
|
2588
|
+
TOPO.nodes.forEach(function(d) {
|
|
2589
|
+
var cn = centroids[d.layer];
|
|
2590
|
+
if (cn) { d.vx += (cn.x - d.x) * strength; d.vy += (cn.y - d.y) * strength; }
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// Hulls
|
|
2595
|
+
var hullGroup = g.append('g').attr('class', 'hulls');
|
|
2596
|
+
var hullPaths = {}, hullLabels = {};
|
|
2597
|
+
layers.forEach(function(layer) {
|
|
2598
|
+
hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
|
|
2599
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
|
|
2600
|
+
hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
|
|
2601
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
|
|
2602
|
+
});
|
|
2603
|
+
|
|
2604
|
+
function updateHulls() {
|
|
2605
|
+
layers.forEach(function(layer) {
|
|
2606
|
+
if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
|
|
2607
|
+
var pts = TOPO.nodes.filter(function(d) { return d.layer === layer && layerVisible[d.layer]; }).map(function(d) { return [d.x, d.y]; });
|
|
2608
|
+
if (pts.length < 3) {
|
|
2609
|
+
hullPaths[layer].attr('d', null);
|
|
2610
|
+
if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
|
|
2611
|
+
else hullLabels[layer].attr('x', -9999);
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
var hull = d3.polygonHull(pts);
|
|
2615
|
+
if (!hull) { hullPaths[layer].attr('d', null); return; }
|
|
2616
|
+
var cx = d3.mean(hull, function(p) { return p[0]; });
|
|
2617
|
+
var cy = d3.mean(hull, function(p) { return p[1]; });
|
|
2618
|
+
var padded = hull.map(function(p) {
|
|
2619
|
+
var dx = p[0] - cx, dy = p[1] - cy;
|
|
2620
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
2621
|
+
return [p[0] + dx / len * 40, p[1] + dy / len * 40];
|
|
2622
|
+
});
|
|
2623
|
+
hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
|
|
2624
|
+
hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, function(p) { return Math.abs(p[1] - cy); }) - 30);
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// Graph
|
|
2629
|
+
var linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
|
|
2630
|
+
var linkGroup = g.append('g');
|
|
2631
|
+
var nodeGroup = g.append('g');
|
|
2632
|
+
|
|
2633
|
+
function focusTopoNode(d) {
|
|
2634
|
+
if (!d.x || !d.y) return;
|
|
2635
|
+
var w = gW(), h = gH();
|
|
2636
|
+
svgEl.transition().duration(500).call(
|
|
2637
|
+
zoomBehavior.transform,
|
|
2638
|
+
d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
window.focusTopoNode = focusTopoNode;
|
|
2642
|
+
|
|
2643
|
+
function rebuildTopoGraph() {
|
|
2644
|
+
if (sim) sim.stop();
|
|
2645
|
+
|
|
2646
|
+
linkSel = linkGroup.selectAll('line').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
2647
|
+
linkSel.exit().remove();
|
|
2648
|
+
var linkEnter = linkSel.enter().append('line').attr('class', 'link');
|
|
2649
|
+
linkSel = linkEnter.merge(linkSel)
|
|
2650
|
+
.attr('stroke', function(d) { return d.confidence < 0.6 ? '#2a2e35' : '#3d434b'; })
|
|
2651
|
+
.attr('stroke-dasharray', function(d) { return d.confidence < 0.6 ? '4 3' : null; })
|
|
2652
|
+
.attr('stroke-width', function(d) { return d.confidence < 0.6 ? 0.8 : 1.2; })
|
|
2653
|
+
.attr('marker-end', 'url(#arrow)');
|
|
2654
|
+
linkSel.select('title').remove();
|
|
2655
|
+
linkSel.append('title').text(function(d) { return d.relationship + ' (' + Math.round(d.confidence * 100) + '%)\\n' + (d.evidence || ''); });
|
|
2656
|
+
|
|
2657
|
+
linkLabelSel = linkGroup.selectAll('text').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
2658
|
+
linkLabelSel.exit().remove();
|
|
2659
|
+
linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel).text(function(d) { return d.relationship; });
|
|
2660
|
+
|
|
2661
|
+
nodeSel = nodeGroup.selectAll('g').data(TOPO.nodes, function(d) { return d.id; });
|
|
2662
|
+
nodeSel.exit().remove();
|
|
2663
|
+
var nodeEnter = nodeSel.enter().append('g')
|
|
2664
|
+
.call(d3.drag()
|
|
2665
|
+
.on('start', function(e, d) { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
2666
|
+
.on('drag', function(e, d) { d.fx = e.x; d.fy = e.y; })
|
|
2667
|
+
.on('end', function(e, d) { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
2668
|
+
)
|
|
2669
|
+
.on('click', function(e, d) { e.stopPropagation(); selectTopoNode(d); });
|
|
2670
|
+
nodeEnter.append('path').attr('class', 'node-hex');
|
|
2671
|
+
nodeEnter.append('title');
|
|
2672
|
+
nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
|
|
2673
|
+
|
|
2674
|
+
nodeSel = nodeEnter.merge(nodeSel);
|
|
2675
|
+
nodeSel.select('.node-hex')
|
|
2676
|
+
.attr('d', function(d) { return tHexPath(tHexSize(d)); })
|
|
2677
|
+
.attr('fill', function(d) { return TYPE_COLORS[d.type] || '#aaa'; })
|
|
2678
|
+
.attr('stroke', function(d) { var c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
|
|
2679
|
+
.attr('fill-opacity', function(d) { return 0.6 + d.confidence * 0.4; })
|
|
2680
|
+
.classed('selected', function(d) { return d.id === topoSelectedId; });
|
|
2681
|
+
nodeSel.select('title').text(function(d) { return d.name + ' (' + d.type + ')\\nconf: ' + Math.round(d.confidence * 100) + '%'; });
|
|
2682
|
+
nodeLabelSel = nodeSel.select('.node-label')
|
|
2683
|
+
.attr('dy', function(d) { return tHexSize(d) + 13; })
|
|
2684
|
+
.text(function(d) { return d.name.length > 20 ? d.name.substring(0, 18) + '\\u2026' : d.name; });
|
|
2685
|
+
|
|
2686
|
+
sim = d3.forceSimulation(TOPO.nodes)
|
|
2687
|
+
.force('link', d3.forceLink(TOPO.links).id(function(d) { return d.id; }).distance(function(d) { return d.relationship === 'contains' ? 50 : 100; }).strength(0.4))
|
|
2688
|
+
.force('charge', d3.forceManyBody().strength(-280))
|
|
2689
|
+
.force('center', d3.forceCenter(gW() / 2, gH() / 2))
|
|
2690
|
+
.force('collision', d3.forceCollide().radius(function(d) { return tHexSize(d) + 10; }))
|
|
2691
|
+
.force('cluster', clusterForce)
|
|
2692
|
+
.on('tick', function() {
|
|
2693
|
+
updateHulls();
|
|
2694
|
+
linkSel.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; })
|
|
2695
|
+
.attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; });
|
|
2696
|
+
linkLabelSel.attr('x', function(d) { return (d.source.x + d.target.x) / 2; })
|
|
2697
|
+
.attr('y', function(d) { return (d.source.y + d.target.y) / 2 - 4; });
|
|
2698
|
+
nodeSel.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
window.rebuildTopoGraph = rebuildTopoGraph;
|
|
2702
|
+
|
|
2703
|
+
function updateTopoLOD(k) {
|
|
2704
|
+
if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
|
|
2705
|
+
if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
|
|
2706
|
+
d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
function updateTopoVisibility() {
|
|
2710
|
+
if (!nodeSel) return;
|
|
2711
|
+
nodeSel.style('display', function(d) { return layerVisible[d.layer] ? null : 'none'; });
|
|
2712
|
+
linkSel.style('display', function(d) {
|
|
2713
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
2714
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
2715
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2716
|
+
});
|
|
2717
|
+
linkLabelSel.style('display', function(d) {
|
|
2718
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
2719
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
2720
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
rebuildTopoGraph();
|
|
2725
|
+
buildTopoList();
|
|
2726
|
+
updateTopoLOD(1);
|
|
2727
|
+
|
|
2728
|
+
svgEl.on('click', function() {
|
|
2729
|
+
topoSelectedId = null;
|
|
2730
|
+
d3.selectAll('.node-hex').classed('selected', false);
|
|
2731
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
2732
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// Init topology node list (non-D3 part)
|
|
2737
|
+
buildTopoList();
|
|
2809
2738
|
</script>
|
|
2810
2739
|
</body>
|
|
2811
2740
|
</html>`;
|
|
2812
2741
|
}
|
|
2813
|
-
function
|
|
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
|
+
}
|
|
2771
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "discovery", "sops"]) {
|
|
2814
2772
|
mkdirSync2(outputDir, { recursive: true });
|
|
2815
2773
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
2816
2774
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
2817
2775
|
const nodes = db.getNodes(sessionId);
|
|
2818
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");
|
|
2819
2780
|
if (formats.includes("mermaid")) {
|
|
2820
2781
|
writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
2821
2782
|
writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
@@ -2829,13 +2790,9 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
2829
2790
|
writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
2830
2791
|
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
2831
2792
|
}
|
|
2832
|
-
if (formats.includes("html")) {
|
|
2833
|
-
writeFileSync(join2(outputDir, "
|
|
2834
|
-
process.stderr.write("\u2713
|
|
2835
|
-
}
|
|
2836
|
-
if (formats.includes("map")) {
|
|
2837
|
-
writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
|
|
2838
|
-
process.stderr.write("\u2713 cartography-map.html\n");
|
|
2793
|
+
if (formats.includes("html") || formats.includes("map") || formats.includes("discovery")) {
|
|
2794
|
+
writeFileSync(join2(outputDir, "discovery.html"), exportDiscoveryApp(nodes, edges));
|
|
2795
|
+
process.stderr.write("\u2713 discovery.html\n");
|
|
2839
2796
|
}
|
|
2840
2797
|
if (formats.includes("sops")) {
|
|
2841
2798
|
const sops = db.getSOPs(sessionId);
|
|
@@ -2853,10 +2810,9 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
2853
2810
|
}
|
|
2854
2811
|
|
|
2855
2812
|
// src/cli.ts
|
|
2856
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2
|
|
2813
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
2857
2814
|
import { resolve } from "path";
|
|
2858
2815
|
import { createInterface } from "readline";
|
|
2859
|
-
import { execSync as execSync2 } from "child_process";
|
|
2860
2816
|
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
2861
2817
|
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
2862
2818
|
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
@@ -3080,27 +3036,12 @@ function main() {
|
|
|
3080
3036
|
}
|
|
3081
3037
|
exportAll(db, sessionId, config.outputDir);
|
|
3082
3038
|
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
3083
|
-
const
|
|
3084
|
-
const mapPath = resolve(config.outputDir, "cartography-map.html");
|
|
3085
|
-
const topoPath = resolve(config.outputDir, "topology.mermaid");
|
|
3039
|
+
const discoveryPath = resolve(config.outputDir, "discovery.html");
|
|
3086
3040
|
w("\n");
|
|
3087
|
-
if (existsSync2(
|
|
3088
|
-
w(` ${green("\u2192")} ${osc8(`file://${
|
|
3089
|
-
`);
|
|
3090
|
-
}
|
|
3091
|
-
if (existsSync2(htmlPath)) {
|
|
3092
|
-
w(` ${green("\u2192")} ${osc8(`file://${htmlPath}`, bold("Open topology.html"))}
|
|
3041
|
+
if (existsSync2(discoveryPath)) {
|
|
3042
|
+
w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("Open discovery.html"))} ${dim("\u2190 Map + Topology")}
|
|
3093
3043
|
`);
|
|
3094
3044
|
}
|
|
3095
|
-
if (existsSync2(topoPath)) {
|
|
3096
|
-
try {
|
|
3097
|
-
const code = readFileSync2(topoPath, "utf8");
|
|
3098
|
-
const b64 = Buffer.from(JSON.stringify({ code, mermaid: { theme: "dark" } })).toString("base64");
|
|
3099
|
-
w(` ${cyan("\u2192")} ${osc8(`https://mermaid.live/view#base64:${b64}`, bold("Open in mermaid.live"))}
|
|
3100
|
-
`);
|
|
3101
|
-
} catch {
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
3045
|
w("\n");
|
|
3105
3046
|
if (process.stdin.isTTY) {
|
|
3106
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"));
|
|
@@ -3142,8 +3083,8 @@ function main() {
|
|
|
3142
3083
|
`);
|
|
3143
3084
|
w("\n");
|
|
3144
3085
|
exportAll(db, sessionId, config.outputDir);
|
|
3145
|
-
if (existsSync2(
|
|
3146
|
-
w(` ${green("\u2192")} ${osc8(`file://${
|
|
3086
|
+
if (existsSync2(discoveryPath)) {
|
|
3087
|
+
w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("discovery.html updated"))}
|
|
3147
3088
|
`);
|
|
3148
3089
|
}
|
|
3149
3090
|
w("\n");
|
|
@@ -3167,37 +3108,6 @@ function main() {
|
|
|
3167
3108
|
`);
|
|
3168
3109
|
db.close();
|
|
3169
3110
|
});
|
|
3170
|
-
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) => {
|
|
3171
|
-
const config = defaultConfig({ outputDir: opts.output });
|
|
3172
|
-
const db = new CartographyDB(config.dbPath);
|
|
3173
|
-
const session = sessionId ? db.getSession(sessionId) : db.getLatestSession();
|
|
3174
|
-
if (!session) {
|
|
3175
|
-
process.stderr.write("No session found. Run discover first.\n");
|
|
3176
|
-
db.close();
|
|
3177
|
-
process.exitCode = 1;
|
|
3178
|
-
return;
|
|
3179
|
-
}
|
|
3180
|
-
const nodes = db.getNodes(session.id);
|
|
3181
|
-
const edges = db.getEdges(session.id);
|
|
3182
|
-
const outDir = resolve(opts.output);
|
|
3183
|
-
mkdirSync3(outDir, { recursive: true });
|
|
3184
|
-
const outPath = resolve(outDir, "cartography-map.html");
|
|
3185
|
-
writeFileSync2(outPath, exportCartographyMap(nodes, edges, { theme: opts.theme }));
|
|
3186
|
-
db.close();
|
|
3187
|
-
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
3188
|
-
const fileUrl = `file://${outPath}`;
|
|
3189
|
-
process.stderr.write(`
|
|
3190
|
-
${green("OK")} ${osc8(fileUrl, bold("Open cartography-map.html"))}
|
|
3191
|
-
`);
|
|
3192
|
-
process.stderr.write(` ${dim(fileUrl)}
|
|
3193
|
-
|
|
3194
|
-
`);
|
|
3195
|
-
try {
|
|
3196
|
-
const cmd = process.platform === "darwin" ? `open "${outPath}"` : process.platform === "win32" ? `start "" "${outPath}"` : `xdg-open "${outPath}"`;
|
|
3197
|
-
execSync2(cmd, { stdio: "ignore" });
|
|
3198
|
-
} catch {
|
|
3199
|
-
}
|
|
3200
|
-
});
|
|
3201
3111
|
program.command("show [session-id]").description("Show session details").action((sessionId) => {
|
|
3202
3112
|
const config = defaultConfig();
|
|
3203
3113
|
const db = new CartographyDB(config.dbPath);
|
|
@@ -3440,8 +3350,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3440
3350
|
out(dim(" catalog-info.yaml Backstage service catalog\n"));
|
|
3441
3351
|
out(dim(" topology.mermaid Infrastructure topology (graph TB)\n"));
|
|
3442
3352
|
out(dim(" dependencies.mermaid Service dependencies (graph LR)\n"));
|
|
3443
|
-
out(dim("
|
|
3444
|
-
out(dim(" cartography-map.html Hex grid data cartography map\n"));
|
|
3353
|
+
out(dim(" discovery.html Enterprise discovery frontend (Map + Topology)\n"));
|
|
3445
3354
|
out(dim(" sops/ Generated SOPs as Markdown\n"));
|
|
3446
3355
|
out(dim(" workflows/ Workflow flowcharts as Mermaid\n"));
|
|
3447
3356
|
out("\n");
|
|
@@ -3658,7 +3567,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3658
3567
|
`);
|
|
3659
3568
|
});
|
|
3660
3569
|
program.command("doctor").description("Check all requirements and cloud CLIs").action(async () => {
|
|
3661
|
-
const { execSync:
|
|
3570
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
3662
3571
|
const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
|
|
3663
3572
|
const { join: join3 } = await import("path");
|
|
3664
3573
|
const out = (s) => process.stdout.write(s);
|
|
@@ -3681,7 +3590,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3681
3590
|
allGood = false;
|
|
3682
3591
|
}
|
|
3683
3592
|
try {
|
|
3684
|
-
const v =
|
|
3593
|
+
const v = execSync2("claude --version", { stdio: "pipe" }).toString().trim();
|
|
3685
3594
|
ok(`Claude CLI ${dim2(v)}`);
|
|
3686
3595
|
} catch {
|
|
3687
3596
|
err("Claude CLI not found \u2014 npm i -g @anthropic-ai/claude-code");
|
|
@@ -3705,7 +3614,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3705
3614
|
allGood = false;
|
|
3706
3615
|
}
|
|
3707
3616
|
try {
|
|
3708
|
-
const v =
|
|
3617
|
+
const v = execSync2("kubectl version --client --short 2>/dev/null || kubectl version --client", { stdio: "pipe" }).toString().split("\n")[0]?.trim() ?? "";
|
|
3709
3618
|
ok(`kubectl ${dim2(v || "(client OK)")}`);
|
|
3710
3619
|
} catch {
|
|
3711
3620
|
warn(`kubectl not found ${dim2("\u2014 install: https://kubernetes.io/docs/tasks/tools/")}`);
|
|
@@ -3717,7 +3626,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3717
3626
|
];
|
|
3718
3627
|
for (const [name, cmd, hint] of cloudClis) {
|
|
3719
3628
|
try {
|
|
3720
|
-
|
|
3629
|
+
execSync2(cmd, { stdio: "pipe" });
|
|
3721
3630
|
ok(`${name} ${dim2("(cloud scanning available)")}`);
|
|
3722
3631
|
} catch {
|
|
3723
3632
|
warn(`${name} not found ${dim2("\u2014 cloud scan skipped | " + hint)}`);
|
|
@@ -3729,7 +3638,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3729
3638
|
];
|
|
3730
3639
|
for (const [name, cmd] of localTools) {
|
|
3731
3640
|
try {
|
|
3732
|
-
|
|
3641
|
+
execSync2(cmd, { stdio: "pipe" });
|
|
3733
3642
|
ok(`${name} ${dim2("(discovery tool)")}`);
|
|
3734
3643
|
} catch {
|
|
3735
3644
|
warn(`${name} not found ${dim2("\u2014 discovery without " + name + " will be limited")}`);
|