@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/index.js
CHANGED
|
@@ -2015,525 +2015,6 @@ function exportJSON(db, sessionId) {
|
|
|
2015
2015
|
sops
|
|
2016
2016
|
}, null, 2);
|
|
2017
2017
|
}
|
|
2018
|
-
function exportHTML(nodes, edges) {
|
|
2019
|
-
const graphData = JSON.stringify({
|
|
2020
|
-
nodes: nodes.map((n) => ({
|
|
2021
|
-
id: n.id,
|
|
2022
|
-
name: n.name,
|
|
2023
|
-
type: n.type,
|
|
2024
|
-
layer: nodeLayer(n.type),
|
|
2025
|
-
confidence: n.confidence,
|
|
2026
|
-
discoveredVia: n.discoveredVia,
|
|
2027
|
-
discoveredAt: n.discoveredAt,
|
|
2028
|
-
tags: n.tags,
|
|
2029
|
-
metadata: n.metadata
|
|
2030
|
-
})),
|
|
2031
|
-
links: edges.map((e) => ({
|
|
2032
|
-
source: e.sourceId,
|
|
2033
|
-
target: e.targetId,
|
|
2034
|
-
relationship: e.relationship,
|
|
2035
|
-
confidence: e.confidence,
|
|
2036
|
-
evidence: e.evidence
|
|
2037
|
-
}))
|
|
2038
|
-
});
|
|
2039
|
-
return `<!DOCTYPE html>
|
|
2040
|
-
<html lang="en">
|
|
2041
|
-
<head>
|
|
2042
|
-
<meta charset="UTF-8">
|
|
2043
|
-
<title>Cartography \u2014 Infrastructure Map</title>
|
|
2044
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
2045
|
-
<style>
|
|
2046
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2047
|
-
body { background: #0a0e14; color: #e6edf3; font-family: 'SF Mono','Fira Code','Cascadia Code',monospace; display: flex; overflow: hidden; height: 100vh; }
|
|
2048
|
-
|
|
2049
|
-
/* \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 */
|
|
2050
|
-
#node-panel {
|
|
2051
|
-
width: 220px; min-width: 220px; height: 100vh; overflow: hidden;
|
|
2052
|
-
background: #0d1117; border-right: 1px solid #1b2028;
|
|
2053
|
-
display: flex; flex-direction: column;
|
|
2054
|
-
}
|
|
2055
|
-
#node-panel-header {
|
|
2056
|
-
padding: 10px 12px 8px; border-bottom: 1px solid #1b2028;
|
|
2057
|
-
font-size: 11px; color: #6e7681; text-transform: uppercase; letter-spacing: 0.6px;
|
|
2058
|
-
}
|
|
2059
|
-
#node-search {
|
|
2060
|
-
width: calc(100% - 16px); margin: 8px; padding: 5px 8px;
|
|
2061
|
-
background: #161b22; border: 1px solid #30363d; border-radius: 5px;
|
|
2062
|
-
color: #e6edf3; font-size: 11px; font-family: inherit; outline: none;
|
|
2063
|
-
}
|
|
2064
|
-
#node-search:focus { border-color: #58a6ff; }
|
|
2065
|
-
#node-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
|
|
2066
|
-
.node-list-item {
|
|
2067
|
-
padding: 5px 12px; cursor: pointer; font-size: 11px;
|
|
2068
|
-
display: flex; align-items: center; gap: 6px; border-left: 2px solid transparent;
|
|
2069
|
-
}
|
|
2070
|
-
.node-list-item:hover { background: #161b22; }
|
|
2071
|
-
.node-list-item.active { background: #1a2436; border-left-color: #58a6ff; }
|
|
2072
|
-
.node-list-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }
|
|
2073
|
-
.node-list-name { color: #c9d1d9; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
2074
|
-
.node-list-type { color: #484f58; font-size: 9px; flex-shrink: 0; }
|
|
2075
|
-
|
|
2076
|
-
/* \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 */
|
|
2077
|
-
#graph { flex: 1; height: 100vh; position: relative; }
|
|
2078
|
-
svg { width: 100%; height: 100%; }
|
|
2079
|
-
.hull { opacity: 0.12; stroke-width: 1.5; stroke-opacity: 0.25; }
|
|
2080
|
-
.hull-label { font-size: 13px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; fill-opacity: 0.5; pointer-events: none; }
|
|
2081
|
-
.link { stroke-opacity: 0.4; }
|
|
2082
|
-
.link-label { font-size: 8px; fill: #6e7681; pointer-events: none; opacity: 0; }
|
|
2083
|
-
.node-hex { stroke-width: 1.8; cursor: pointer; transition: opacity 0.15s; }
|
|
2084
|
-
.node-hex:hover { filter: brightness(1.3); stroke-width: 3; }
|
|
2085
|
-
.node-hex.selected { stroke-width: 3.5; filter: brightness(1.5); }
|
|
2086
|
-
.node-label { font-size: 10px; fill: #c9d1d9; pointer-events: none; opacity: 0; }
|
|
2087
|
-
|
|
2088
|
-
/* \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 */
|
|
2089
|
-
#sidebar {
|
|
2090
|
-
width: 300px; min-width: 300px; height: 100vh; overflow-y: auto;
|
|
2091
|
-
background: #0d1117; border-left: 1px solid #1b2028;
|
|
2092
|
-
padding: 16px; font-size: 12px; line-height: 1.6;
|
|
2093
|
-
}
|
|
2094
|
-
#sidebar h2 { margin: 0 0 8px; font-size: 14px; color: #58a6ff; }
|
|
2095
|
-
#sidebar .meta-table { width: 100%; border-collapse: collapse; }
|
|
2096
|
-
#sidebar .meta-table td { padding: 3px 6px; border-bottom: 1px solid #161b22; vertical-align: top; }
|
|
2097
|
-
#sidebar .meta-table td:first-child { color: #6e7681; white-space: nowrap; width: 90px; }
|
|
2098
|
-
#sidebar .tag { display: inline-block; background: #161b22; border-radius: 3px; padding: 1px 5px; margin: 1px; font-size: 10px; }
|
|
2099
|
-
#sidebar .conf-bar { height: 5px; border-radius: 3px; background: #161b22; margin-top: 3px; }
|
|
2100
|
-
#sidebar .conf-fill { height: 100%; border-radius: 3px; }
|
|
2101
|
-
#sidebar .edges-list { margin-top: 12px; }
|
|
2102
|
-
#sidebar .edge-item { padding: 4px 0; border-bottom: 1px solid #161b22; color: #6e7681; font-size: 11px; }
|
|
2103
|
-
#sidebar .edge-item span { color: #c9d1d9; }
|
|
2104
|
-
#sidebar .action-row { display: flex; gap: 6px; margin-top: 14px; }
|
|
2105
|
-
.btn-delete {
|
|
2106
|
-
flex: 1; padding: 6px 10px; background: transparent; border: 1px solid #6e191d;
|
|
2107
|
-
color: #f85149; border-radius: 5px; font-size: 11px; font-family: inherit;
|
|
2108
|
-
cursor: pointer; text-align: center;
|
|
2109
|
-
}
|
|
2110
|
-
.btn-delete:hover { background: #3d0c0c; }
|
|
2111
|
-
.hint { color: #3d434b; font-size: 11px; margin-top: 8px; }
|
|
2112
|
-
|
|
2113
|
-
/* \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 */
|
|
2114
|
-
#hud { position: absolute; top: 10px; left: 10px; background: rgba(10,14,20,0.88);
|
|
2115
|
-
padding: 10px 14px; border-radius: 8px; font-size: 12px; border: 1px solid #1b2028; pointer-events: none; }
|
|
2116
|
-
#hud strong { color: #58a6ff; }
|
|
2117
|
-
#hud .stats { color: #6e7681; }
|
|
2118
|
-
#hud .zoom-level { color: #3d434b; font-size: 10px; margin-top: 2px; }
|
|
2119
|
-
|
|
2120
|
-
/* \u2500\u2500 Toolbar (filters + JGF export) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2121
|
-
#toolbar { position: absolute; top: 10px; right: 10px; display: flex; flex-wrap: wrap; gap: 4px; pointer-events: auto; align-items: center; }
|
|
2122
|
-
.filter-btn {
|
|
2123
|
-
background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
|
|
2124
|
-
color: #c9d1d9; padding: 4px 10px; font-size: 11px; cursor: pointer;
|
|
2125
|
-
font-family: inherit; display: flex; align-items: center; gap: 5px;
|
|
2126
|
-
}
|
|
2127
|
-
.filter-btn:hover { border-color: #30363d; }
|
|
2128
|
-
.filter-btn.off { opacity: 0.35; }
|
|
2129
|
-
.filter-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
|
|
2130
|
-
.export-btn {
|
|
2131
|
-
background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
|
|
2132
|
-
color: #58a6ff; padding: 4px 12px; font-size: 11px; cursor: pointer;
|
|
2133
|
-
font-family: inherit;
|
|
2134
|
-
}
|
|
2135
|
-
.export-btn:hover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
|
|
2136
|
-
</style>
|
|
2137
|
-
</head>
|
|
2138
|
-
<body>
|
|
2139
|
-
|
|
2140
|
-
<!-- Left: node list panel -->
|
|
2141
|
-
<div id="node-panel">
|
|
2142
|
-
<div id="node-panel-header">Nodes (${nodes.length})</div>
|
|
2143
|
-
<input id="node-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false">
|
|
2144
|
-
<div id="node-list"></div>
|
|
2145
|
-
</div>
|
|
2146
|
-
|
|
2147
|
-
<!-- Center: graph -->
|
|
2148
|
-
<div id="graph">
|
|
2149
|
-
<div id="hud">
|
|
2150
|
-
<strong>Cartography</strong>
|
|
2151
|
-
<span class="stats" id="hud-stats">${nodes.length} nodes \xB7 ${edges.length} edges</span><br>
|
|
2152
|
-
<span class="zoom-level">Scroll = zoom \xB7 Drag = pan \xB7 Click = details</span>
|
|
2153
|
-
</div>
|
|
2154
|
-
<div id="toolbar"></div>
|
|
2155
|
-
<svg></svg>
|
|
2156
|
-
</div>
|
|
2157
|
-
|
|
2158
|
-
<!-- Right: detail sidebar -->
|
|
2159
|
-
<div id="sidebar">
|
|
2160
|
-
<h2>Infrastructure Map</h2>
|
|
2161
|
-
<p class="hint">Click a node to view details.</p>
|
|
2162
|
-
</div>
|
|
2163
|
-
|
|
2164
|
-
<script>
|
|
2165
|
-
const data = ${graphData};
|
|
2166
|
-
|
|
2167
|
-
// \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
|
|
2168
|
-
const TYPE_COLORS = {
|
|
2169
|
-
host: '#4a9eff', database_server: '#ff6b6b', database: '#ff8c42',
|
|
2170
|
-
web_service: '#6bcb77', api_endpoint: '#4d96ff', cache_server: '#ffd93d',
|
|
2171
|
-
message_broker: '#c77dff', queue: '#e0aaff', topic: '#9d4edd',
|
|
2172
|
-
container: '#48cae4', pod: '#00b4d8', k8s_cluster: '#0077b6',
|
|
2173
|
-
config_file: '#adb5bd', saas_tool: '#c084fc', table: '#f97316', unknown: '#6c757d',
|
|
2174
|
-
};
|
|
2175
|
-
|
|
2176
|
-
const LAYER_COLORS = {
|
|
2177
|
-
saas: '#c084fc', web: '#6bcb77', data: '#ff6b6b',
|
|
2178
|
-
messaging: '#c77dff', infra: '#4a9eff', config: '#adb5bd', other: '#6c757d',
|
|
2179
|
-
};
|
|
2180
|
-
const LAYER_NAMES = {
|
|
2181
|
-
saas: 'SaaS Tools', web: 'Web / API', data: 'Data Layer',
|
|
2182
|
-
messaging: 'Messaging', infra: 'Infrastructure', config: 'Config', other: 'Other',
|
|
2183
|
-
};
|
|
2184
|
-
|
|
2185
|
-
// \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
|
|
2186
|
-
const HEX_SIZE = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
|
|
2187
|
-
function hexSize(d) { return HEX_SIZE[d.type] || HEX_SIZE.default; }
|
|
2188
|
-
function hexPath(size) {
|
|
2189
|
-
const pts = [];
|
|
2190
|
-
for (let i = 0; i < 6; i++) {
|
|
2191
|
-
const angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
2192
|
-
pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
|
|
2193
|
-
}
|
|
2194
|
-
return 'M' + pts.map(p => p.join(',')).join('L') + 'Z';
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
// \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
|
|
2198
|
-
const nodeListEl = document.getElementById('node-list');
|
|
2199
|
-
const nodeSearchEl = document.getElementById('node-search');
|
|
2200
|
-
let selectedNodeId = null;
|
|
2201
|
-
|
|
2202
|
-
function buildNodeList(filter) {
|
|
2203
|
-
const q = (filter || '').toLowerCase();
|
|
2204
|
-
nodeListEl.innerHTML = '';
|
|
2205
|
-
const sorted = [...data.nodes].sort((a, b) => a.name.localeCompare(b.name));
|
|
2206
|
-
for (const d of sorted) {
|
|
2207
|
-
if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
|
|
2208
|
-
const item = document.createElement('div');
|
|
2209
|
-
item.className = 'node-list-item' + (d.id === selectedNodeId ? ' active' : '');
|
|
2210
|
-
item.dataset.id = d.id;
|
|
2211
|
-
const color = TYPE_COLORS[d.type] || '#aaa';
|
|
2212
|
-
item.innerHTML = \`<span class="node-list-dot" style="background:\${color}"></span>
|
|
2213
|
-
<span class="node-list-name" title="\${d.id}">\${d.name}</span>
|
|
2214
|
-
<span class="node-list-type">\${d.type.replace(/_/g,' ')}</span>\`;
|
|
2215
|
-
item.onclick = () => { selectNode(d); focusNode(d); };
|
|
2216
|
-
nodeListEl.appendChild(item);
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
nodeSearchEl.addEventListener('input', e => buildNodeList(e.target.value));
|
|
2221
|
-
|
|
2222
|
-
// \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
|
|
2223
|
-
const sidebar = document.getElementById('sidebar');
|
|
2224
|
-
|
|
2225
|
-
function selectNode(d) {
|
|
2226
|
-
selectedNodeId = d.id;
|
|
2227
|
-
buildNodeList(nodeSearchEl.value);
|
|
2228
|
-
showNode(d);
|
|
2229
|
-
// highlight hex
|
|
2230
|
-
d3.selectAll('.node-hex').classed('selected', nd => nd.id === d.id);
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
function showNode(d) {
|
|
2234
|
-
const c = TYPE_COLORS[d.type] || '#aaa';
|
|
2235
|
-
const confPct = Math.round(d.confidence * 100);
|
|
2236
|
-
const tags = (d.tags || []).map(t => \`<span class="tag">\${t}</span>\`).join('');
|
|
2237
|
-
const metaRows = Object.entries(d.metadata || {})
|
|
2238
|
-
.filter(([,v]) => v !== null && v !== undefined && String(v).length > 0)
|
|
2239
|
-
.map(([k,v]) => \`<tr><td>\${k}</td><td>\${JSON.stringify(v)}</td></tr>\`)
|
|
2240
|
-
.join('');
|
|
2241
|
-
const related = data.links.filter(l =>
|
|
2242
|
-
(l.source.id||l.source) === d.id || (l.target.id||l.target) === d.id
|
|
2243
|
-
);
|
|
2244
|
-
const edgeItems = related.map(l => {
|
|
2245
|
-
const isOut = (l.source.id||l.source) === d.id;
|
|
2246
|
-
const other = isOut ? (l.target.id||l.target) : (l.source.id||l.source);
|
|
2247
|
-
return \`<div class="edge-item">\${isOut ? '\u2192' : '\u2190'} <span>\${other}</span> <small>[\${l.relationship}]</small></div>\`;
|
|
2248
|
-
}).join('');
|
|
2249
|
-
|
|
2250
|
-
sidebar.innerHTML = \`
|
|
2251
|
-
<h2>\${d.name}</h2>
|
|
2252
|
-
<table class="meta-table">
|
|
2253
|
-
<tr><td>ID</td><td style="font-size:10px;word-break:break-all">\${d.id}</td></tr>
|
|
2254
|
-
<tr><td>Type</td><td><span style="color:\${c}">\${d.type}</span></td></tr>
|
|
2255
|
-
<tr><td>Layer</td><td>\${d.layer}</td></tr>
|
|
2256
|
-
<tr><td>Confidence</td><td>
|
|
2257
|
-
\${confPct}%
|
|
2258
|
-
<div class="conf-bar"><div class="conf-fill" style="width:\${confPct}%;background:\${c}"></div></div>
|
|
2259
|
-
</td></tr>
|
|
2260
|
-
<tr><td>Discovered via</td><td>\${d.discoveredVia || '\u2014'}</td></tr>
|
|
2261
|
-
<tr><td>Timestamp</td><td>\${d.discoveredAt ? d.discoveredAt.substring(0,19).replace('T',' ') : '\u2014'}</td></tr>
|
|
2262
|
-
\${tags ? '<tr><td>Tags</td><td>'+tags+'</td></tr>' : ''}
|
|
2263
|
-
\${metaRows}
|
|
2264
|
-
</table>
|
|
2265
|
-
\${related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>'+edgeItems+'</div>' : ''}
|
|
2266
|
-
<div class="action-row">
|
|
2267
|
-
<button class="btn-delete" onclick="deleteNode('\${d.id}')">\u{1F5D1} Delete node</button>
|
|
2268
|
-
</div>
|
|
2269
|
-
\`;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
// \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
|
|
2273
|
-
function deleteNode(id) {
|
|
2274
|
-
const idx = data.nodes.findIndex(n => n.id === id);
|
|
2275
|
-
if (idx === -1) return;
|
|
2276
|
-
data.nodes.splice(idx, 1);
|
|
2277
|
-
data.links = data.links.filter(l =>
|
|
2278
|
-
(l.source.id || l.source) !== id && (l.target.id || l.target) !== id
|
|
2279
|
-
);
|
|
2280
|
-
selectedNodeId = null;
|
|
2281
|
-
sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
|
|
2282
|
-
document.getElementById('hud-stats').textContent =
|
|
2283
|
-
data.nodes.length + ' nodes \xB7 ' + data.links.length + ' edges';
|
|
2284
|
-
rebuildGraph();
|
|
2285
|
-
buildNodeList(nodeSearchEl.value);
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
// \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
|
|
2289
|
-
const svgEl = d3.select('svg');
|
|
2290
|
-
const graphDiv = document.getElementById('graph');
|
|
2291
|
-
const W = () => graphDiv.clientWidth;
|
|
2292
|
-
const H = () => graphDiv.clientHeight;
|
|
2293
|
-
const g = svgEl.append('g');
|
|
2294
|
-
|
|
2295
|
-
svgEl.append('defs').append('marker')
|
|
2296
|
-
.attr('id', 'arrow').attr('viewBox', '0 0 10 6')
|
|
2297
|
-
.attr('refX', 10).attr('refY', 3)
|
|
2298
|
-
.attr('markerWidth', 8).attr('markerHeight', 6)
|
|
2299
|
-
.attr('orient', 'auto')
|
|
2300
|
-
.append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
|
|
2301
|
-
|
|
2302
|
-
let currentZoom = 1;
|
|
2303
|
-
const zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', e => {
|
|
2304
|
-
g.attr('transform', e.transform);
|
|
2305
|
-
currentZoom = e.transform.k;
|
|
2306
|
-
updateLOD(currentZoom);
|
|
2307
|
-
});
|
|
2308
|
-
svgEl.call(zoomBehavior);
|
|
2309
|
-
|
|
2310
|
-
// \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
|
|
2311
|
-
const layers = [...new Set(data.nodes.map(d => d.layer))];
|
|
2312
|
-
const layerVisible = {};
|
|
2313
|
-
layers.forEach(l => layerVisible[l] = true);
|
|
2314
|
-
|
|
2315
|
-
const toolbarEl = document.getElementById('toolbar');
|
|
2316
|
-
|
|
2317
|
-
// Filter buttons
|
|
2318
|
-
layers.forEach(layer => {
|
|
2319
|
-
const btn = document.createElement('button');
|
|
2320
|
-
btn.className = 'filter-btn';
|
|
2321
|
-
btn.innerHTML = \`<span class="filter-dot" style="background:\${LAYER_COLORS[layer]||'#666'}"></span>\${LAYER_NAMES[layer]||layer}\`;
|
|
2322
|
-
btn.onclick = () => {
|
|
2323
|
-
layerVisible[layer] = !layerVisible[layer];
|
|
2324
|
-
btn.classList.toggle('off', !layerVisible[layer]);
|
|
2325
|
-
updateVisibility();
|
|
2326
|
-
};
|
|
2327
|
-
toolbarEl.appendChild(btn);
|
|
2328
|
-
});
|
|
2329
|
-
|
|
2330
|
-
// JGF export button
|
|
2331
|
-
const jgfBtn = document.createElement('button');
|
|
2332
|
-
jgfBtn.className = 'export-btn';
|
|
2333
|
-
jgfBtn.textContent = '\u2193 JGF';
|
|
2334
|
-
jgfBtn.title = 'Export JSON Graph Format';
|
|
2335
|
-
jgfBtn.onclick = exportJGF;
|
|
2336
|
-
toolbarEl.appendChild(jgfBtn);
|
|
2337
|
-
|
|
2338
|
-
// \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
|
|
2339
|
-
function exportJGF() {
|
|
2340
|
-
const jgf = {
|
|
2341
|
-
graph: {
|
|
2342
|
-
directed: true,
|
|
2343
|
-
type: 'cartography',
|
|
2344
|
-
label: 'Infrastructure Map',
|
|
2345
|
-
metadata: { exportedAt: new Date().toISOString() },
|
|
2346
|
-
nodes: Object.fromEntries(data.nodes.map(n => [n.id, {
|
|
2347
|
-
label: n.name,
|
|
2348
|
-
metadata: { type: n.type, layer: n.layer, confidence: n.confidence,
|
|
2349
|
-
discoveredVia: n.discoveredVia, discoveredAt: n.discoveredAt,
|
|
2350
|
-
tags: n.tags, ...n.metadata }
|
|
2351
|
-
}])),
|
|
2352
|
-
edges: data.links.map(l => ({
|
|
2353
|
-
source: l.source.id || l.source,
|
|
2354
|
-
target: l.target.id || l.target,
|
|
2355
|
-
relation: l.relationship,
|
|
2356
|
-
metadata: { confidence: l.confidence, evidence: l.evidence }
|
|
2357
|
-
})),
|
|
2358
|
-
}
|
|
2359
|
-
};
|
|
2360
|
-
const blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
|
|
2361
|
-
const url = URL.createObjectURL(blob);
|
|
2362
|
-
const a = document.createElement('a');
|
|
2363
|
-
a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
|
|
2364
|
-
URL.revokeObjectURL(url);
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
// \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
|
|
2368
|
-
function clusterForce(alpha) {
|
|
2369
|
-
const centroids = {};
|
|
2370
|
-
const counts = {};
|
|
2371
|
-
data.nodes.forEach(d => {
|
|
2372
|
-
if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
|
|
2373
|
-
centroids[d.layer].x += d.x || 0;
|
|
2374
|
-
centroids[d.layer].y += d.y || 0;
|
|
2375
|
-
counts[d.layer]++;
|
|
2376
|
-
});
|
|
2377
|
-
for (const l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
|
|
2378
|
-
const strength = alpha * 0.15;
|
|
2379
|
-
data.nodes.forEach(d => {
|
|
2380
|
-
const c = centroids[d.layer];
|
|
2381
|
-
if (c) { d.vx += (c.x - d.x) * strength; d.vy += (c.y - d.y) * strength; }
|
|
2382
|
-
});
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
// \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
|
|
2386
|
-
const hullGroup = g.append('g').attr('class', 'hulls');
|
|
2387
|
-
const hullPaths = {};
|
|
2388
|
-
const hullLabels = {};
|
|
2389
|
-
layers.forEach(layer => {
|
|
2390
|
-
hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
|
|
2391
|
-
.attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
|
|
2392
|
-
hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
|
|
2393
|
-
.attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
|
|
2394
|
-
});
|
|
2395
|
-
|
|
2396
|
-
function updateHulls() {
|
|
2397
|
-
layers.forEach(layer => {
|
|
2398
|
-
if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
|
|
2399
|
-
const pts = data.nodes.filter(d => d.layer === layer && layerVisible[d.layer]).map(d => [d.x, d.y]);
|
|
2400
|
-
if (pts.length < 3) {
|
|
2401
|
-
hullPaths[layer].attr('d', null);
|
|
2402
|
-
if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
|
|
2403
|
-
else hullLabels[layer].attr('x', -9999);
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
const hull = d3.polygonHull(pts);
|
|
2407
|
-
if (!hull) { hullPaths[layer].attr('d', null); return; }
|
|
2408
|
-
const cx = d3.mean(hull, p => p[0]);
|
|
2409
|
-
const cy = d3.mean(hull, p => p[1]);
|
|
2410
|
-
const padded = hull.map(p => {
|
|
2411
|
-
const dx = p[0] - cx, dy = p[1] - cy;
|
|
2412
|
-
const len = Math.sqrt(dx*dx + dy*dy) || 1;
|
|
2413
|
-
return [p[0] + dx/len * 40, p[1] + dy/len * 40];
|
|
2414
|
-
});
|
|
2415
|
-
hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
|
|
2416
|
-
hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, p => Math.abs(p[1] - cy)) - 30);
|
|
2417
|
-
});
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
// \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
|
|
2421
|
-
let linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
|
|
2422
|
-
const linkGroup = g.append('g');
|
|
2423
|
-
const nodeGroup = g.append('g');
|
|
2424
|
-
|
|
2425
|
-
function focusNode(d) {
|
|
2426
|
-
if (!d.x || !d.y) return;
|
|
2427
|
-
const w = W(), h = H();
|
|
2428
|
-
svgEl.transition().duration(500).call(
|
|
2429
|
-
zoomBehavior.transform,
|
|
2430
|
-
d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
|
|
2431
|
-
);
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
function rebuildGraph() {
|
|
2435
|
-
if (sim) sim.stop();
|
|
2436
|
-
|
|
2437
|
-
// Links
|
|
2438
|
-
linkSel = linkGroup.selectAll('line').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
|
|
2439
|
-
linkSel.exit().remove();
|
|
2440
|
-
const linkEnter = linkSel.enter().append('line').attr('class', 'link');
|
|
2441
|
-
linkSel = linkEnter.merge(linkSel)
|
|
2442
|
-
.attr('stroke', d => d.confidence < 0.6 ? '#2a2e35' : '#3d434b')
|
|
2443
|
-
.attr('stroke-dasharray', d => d.confidence < 0.6 ? '4 3' : null)
|
|
2444
|
-
.attr('stroke-width', d => d.confidence < 0.6 ? 0.8 : 1.2)
|
|
2445
|
-
.attr('marker-end', 'url(#arrow)');
|
|
2446
|
-
linkSel.select('title').remove();
|
|
2447
|
-
linkSel.append('title').text(d => \`\${d.relationship} (\${Math.round(d.confidence*100)}%)
|
|
2448
|
-
\${d.evidence||''}\`);
|
|
2449
|
-
|
|
2450
|
-
// Link labels
|
|
2451
|
-
linkLabelSel = linkGroup.selectAll('text').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
|
|
2452
|
-
linkLabelSel.exit().remove();
|
|
2453
|
-
linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel)
|
|
2454
|
-
.text(d => d.relationship);
|
|
2455
|
-
|
|
2456
|
-
// Nodes
|
|
2457
|
-
nodeSel = nodeGroup.selectAll('g').data(data.nodes, d => d.id);
|
|
2458
|
-
nodeSel.exit().remove();
|
|
2459
|
-
const nodeEnter = nodeSel.enter().append('g')
|
|
2460
|
-
.call(d3.drag()
|
|
2461
|
-
.on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
2462
|
-
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
2463
|
-
.on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
2464
|
-
)
|
|
2465
|
-
.on('click', (e, d) => { e.stopPropagation(); selectNode(d); });
|
|
2466
|
-
nodeEnter.append('path').attr('class', 'node-hex');
|
|
2467
|
-
nodeEnter.append('title');
|
|
2468
|
-
nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
|
|
2469
|
-
|
|
2470
|
-
nodeSel = nodeEnter.merge(nodeSel);
|
|
2471
|
-
nodeSel.select('.node-hex')
|
|
2472
|
-
.attr('d', d => hexPath(hexSize(d)))
|
|
2473
|
-
.attr('fill', d => TYPE_COLORS[d.type] || '#aaa')
|
|
2474
|
-
.attr('stroke', d => { const c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
|
|
2475
|
-
.attr('fill-opacity', d => 0.6 + d.confidence * 0.4)
|
|
2476
|
-
.classed('selected', d => d.id === selectedNodeId);
|
|
2477
|
-
nodeSel.select('title').text(d => \`\${d.name} (\${d.type})
|
|
2478
|
-
conf: \${Math.round(d.confidence*100)}%\`);
|
|
2479
|
-
nodeLabelSel = nodeSel.select('.node-label')
|
|
2480
|
-
.attr('dy', d => hexSize(d) + 13)
|
|
2481
|
-
.text(d => d.name.length > 20 ? d.name.substring(0, 18) + '\u2026' : d.name);
|
|
2482
|
-
|
|
2483
|
-
// Simulation
|
|
2484
|
-
sim = d3.forceSimulation(data.nodes)
|
|
2485
|
-
.force('link', d3.forceLink(data.links).id(d => d.id).distance(d => d.relationship === 'contains' ? 50 : 100).strength(0.4))
|
|
2486
|
-
.force('charge', d3.forceManyBody().strength(-280))
|
|
2487
|
-
.force('center', d3.forceCenter(W() / 2, H() / 2))
|
|
2488
|
-
.force('collision', d3.forceCollide().radius(d => hexSize(d) + 10))
|
|
2489
|
-
.force('cluster', clusterForce)
|
|
2490
|
-
.on('tick', () => {
|
|
2491
|
-
updateHulls();
|
|
2492
|
-
linkSel.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
2493
|
-
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
2494
|
-
linkLabelSel.attr('x', d => (d.source.x + d.target.x) / 2)
|
|
2495
|
-
.attr('y', d => (d.source.y + d.target.y) / 2 - 4);
|
|
2496
|
-
nodeSel.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
2497
|
-
});
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
// \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
|
|
2501
|
-
function updateLOD(k) {
|
|
2502
|
-
if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
|
|
2503
|
-
if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
|
|
2504
|
-
d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
function updateVisibility() {
|
|
2508
|
-
if (!nodeSel) return;
|
|
2509
|
-
nodeSel.style('display', d => layerVisible[d.layer] ? null : 'none');
|
|
2510
|
-
linkSel.style('display', d => {
|
|
2511
|
-
const s = data.nodes.find(n => n.id === (d.source.id||d.source));
|
|
2512
|
-
const t = data.nodes.find(n => n.id === (d.target.id||d.target));
|
|
2513
|
-
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2514
|
-
});
|
|
2515
|
-
linkLabelSel.style('display', d => {
|
|
2516
|
-
const s = data.nodes.find(n => n.id === (d.source.id||d.source));
|
|
2517
|
-
const t = data.nodes.find(n => n.id === (d.target.id||d.target));
|
|
2518
|
-
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2519
|
-
});
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
// \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
|
|
2523
|
-
rebuildGraph();
|
|
2524
|
-
buildNodeList();
|
|
2525
|
-
updateLOD(1);
|
|
2526
|
-
|
|
2527
|
-
svgEl.on('click', () => {
|
|
2528
|
-
selectedNodeId = null;
|
|
2529
|
-
d3.selectAll('.node-hex').classed('selected', false);
|
|
2530
|
-
buildNodeList(nodeSearchEl.value);
|
|
2531
|
-
sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
|
|
2532
|
-
});
|
|
2533
|
-
</script>
|
|
2534
|
-
</body>
|
|
2535
|
-
</html>`;
|
|
2536
|
-
}
|
|
2537
2018
|
function exportSOPMarkdown(sop) {
|
|
2538
2019
|
const lines = [
|
|
2539
2020
|
`# ${sop.title}`,
|
|
@@ -2746,12 +2227,32 @@ function toggle(i) {
|
|
|
2746
2227
|
</body>
|
|
2747
2228
|
</html>`;
|
|
2748
2229
|
}
|
|
2749
|
-
function
|
|
2750
|
-
const
|
|
2751
|
-
const
|
|
2230
|
+
function exportDiscoveryApp(nodes, edges, options) {
|
|
2231
|
+
const theme = options?.theme ?? "dark";
|
|
2232
|
+
const graphData = JSON.stringify({
|
|
2233
|
+
nodes: nodes.map((n) => ({
|
|
2234
|
+
id: n.id,
|
|
2235
|
+
name: n.name,
|
|
2236
|
+
type: n.type,
|
|
2237
|
+
layer: nodeLayer(n.type),
|
|
2238
|
+
confidence: n.confidence,
|
|
2239
|
+
discoveredVia: n.discoveredVia,
|
|
2240
|
+
discoveredAt: n.discoveredAt,
|
|
2241
|
+
tags: n.tags,
|
|
2242
|
+
metadata: n.metadata
|
|
2243
|
+
})),
|
|
2244
|
+
links: edges.map((e) => ({
|
|
2245
|
+
source: e.sourceId,
|
|
2246
|
+
target: e.targetId,
|
|
2247
|
+
relationship: e.relationship,
|
|
2248
|
+
confidence: e.confidence,
|
|
2249
|
+
evidence: e.evidence
|
|
2250
|
+
}))
|
|
2251
|
+
});
|
|
2252
|
+
const { assets, clusters, connections } = buildMapData(nodes, edges, { theme });
|
|
2752
2253
|
const isEmpty = assets.length === 0;
|
|
2753
2254
|
const HEX_SIZE2 = 24;
|
|
2754
|
-
const
|
|
2255
|
+
const mapJson = JSON.stringify({
|
|
2755
2256
|
assets: assets.map((a) => ({
|
|
2756
2257
|
id: a.id,
|
|
2757
2258
|
name: a.name,
|
|
@@ -2777,621 +2278,1081 @@ function exportCartographyMap(nodes, edges, options) {
|
|
|
2777
2278
|
type: c.type ?? "connection"
|
|
2778
2279
|
}))
|
|
2779
2280
|
});
|
|
2281
|
+
const nodeCount = nodes.length;
|
|
2282
|
+
const edgeCount = edges.length;
|
|
2283
|
+
const assetCount = assets.length;
|
|
2284
|
+
const clusterCount = clusters.length;
|
|
2780
2285
|
return `<!DOCTYPE html>
|
|
2781
|
-
<html lang="en">
|
|
2286
|
+
<html lang="en" data-theme="${theme}">
|
|
2782
2287
|
<head>
|
|
2783
2288
|
<meta charset="UTF-8"/>
|
|
2784
|
-
<meta name="viewport" content="width=device-width,
|
|
2785
|
-
<title>
|
|
2289
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
2290
|
+
<title>Cartography \u2014 Datasynx Discovery</title>
|
|
2291
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
2786
2292
|
<style>
|
|
2293
|
+
/* \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 */
|
|
2294
|
+
:root{
|
|
2295
|
+
--bg-base:#0f172a;--bg-surface:#1e293b;--bg-elevated:#273148;
|
|
2296
|
+
--border:#334155;--border-dim:#1e293b;
|
|
2297
|
+
--text:#e2e8f0;--text-muted:#94a3b8;--text-dim:#475569;
|
|
2298
|
+
--accent:#3b82f6;--accent-hover:#2563eb;--accent-dim:rgba(59,130,246,.12);
|
|
2299
|
+
}
|
|
2300
|
+
[data-theme="light"]{
|
|
2301
|
+
--bg-base:#f8fafc;--bg-surface:#ffffff;--bg-elevated:#f1f5f9;
|
|
2302
|
+
--border:#e2e8f0;--border-dim:#f1f5f9;
|
|
2303
|
+
--text:#0f172a;--text-muted:#64748b;--text-dim:#94a3b8;
|
|
2304
|
+
--accent:#2563eb;--accent-hover:#1d4ed8;--accent-dim:rgba(37,99,235,.08);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/* \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 */
|
|
2787
2308
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
2788
|
-
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
2789
|
-
body{display:flex;flex-direction:column;background
|
|
2309
|
+
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',sans-serif}
|
|
2310
|
+
body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--text)}
|
|
2311
|
+
|
|
2312
|
+
/* \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 */
|
|
2790
2313
|
#topbar{
|
|
2791
|
-
height:
|
|
2792
|
-
background
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2314
|
+
height:56px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2315
|
+
background:var(--bg-surface);border-bottom:1px solid var(--border);z-index:100;flex-shrink:0;
|
|
2316
|
+
}
|
|
2317
|
+
.tb-left{display:flex;align-items:center;gap:10px}
|
|
2318
|
+
.brand-logo{flex-shrink:0}
|
|
2319
|
+
.brand-name{font-size:15px;font-weight:700;color:var(--accent);letter-spacing:-.02em}
|
|
2320
|
+
.brand-product{font-size:14px;font-weight:500;color:var(--text-muted);margin-left:2px}
|
|
2321
|
+
.brand-sep{width:1px;height:24px;background:var(--border);margin:0 6px}
|
|
2322
|
+
.tb-center{display:flex;align-items:center;gap:2px;margin-left:auto;
|
|
2323
|
+
background:var(--bg-elevated);border-radius:8px;padding:3px}
|
|
2324
|
+
.tab-btn{
|
|
2325
|
+
padding:6px 16px;border:none;border-radius:6px;font-size:13px;font-weight:500;
|
|
2326
|
+
cursor:pointer;color:var(--text-muted);background:transparent;font-family:inherit;
|
|
2327
|
+
transition:all .15s;
|
|
2328
|
+
}
|
|
2329
|
+
.tab-btn:hover{color:var(--text)}
|
|
2330
|
+
.tab-btn.active{background:var(--accent);color:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2)}
|
|
2331
|
+
.tb-right{display:flex;align-items:center;gap:8px;margin-left:auto}
|
|
2332
|
+
.tb-search{
|
|
2333
|
+
display:flex;align-items:center;gap:6px;background:var(--bg-elevated);
|
|
2334
|
+
border:1px solid var(--border);border-radius:8px;padding:5px 10px;
|
|
2335
|
+
}
|
|
2336
|
+
.tb-search input{
|
|
2337
|
+
border:none;background:transparent;font-size:13px;outline:none;width:160px;
|
|
2338
|
+
color:var(--text);font-family:inherit;
|
|
2339
|
+
}
|
|
2340
|
+
.tb-search input::placeholder{color:var(--text-dim)}
|
|
2341
|
+
.tb-search svg{flex-shrink:0;color:var(--text-dim)}
|
|
2342
|
+
.icon-btn{
|
|
2343
|
+
width:36px;height:36px;border-radius:8px;border:1px solid var(--border);
|
|
2344
|
+
background:var(--bg-surface);cursor:pointer;display:flex;align-items:center;
|
|
2345
|
+
justify-content:center;color:var(--text-muted);text-decoration:none;transition:all .15s;font-size:16px;
|
|
2346
|
+
}
|
|
2347
|
+
.icon-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
|
|
2348
|
+
.tb-stats{font-size:11px;color:var(--text-dim);white-space:nowrap}
|
|
2349
|
+
|
|
2350
|
+
/* \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 */
|
|
2351
|
+
.view{flex:1;display:none;overflow:hidden;position:relative}
|
|
2352
|
+
.view.active{display:flex}
|
|
2353
|
+
|
|
2354
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2355
|
+
MAP VIEW
|
|
2356
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
2357
|
+
#map-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2358
|
+
#map-wrap.dragging{cursor:grabbing}
|
|
2359
|
+
#map-wrap.connecting{cursor:crosshair}
|
|
2360
|
+
#map-wrap canvas{display:block;width:100%;height:100%}
|
|
2361
|
+
#map-detail{
|
|
2362
|
+
width:280px;background:var(--bg-surface);border-left:1px solid var(--border);
|
|
2811
2363
|
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2812
2364
|
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2813
2365
|
}
|
|
2814
|
-
#detail
|
|
2815
|
-
#detail
|
|
2816
|
-
padding:16px;border-bottom:1px solid
|
|
2366
|
+
#map-detail.open{transform:translateX(0)}
|
|
2367
|
+
#map-detail .panel-header{
|
|
2368
|
+
padding:16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;
|
|
2817
2369
|
}
|
|
2818
|
-
#detail
|
|
2819
|
-
|
|
2370
|
+
#map-detail .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2371
|
+
.close-btn{
|
|
2820
2372
|
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2821
|
-
color
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
}
|
|
2834
|
-
.tb-
|
|
2835
|
-
width:40px;height:40px;border-radius:10px;border:1px solid
|
|
2836
|
-
background
|
|
2373
|
+
color:var(--text-muted);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2374
|
+
}
|
|
2375
|
+
.close-btn:hover{background:var(--bg-elevated)}
|
|
2376
|
+
.panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2377
|
+
.meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2378
|
+
.meta-label{font-size:11px;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
|
|
2379
|
+
.meta-value{font-size:13px;word-break:break-all}
|
|
2380
|
+
.quality-bar{height:6px;border-radius:3px;background:var(--bg-elevated);margin-top:4px}
|
|
2381
|
+
.quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2382
|
+
|
|
2383
|
+
/* Map toolbars */
|
|
2384
|
+
#map-tb-left{position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10}
|
|
2385
|
+
#map-tb-right{position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;align-items:flex-end;gap:8px;z-index:10}
|
|
2386
|
+
.tb-tool{
|
|
2387
|
+
width:40px;height:40px;border-radius:10px;border:1px solid var(--border);
|
|
2388
|
+
background:var(--bg-surface);box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2837
2389
|
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2838
|
-
transition:all .15s;color:
|
|
2390
|
+
transition:all .15s;color:var(--text);font-family:-apple-system,sans-serif;
|
|
2839
2391
|
}
|
|
2840
|
-
|
|
2841
|
-
.tb-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
|
|
2845
|
-
align-items:flex-end;gap:8px;z-index:10;
|
|
2846
|
-
}
|
|
2847
|
-
#zoom-controls{display:flex;align-items:center;gap:6px}
|
|
2392
|
+
#btn-all-labels{font-size:14px;font-weight:700;letter-spacing:-.02em}
|
|
2393
|
+
.tb-tool:hover{border-color:var(--text-muted)}
|
|
2394
|
+
.tb-tool.active{background:var(--accent-dim);border-color:var(--accent)}
|
|
2395
|
+
.map-zoom{display:flex;align-items:center;gap:6px}
|
|
2848
2396
|
.zoom-btn{
|
|
2849
|
-
width:34px;height:34px;border-radius:8px;border:1px solid
|
|
2850
|
-
background
|
|
2851
|
-
|
|
2852
|
-
}
|
|
2853
|
-
.zoom-btn:hover{background
|
|
2854
|
-
#zoom-pct{font-size:12px;font-weight:500;color
|
|
2855
|
-
#
|
|
2856
|
-
.detail-btn{
|
|
2857
|
-
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2858
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2859
|
-
font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
|
|
2860
|
-
}
|
|
2861
|
-
.detail-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2862
|
-
.detail-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6;color:#2563eb}
|
|
2863
|
-
#connect-btn{
|
|
2864
|
-
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2865
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2866
|
-
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2867
|
-
}
|
|
2868
|
-
#connect-btn.active{background:#fef3c7;border-color:#f59e0b}
|
|
2869
|
-
/* Tooltip */
|
|
2870
|
-
#tooltip{
|
|
2871
|
-
position:fixed;background:#1e293b;color:#fff;border-radius:8px;
|
|
2872
|
-
padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
|
|
2873
|
-
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
|
2874
|
-
}
|
|
2875
|
-
#tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2876
|
-
#tooltip .tt-domain{color:#94a3b8;font-size:11px}
|
|
2877
|
-
#tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2878
|
-
/* Empty state */
|
|
2879
|
-
#empty-state{
|
|
2880
|
-
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2881
|
-
align-items:center;justify-content:center;gap:12px;color:#94a3b8;
|
|
2882
|
-
}
|
|
2883
|
-
#empty-state p{font-size:14px}
|
|
2884
|
-
/* Theme toggle */
|
|
2885
|
-
#theme-btn{
|
|
2886
|
-
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2887
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2888
|
-
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2889
|
-
}
|
|
2890
|
-
/* Dark mode overrides (toggled via JS) */
|
|
2891
|
-
body.dark{background:#0f172a;color:#e2e8f0}
|
|
2892
|
-
body.dark #topbar{background:#1e293b;border-color:#334155}
|
|
2893
|
-
body.dark #search-box{background:#334155}
|
|
2894
|
-
body.dark #detail-panel{background:#1e293b;border-color:#334155}
|
|
2895
|
-
body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
|
|
2896
|
-
background:#1e293b;border-color:#334155;color:#e2e8f0;
|
|
2897
|
-
}
|
|
2898
|
-
/* Light mode overrides */
|
|
2899
|
-
body.light{background:#f8fafc;color:#1e293b}
|
|
2900
|
-
body.light #topbar{background:#fff;border-color:#e2e8f0}
|
|
2901
|
-
/* Connection hint */
|
|
2902
|
-
#connect-hint{
|
|
2397
|
+
width:34px;height:34px;border-radius:8px;border:1px solid var(--border);
|
|
2398
|
+
background:var(--bg-surface);cursor:pointer;font-size:18px;color:var(--text);
|
|
2399
|
+
display:flex;align-items:center;justify-content:center;
|
|
2400
|
+
}
|
|
2401
|
+
.zoom-btn:hover{background:var(--bg-elevated)}
|
|
2402
|
+
#map-zoom-pct{font-size:12px;font-weight:500;color:var(--text-dim);min-width:38px;text-align:center}
|
|
2403
|
+
#map-connect-hint{
|
|
2903
2404
|
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2904
2405
|
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2905
2406
|
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2906
2407
|
display:none;z-index:20;pointer-events:none;
|
|
2907
2408
|
}
|
|
2908
|
-
|
|
2909
|
-
|
|
2409
|
+
#map-tooltip{
|
|
2410
|
+
position:fixed;background:var(--bg-surface);color:var(--text);border-radius:8px;
|
|
2411
|
+
padding:8px 12px;font-size:12px;pointer-events:none;z-index:200;
|
|
2412
|
+
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.25);border:1px solid var(--border);
|
|
2413
|
+
}
|
|
2414
|
+
#map-tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2415
|
+
#map-tooltip .tt-domain{color:var(--text-muted);font-size:11px}
|
|
2416
|
+
#map-tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2417
|
+
#map-empty{
|
|
2418
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2419
|
+
align-items:center;justify-content:center;gap:12px;color:var(--text-muted);
|
|
2420
|
+
}
|
|
2421
|
+
#map-empty p{font-size:14px}
|
|
2422
|
+
|
|
2423
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2424
|
+
TOPOLOGY VIEW
|
|
2425
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
2426
|
+
#topo-panel{
|
|
2427
|
+
width:220px;min-width:220px;height:100%;overflow:hidden;
|
|
2428
|
+
background:var(--bg-surface);border-right:1px solid var(--border);
|
|
2429
|
+
display:flex;flex-direction:column;
|
|
2430
|
+
}
|
|
2431
|
+
#topo-panel-header{
|
|
2432
|
+
padding:10px 12px 8px;border-bottom:1px solid var(--border);
|
|
2433
|
+
font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.6px;
|
|
2434
|
+
}
|
|
2435
|
+
#topo-search{
|
|
2436
|
+
width:calc(100% - 16px);margin:8px;padding:5px 8px;
|
|
2437
|
+
background:var(--bg-elevated);border:1px solid var(--border);border-radius:5px;
|
|
2438
|
+
color:var(--text);font-size:11px;font-family:inherit;outline:none;
|
|
2439
|
+
}
|
|
2440
|
+
#topo-search:focus{border-color:var(--accent)}
|
|
2441
|
+
#topo-list{flex:1;overflow-y:auto;padding-bottom:8px}
|
|
2442
|
+
.topo-item{
|
|
2443
|
+
padding:5px 12px;cursor:pointer;font-size:11px;
|
|
2444
|
+
display:flex;align-items:center;gap:6px;border-left:2px solid transparent;
|
|
2445
|
+
}
|
|
2446
|
+
.topo-item:hover{background:var(--bg-elevated)}
|
|
2447
|
+
.topo-item.active{background:var(--accent-dim);border-left-color:var(--accent)}
|
|
2448
|
+
.topo-dot{width:7px;height:7px;border-radius:2px;flex-shrink:0}
|
|
2449
|
+
.topo-name{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
2450
|
+
.topo-type{color:var(--text-dim);font-size:9px;flex-shrink:0}
|
|
2451
|
+
|
|
2452
|
+
#topo-graph{flex:1;height:100%;position:relative}
|
|
2453
|
+
#topo-graph svg{width:100%;height:100%}
|
|
2454
|
+
.hull{opacity:.12;stroke-width:1.5;stroke-opacity:.25}
|
|
2455
|
+
.hull-label{font-size:13px;font-weight:700;letter-spacing:1px;text-transform:uppercase;fill-opacity:.5;pointer-events:none}
|
|
2456
|
+
.link{stroke-opacity:.4}
|
|
2457
|
+
.link-label{font-size:8px;fill:var(--text-dim);pointer-events:none;opacity:0}
|
|
2458
|
+
.node-hex{stroke-width:1.8;cursor:pointer;transition:opacity .15s}
|
|
2459
|
+
.node-hex:hover{filter:brightness(1.3);stroke-width:3}
|
|
2460
|
+
.node-hex.selected{stroke-width:3.5;filter:brightness(1.5)}
|
|
2461
|
+
.node-label{font-size:10px;fill:var(--text);pointer-events:none;opacity:0}
|
|
2462
|
+
|
|
2463
|
+
#topo-sidebar{
|
|
2464
|
+
width:300px;min-width:300px;height:100%;overflow-y:auto;
|
|
2465
|
+
background:var(--bg-surface);border-left:1px solid var(--border);
|
|
2466
|
+
padding:16px;font-size:12px;line-height:1.6;
|
|
2467
|
+
}
|
|
2468
|
+
#topo-sidebar h2{margin:0 0 8px;font-size:14px;color:var(--accent)}
|
|
2469
|
+
#topo-sidebar .meta-table{width:100%;border-collapse:collapse}
|
|
2470
|
+
#topo-sidebar .meta-table td{padding:3px 6px;border-bottom:1px solid var(--border-dim);vertical-align:top}
|
|
2471
|
+
#topo-sidebar .meta-table td:first-child{color:var(--text-dim);white-space:nowrap;width:90px}
|
|
2472
|
+
#topo-sidebar .tag{display:inline-block;background:var(--bg-elevated);border-radius:3px;padding:1px 5px;margin:1px;font-size:10px}
|
|
2473
|
+
#topo-sidebar .conf-bar{height:5px;border-radius:3px;background:var(--bg-elevated);margin-top:3px}
|
|
2474
|
+
#topo-sidebar .conf-fill{height:100%;border-radius:3px}
|
|
2475
|
+
#topo-sidebar .edges-list{margin-top:12px}
|
|
2476
|
+
#topo-sidebar .edge-item{padding:4px 0;border-bottom:1px solid var(--border-dim);color:var(--text-dim);font-size:11px}
|
|
2477
|
+
#topo-sidebar .edge-item span{color:var(--text)}
|
|
2478
|
+
.hint{color:var(--text-dim);font-size:11px;margin-top:8px}
|
|
2479
|
+
|
|
2480
|
+
#topo-hud{
|
|
2481
|
+
position:absolute;top:10px;left:10px;background:rgba(15,23,42,.88);
|
|
2482
|
+
padding:10px 14px;border-radius:8px;font-size:12px;border:1px solid var(--border);pointer-events:none;
|
|
2483
|
+
}
|
|
2484
|
+
#topo-hud strong{color:var(--accent)}
|
|
2485
|
+
#topo-hud .stats{color:var(--text-dim)}
|
|
2486
|
+
#topo-hud .zoom-level{color:var(--text-dim);font-size:10px;margin-top:2px}
|
|
2487
|
+
|
|
2488
|
+
#topo-toolbar{position:absolute;top:10px;right:10px;display:flex;flex-wrap:wrap;gap:4px;pointer-events:auto;align-items:center}
|
|
2489
|
+
.filter-btn{
|
|
2490
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
2491
|
+
color:var(--text);padding:4px 10px;font-size:11px;cursor:pointer;
|
|
2492
|
+
font-family:inherit;display:flex;align-items:center;gap:5px;
|
|
2493
|
+
}
|
|
2494
|
+
.filter-btn:hover{border-color:var(--text-dim)}
|
|
2495
|
+
.filter-btn.off{opacity:.35}
|
|
2496
|
+
.filter-dot{width:8px;height:8px;border-radius:2px;display:inline-block}
|
|
2497
|
+
.export-btn{
|
|
2498
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
2499
|
+
color:var(--accent);padding:4px 12px;font-size:11px;cursor:pointer;font-family:inherit;
|
|
2500
|
+
}
|
|
2501
|
+
.export-btn:hover{border-color:var(--accent);background:var(--accent-dim)}
|
|
2910
2502
|
</style>
|
|
2911
2503
|
</head>
|
|
2912
|
-
<body
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2504
|
+
<body>
|
|
2505
|
+
|
|
2506
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2507
|
+
TOPBAR
|
|
2508
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
2509
|
+
<header id="topbar">
|
|
2510
|
+
<div class="tb-left">
|
|
2511
|
+
<svg class="brand-logo" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
2512
|
+
<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"/>
|
|
2513
|
+
<circle cx="10" cy="16" r="2.8" fill="#60A5FA"/><circle cx="22" cy="10.5" r="2.2" fill="#38BDF8"/>
|
|
2514
|
+
<circle cx="22" cy="21.5" r="2.2" fill="#38BDF8"/>
|
|
2515
|
+
<line x1="12.5" y1="14.8" x2="19.8" y2="11.2" stroke="#93C5FD" stroke-width="1.2"/>
|
|
2516
|
+
<line x1="12.5" y1="17.2" x2="19.8" y2="20.8" stroke="#93C5FD" stroke-width="1.2"/>
|
|
2517
|
+
<line x1="22" y1="12.7" x2="22" y2="19.3" stroke="#93C5FD" stroke-width="1" stroke-dasharray="2 1.5"/>
|
|
2518
|
+
</svg>
|
|
2519
|
+
<span class="brand-name">datasynx</span>
|
|
2520
|
+
<span class="brand-sep"></span>
|
|
2521
|
+
<span class="brand-product">Cartography</span>
|
|
2919
2522
|
</div>
|
|
2920
|
-
<
|
|
2921
|
-
</
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
</
|
|
2926
|
-
|
|
2927
|
-
<
|
|
2928
|
-
|
|
2523
|
+
<div class="tb-center">
|
|
2524
|
+
<button class="tab-btn active" id="tab-map-btn" data-tab="map">Map</button>
|
|
2525
|
+
<button class="tab-btn" id="tab-topo-btn" data-tab="topo">Topology</button>
|
|
2526
|
+
</div>
|
|
2527
|
+
<div class="tb-right">
|
|
2528
|
+
<span class="tb-stats">${nodeCount} nodes · ${edgeCount} edges</span>
|
|
2529
|
+
<div class="tb-search">
|
|
2530
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
2531
|
+
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
|
|
2532
|
+
</svg>
|
|
2533
|
+
<input id="global-search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false"/>
|
|
2534
|
+
</div>
|
|
2535
|
+
<a href="https://www.linkedin.com/company/datasynx-ai/" target="_blank" rel="noopener noreferrer"
|
|
2536
|
+
class="icon-btn" title="Datasynx on LinkedIn" aria-label="LinkedIn">
|
|
2537
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
|
2538
|
+
<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"/>
|
|
2539
|
+
</svg>
|
|
2540
|
+
</a>
|
|
2541
|
+
<button id="theme-btn" class="icon-btn" title="Toggle theme" aria-label="Toggle theme">
|
|
2542
|
+
${theme === "dark" ? "☼" : "☾"}
|
|
2543
|
+
</button>
|
|
2544
|
+
</div>
|
|
2545
|
+
</header>
|
|
2546
|
+
|
|
2547
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2548
|
+
MAP VIEW
|
|
2549
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
2550
|
+
<div id="view-map" class="view active">
|
|
2551
|
+
<div id="map-wrap" tabindex="0" aria-label="Data cartography hex map">
|
|
2929
2552
|
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
2930
|
-
${isEmpty ? '<div id="empty
|
|
2553
|
+
${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>' : ""}
|
|
2931
2554
|
</div>
|
|
2932
|
-
<div id="detail
|
|
2555
|
+
<div id="map-detail">
|
|
2933
2556
|
<div class="panel-header">
|
|
2934
|
-
<h3 id="
|
|
2935
|
-
<button class="close-btn" id="
|
|
2557
|
+
<h3 id="md-name">—</h3>
|
|
2558
|
+
<button class="close-btn" id="md-close" aria-label="Close">✕</button>
|
|
2936
2559
|
</div>
|
|
2937
|
-
<div class="panel-body" id="
|
|
2560
|
+
<div class="panel-body" id="md-body"></div>
|
|
2938
2561
|
</div>
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
<
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
</div>
|
|
2945
|
-
<!-- Bottom-right toolbar -->
|
|
2946
|
-
<div id="toolbar-right">
|
|
2947
|
-
<div id="zoom-controls">
|
|
2948
|
-
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out">−</button>
|
|
2949
|
-
<span id="zoom-pct">100%</span>
|
|
2950
|
-
<button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
|
|
2562
|
+
<div id="map-tb-left">
|
|
2563
|
+
<button class="tb-tool active" id="btn-labels" title="Toggle labels">🏷</button>
|
|
2564
|
+
<button class="tb-tool" id="btn-all-labels" title="Show all hex labels">Aa</button>
|
|
2565
|
+
<button class="tb-tool" id="btn-quality" title="Quality layer">👁</button>
|
|
2566
|
+
<button class="tb-tool" id="btn-connect" title="Connection tool">🔗</button>
|
|
2951
2567
|
</div>
|
|
2952
|
-
<div id="
|
|
2953
|
-
<
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2568
|
+
<div id="map-tb-right">
|
|
2569
|
+
<div class="map-zoom">
|
|
2570
|
+
<button class="zoom-btn" id="mz-out">−</button>
|
|
2571
|
+
<span id="map-zoom-pct">100%</span>
|
|
2572
|
+
<button class="zoom-btn" id="mz-in">+</button>
|
|
2573
|
+
</div>
|
|
2957
2574
|
</div>
|
|
2958
|
-
<
|
|
2575
|
+
<div id="map-connect-hint">Click two assets to create a connection</div>
|
|
2576
|
+
<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>
|
|
2959
2577
|
</div>
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
<div
|
|
2966
|
-
|
|
2578
|
+
|
|
2579
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2580
|
+
TOPOLOGY VIEW
|
|
2581
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
2582
|
+
<div id="view-topo" class="view">
|
|
2583
|
+
<div id="topo-panel">
|
|
2584
|
+
<div id="topo-panel-header">Nodes (${nodeCount})</div>
|
|
2585
|
+
<input id="topo-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false"/>
|
|
2586
|
+
<div id="topo-list"></div>
|
|
2587
|
+
</div>
|
|
2588
|
+
<div id="topo-graph">
|
|
2589
|
+
<div id="topo-hud">
|
|
2590
|
+
<strong>Topology</strong>
|
|
2591
|
+
<span class="stats">${nodeCount} nodes · ${edgeCount} edges</span><br/>
|
|
2592
|
+
<span class="zoom-level">Scroll = zoom · Drag = pan · Click = details</span>
|
|
2593
|
+
</div>
|
|
2594
|
+
<div id="topo-toolbar"></div>
|
|
2595
|
+
<svg></svg>
|
|
2596
|
+
</div>
|
|
2597
|
+
<div id="topo-sidebar">
|
|
2598
|
+
<h2>Infrastructure Map</h2>
|
|
2599
|
+
<p class="hint">Click a node to view details.</p>
|
|
2600
|
+
</div>
|
|
2967
2601
|
</div>
|
|
2968
2602
|
|
|
2969
2603
|
<script>
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
function
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
3000
|
-
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
3001
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3002
|
-
draw();
|
|
3003
|
-
}
|
|
3004
|
-
window.addEventListener('resize', resize);
|
|
3005
|
-
|
|
3006
|
-
// \u2500\u2500 Viewport \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3007
|
-
let vx = 0, vy = 0, scale = 1;
|
|
3008
|
-
let detailLevel = 2, showLabels = true, showQuality = false;
|
|
3009
|
-
let isDark = document.body.classList.contains('dark');
|
|
3010
|
-
let connectMode = false, connectFirst = null;
|
|
3011
|
-
let hoveredAssetId = null, selectedAssetId = null;
|
|
3012
|
-
let searchQuery = '';
|
|
3013
|
-
let localConnections = [...MAP.connections];
|
|
3014
|
-
|
|
3015
|
-
// Flat-top hex math
|
|
3016
|
-
function htp_x(q, r) { return HEX_SIZE * (3/2 * q); }
|
|
3017
|
-
function htp_y(q, r) { return HEX_SIZE * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r); }
|
|
3018
|
-
function w2s(wx, wy) { return { x: wx*scale+vx, y: wy*scale+vy }; }
|
|
3019
|
-
function s2w(sx, sy) { return { x: (sx-vx)/scale, y: (sy-vy)/scale }; }
|
|
3020
|
-
|
|
3021
|
-
function fitToView() {
|
|
3022
|
-
if (IS_EMPTY || MAP.assets.length === 0) { vx = 0; vy = 0; scale = 1; return; }
|
|
3023
|
-
let mnx=Infinity,mny=Infinity,mxx=-Infinity,mxy=-Infinity;
|
|
3024
|
-
for (const a of MAP.assets) {
|
|
3025
|
-
const px=htp_x(a.q,a.r), py=htp_y(a.q,a.r);
|
|
3026
|
-
if(px<mnx)mnx=px;if(py<mny)mny=py;if(px>mxx)mxx=px;if(py>mxy)mxy=py;
|
|
3027
|
-
}
|
|
3028
|
-
const pw=mxx-mnx+HEX_SIZE*4, ph=mxy-mny+HEX_SIZE*4;
|
|
3029
|
-
scale = Math.min(W/pw, H/ph, 2) * 0.85;
|
|
3030
|
-
vx = W/2 - ((mnx+mxx)/2)*scale;
|
|
3031
|
-
vy = H/2 - ((mny+mxy)/2)*scale;
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
// \u2500\u2500 Drawing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3035
|
-
function hexPath(cx, cy, r) {
|
|
3036
|
-
ctx.beginPath();
|
|
3037
|
-
for (let i=0;i<6;i++) {
|
|
3038
|
-
const angle = Math.PI/180*(60*i);
|
|
3039
|
-
const x=cx+r*Math.cos(angle), y=cy+r*Math.sin(angle);
|
|
3040
|
-
i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
|
|
3041
|
-
}
|
|
3042
|
-
ctx.closePath();
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
function shadeV(hex, amt) {
|
|
3046
|
-
if(!hex||hex.length<7)return hex;
|
|
3047
|
-
const n=parseInt(hex.replace('#',''),16);
|
|
3048
|
-
const r=Math.min(255,(n>>16)+amt), g=Math.min(255,((n>>8)&0xff)+amt), b=Math.min(255,(n&0xff)+amt);
|
|
3049
|
-
return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0');
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
function draw() {
|
|
3053
|
-
ctx.clearRect(0,0,W,H);
|
|
3054
|
-
ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
|
|
3055
|
-
ctx.fillRect(0,0,W,H);
|
|
3056
|
-
if (IS_EMPTY) return;
|
|
3057
|
-
|
|
3058
|
-
const size = HEX_SIZE * scale;
|
|
3059
|
-
const matchedIds = getSearchMatches();
|
|
3060
|
-
const hasSearch = searchQuery.length > 0;
|
|
3061
|
-
|
|
3062
|
-
// Draw connections
|
|
3063
|
-
ctx.save();
|
|
3064
|
-
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
3065
|
-
ctx.lineWidth = 1.5;
|
|
3066
|
-
ctx.setLineDash([4,4]);
|
|
3067
|
-
for (const conn of localConnections) {
|
|
3068
|
-
const src = assetIndex.get(conn.sourceAssetId);
|
|
3069
|
-
const tgt = assetIndex.get(conn.targetAssetId);
|
|
3070
|
-
if (!src||!tgt) continue;
|
|
3071
|
-
const sp=w2s(htp_x(src.q,src.r),htp_y(src.q,src.r));
|
|
3072
|
-
const tp=w2s(htp_x(tgt.q,tgt.r),htp_y(tgt.q,tgt.r));
|
|
3073
|
-
ctx.beginPath();ctx.moveTo(sp.x,sp.y);ctx.lineTo(tp.x,tp.y);ctx.stroke();
|
|
3074
|
-
}
|
|
3075
|
-
ctx.setLineDash([]);
|
|
3076
|
-
ctx.restore();
|
|
3077
|
-
|
|
3078
|
-
// Draw hexagons per cluster
|
|
3079
|
-
for (const cluster of MAP.clusters) {
|
|
3080
|
-
const baseColor = cluster.color;
|
|
3081
|
-
const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
|
|
3082
|
-
const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
|
|
3083
|
-
const clusterDim = hasSearch && !isClusterMatch;
|
|
3084
|
-
|
|
3085
|
-
for (let ai=0; ai<clusterAssets.length; ai++) {
|
|
3086
|
-
const asset = clusterAssets[ai];
|
|
3087
|
-
const wx=htp_x(asset.q,asset.r), wy=htp_y(asset.q,asset.r);
|
|
3088
|
-
const s=w2s(wx,wy);
|
|
3089
|
-
const cx=s.x, cy=s.y;
|
|
3090
|
-
|
|
3091
|
-
// Frustum cull
|
|
3092
|
-
if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
|
|
3093
|
-
|
|
3094
|
-
// Shade variation
|
|
3095
|
-
const shade = ai%3===0?18:ai%3===1?8:0;
|
|
3096
|
-
let fillColor = shadeV(baseColor, shade);
|
|
3097
|
-
|
|
3098
|
-
// Quality overlay
|
|
3099
|
-
if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
3100
|
-
const q = asset.qualityScore;
|
|
3101
|
-
if (q < 40) fillColor = '#ef4444';
|
|
3102
|
-
else if (q < 70) fillColor = '#f97316';
|
|
3103
|
-
}
|
|
2604
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2605
|
+
// SHARED STATE
|
|
2606
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2607
|
+
let isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
2608
|
+
let currentTab = 'map';
|
|
2609
|
+
let topoInited = false;
|
|
2610
|
+
|
|
2611
|
+
// \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
|
|
2612
|
+
document.getElementById('theme-btn').addEventListener('click', function() {
|
|
2613
|
+
isDark = !isDark;
|
|
2614
|
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
2615
|
+
this.innerHTML = isDark ? '\\u2606' : '\\u263E';
|
|
2616
|
+
if (typeof drawMap === 'function') drawMap();
|
|
2617
|
+
});
|
|
2618
|
+
|
|
2619
|
+
// \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
|
|
2620
|
+
document.querySelectorAll('.tab-btn').forEach(function(btn) {
|
|
2621
|
+
btn.addEventListener('click', function() {
|
|
2622
|
+
var tab = this.getAttribute('data-tab');
|
|
2623
|
+
if (tab === currentTab) return;
|
|
2624
|
+
currentTab = tab;
|
|
2625
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
2626
|
+
this.classList.add('active');
|
|
2627
|
+
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
2628
|
+
document.getElementById('view-' + tab).classList.add('active');
|
|
2629
|
+
if (tab === 'topo' && !topoInited) { initTopology(); topoInited = true; }
|
|
2630
|
+
if (tab === 'map' && typeof drawMap === 'function') { resizeMap(); }
|
|
2631
|
+
});
|
|
2632
|
+
});
|
|
3104
2633
|
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
2634
|
+
// \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
|
|
2635
|
+
document.getElementById('global-search').addEventListener('input', function(e) {
|
|
2636
|
+
var q = e.target.value.trim();
|
|
2637
|
+
if (typeof setMapSearch === 'function') setMapSearch(q);
|
|
2638
|
+
if (typeof setTopoSearch === 'function') setTopoSearch(q);
|
|
2639
|
+
});
|
|
3109
2640
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
2641
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2642
|
+
// MAP VIEW
|
|
2643
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2644
|
+
var MAP = ${mapJson};
|
|
2645
|
+
var MAP_HEX = ${HEX_SIZE2};
|
|
2646
|
+
var MAP_EMPTY = ${isEmpty};
|
|
2647
|
+
|
|
2648
|
+
var mapAssetIndex = new Map();
|
|
2649
|
+
var mapClusterByAsset = new Map();
|
|
2650
|
+
for (var ci = 0; ci < MAP.clusters.length; ci++) {
|
|
2651
|
+
var c = MAP.clusters[ci];
|
|
2652
|
+
for (var ai = 0; ai < c.assetIds.length; ai++) mapClusterByAsset.set(c.assetIds[ai], c);
|
|
2653
|
+
}
|
|
2654
|
+
for (var ni = 0; ni < MAP.assets.length; ni++) mapAssetIndex.set(MAP.assets[ni].id, MAP.assets[ni]);
|
|
2655
|
+
|
|
2656
|
+
var mapCanvas = document.getElementById('hexmap');
|
|
2657
|
+
var mapCtx = mapCanvas.getContext('2d');
|
|
2658
|
+
var mapWrap = document.getElementById('map-wrap');
|
|
2659
|
+
var mW = 0, mH = 0;
|
|
2660
|
+
var mvx = 0, mvy = 0, mScale = 1;
|
|
2661
|
+
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false, mShowAllLabels = false;
|
|
2662
|
+
var mConnectMode = false, mConnectFirst = null;
|
|
2663
|
+
var mHoveredId = null, mSelectedId = null;
|
|
2664
|
+
var mSearchQuery = '';
|
|
2665
|
+
var mLocalConns = MAP.connections.slice();
|
|
2666
|
+
|
|
2667
|
+
function setMapSearch(q) { mSearchQuery = q; drawMap(); }
|
|
2668
|
+
|
|
2669
|
+
function resizeMap() {
|
|
2670
|
+
var dpr = window.devicePixelRatio || 1;
|
|
2671
|
+
mW = mapWrap.clientWidth; mH = mapWrap.clientHeight;
|
|
2672
|
+
mapCanvas.width = mW * dpr; mapCanvas.height = mH * dpr;
|
|
2673
|
+
mapCanvas.style.width = mW + 'px'; mapCanvas.style.height = mH + 'px';
|
|
2674
|
+
mapCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2675
|
+
drawMap();
|
|
2676
|
+
}
|
|
2677
|
+
window.addEventListener('resize', function() { if (currentTab === 'map') resizeMap(); });
|
|
2678
|
+
|
|
2679
|
+
function mHtp_x(q, r) { return MAP_HEX * (1.5 * q); }
|
|
2680
|
+
function mHtp_y(q, r) { return MAP_HEX * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r); }
|
|
2681
|
+
function mW2s(wx, wy) { return { x: wx * mScale + mvx, y: wy * mScale + mvy }; }
|
|
2682
|
+
function mS2w(sx, sy) { return { x: (sx - mvx) / mScale, y: (sy - mvy) / mScale }; }
|
|
2683
|
+
|
|
2684
|
+
function mapFitToView() {
|
|
2685
|
+
if (MAP_EMPTY || MAP.assets.length === 0) { mvx = 0; mvy = 0; mScale = 1; return; }
|
|
2686
|
+
var mnx = Infinity, mny = Infinity, mxx = -Infinity, mxy = -Infinity;
|
|
2687
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
2688
|
+
var a = MAP.assets[i], px = mHtp_x(a.q, a.r), py = mHtp_y(a.q, a.r);
|
|
2689
|
+
if (px < mnx) mnx = px; if (py < mny) mny = py; if (px > mxx) mxx = px; if (py > mxy) mxy = py;
|
|
2690
|
+
}
|
|
2691
|
+
var pw = mxx - mnx + MAP_HEX * 4, ph = mxy - mny + MAP_HEX * 4;
|
|
2692
|
+
mScale = Math.min(mW / pw, mH / ph, 2) * 0.85;
|
|
2693
|
+
mvx = mW / 2 - ((mnx + mxx) / 2) * mScale;
|
|
2694
|
+
mvy = mH / 2 - ((mny + mxy) / 2) * mScale;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
function mHexPath(cx, cy, r) {
|
|
2698
|
+
mapCtx.beginPath();
|
|
2699
|
+
for (var i = 0; i < 6; i++) {
|
|
2700
|
+
var angle = Math.PI / 180 * (60 * i);
|
|
2701
|
+
var x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle);
|
|
2702
|
+
i === 0 ? mapCtx.moveTo(x, y) : mapCtx.lineTo(x, y);
|
|
2703
|
+
}
|
|
2704
|
+
mapCtx.closePath();
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function mShadeV(hex, amt) {
|
|
2708
|
+
if (!hex || hex.length < 7) return hex;
|
|
2709
|
+
var n = parseInt(hex.replace('#', ''), 16);
|
|
2710
|
+
var r = Math.min(255, (n >> 16) + amt), g = Math.min(255, ((n >> 8) & 0xff) + amt), b = Math.min(255, (n & 0xff) + amt);
|
|
2711
|
+
return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2714
|
+
function mGetSearchMatches() {
|
|
2715
|
+
if (!mSearchQuery) return new Set();
|
|
2716
|
+
var q = mSearchQuery.toLowerCase(), m = new Set();
|
|
2717
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
2718
|
+
var a = MAP.assets[i];
|
|
2719
|
+
if (a.name.toLowerCase().includes(q) || (a.domain && a.domain.toLowerCase().includes(q)) ||
|
|
2720
|
+
(a.subDomain && a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
2721
|
+
}
|
|
2722
|
+
return m;
|
|
2723
|
+
}
|
|
3113
2724
|
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
2725
|
+
function mDrawPill(x, y, text, color, fontSize) {
|
|
2726
|
+
if (!text) return;
|
|
2727
|
+
mapCtx.save();
|
|
2728
|
+
mapCtx.font = '600 ' + fontSize + 'px -apple-system,sans-serif';
|
|
2729
|
+
var tw = mapCtx.measureText(text).width;
|
|
2730
|
+
var ph = fontSize + 8, pw = tw + 20;
|
|
2731
|
+
mapCtx.beginPath();
|
|
2732
|
+
if (mapCtx.roundRect) mapCtx.roundRect(x - pw / 2, y - ph / 2, pw, ph, ph / 2);
|
|
2733
|
+
else mapCtx.rect(x - pw / 2, y - ph / 2, pw, ph);
|
|
2734
|
+
mapCtx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
2735
|
+
mapCtx.shadowColor = 'rgba(0,0,0,0.15)'; mapCtx.shadowBlur = 6;
|
|
2736
|
+
mapCtx.fill(); mapCtx.shadowBlur = 0;
|
|
2737
|
+
mapCtx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
2738
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
2739
|
+
mapCtx.fillText(text, x, y);
|
|
2740
|
+
mapCtx.restore();
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
function drawMap() {
|
|
2744
|
+
mapCtx.clearRect(0, 0, mW, mH);
|
|
2745
|
+
var bg = getComputedStyle(document.documentElement).getPropertyValue('--bg-base').trim();
|
|
2746
|
+
mapCtx.fillStyle = bg || (isDark ? '#0f172a' : '#f8fafc');
|
|
2747
|
+
mapCtx.fillRect(0, 0, mW, mH);
|
|
2748
|
+
if (MAP_EMPTY) return;
|
|
2749
|
+
|
|
2750
|
+
var size = MAP_HEX * mScale;
|
|
2751
|
+
var matchedIds = mGetSearchMatches();
|
|
2752
|
+
var hasSearch = mSearchQuery.length > 0;
|
|
2753
|
+
|
|
2754
|
+
// Connections
|
|
2755
|
+
mapCtx.save();
|
|
2756
|
+
mapCtx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
2757
|
+
mapCtx.lineWidth = 1.5; mapCtx.setLineDash([4, 4]);
|
|
2758
|
+
for (var ci = 0; ci < mLocalConns.length; ci++) {
|
|
2759
|
+
var conn = mLocalConns[ci];
|
|
2760
|
+
var src = mapAssetIndex.get(conn.sourceAssetId), tgt = mapAssetIndex.get(conn.targetAssetId);
|
|
2761
|
+
if (!src || !tgt) continue;
|
|
2762
|
+
var sp = mW2s(mHtp_x(src.q, src.r), mHtp_y(src.q, src.r));
|
|
2763
|
+
var tp = mW2s(mHtp_x(tgt.q, tgt.r), mHtp_y(tgt.q, tgt.r));
|
|
2764
|
+
mapCtx.beginPath(); mapCtx.moveTo(sp.x, sp.y); mapCtx.lineTo(tp.x, tp.y); mapCtx.stroke();
|
|
2765
|
+
}
|
|
2766
|
+
mapCtx.setLineDash([]); mapCtx.restore();
|
|
2767
|
+
|
|
2768
|
+
// Hexagons per cluster
|
|
2769
|
+
for (var cli = 0; cli < MAP.clusters.length; cli++) {
|
|
2770
|
+
var cluster = MAP.clusters[cli];
|
|
2771
|
+
var baseColor = cluster.color;
|
|
2772
|
+
var clusterAssets = cluster.assetIds.map(function(id) { return mapAssetIndex.get(id); }).filter(Boolean);
|
|
2773
|
+
var isClusterMatch = !hasSearch || clusterAssets.some(function(a) { return matchedIds.has(a.id); });
|
|
2774
|
+
var clusterDim = hasSearch && !isClusterMatch;
|
|
2775
|
+
|
|
2776
|
+
for (var ai = 0; ai < clusterAssets.length; ai++) {
|
|
2777
|
+
var asset = clusterAssets[ai];
|
|
2778
|
+
var wx = mHtp_x(asset.q, asset.r), wy = mHtp_y(asset.q, asset.r);
|
|
2779
|
+
var s = mW2s(wx, wy), cx = s.x, cy = s.y;
|
|
2780
|
+
if (cx + size < 0 || cx - size > mW || cy + size < 0 || cy - size > mH) continue;
|
|
2781
|
+
|
|
2782
|
+
var shade = ai % 3 === 0 ? 18 : ai % 3 === 1 ? 8 : 0;
|
|
2783
|
+
var fillColor = mShadeV(baseColor, shade);
|
|
2784
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
2785
|
+
if (asset.qualityScore < 40) fillColor = '#ef4444';
|
|
2786
|
+
else if (asset.qualityScore < 70) fillColor = '#f97316';
|
|
3117
2787
|
}
|
|
3118
2788
|
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
}
|
|
3135
|
-
ctx.restore();
|
|
3136
|
-
|
|
3137
|
-
// Quality dot
|
|
3138
|
-
if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
|
|
3139
|
-
const q = asset.qualityScore;
|
|
3140
|
-
if (q < 70) {
|
|
3141
|
-
ctx.beginPath();
|
|
3142
|
-
ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,size*0.14), 0, Math.PI*2);
|
|
3143
|
-
ctx.fillStyle = q<40?'#ef4444':'#f97316';
|
|
3144
|
-
ctx.fill();
|
|
3145
|
-
}
|
|
2789
|
+
var alpha = clusterDim ? 0.18 : 1;
|
|
2790
|
+
var isHov = asset.id === mHoveredId, isSel = asset.id === mSelectedId, isCF = asset.id === mConnectFirst;
|
|
2791
|
+
|
|
2792
|
+
mapCtx.save(); mapCtx.globalAlpha = alpha;
|
|
2793
|
+
mHexPath(cx, cy, size * 0.92);
|
|
2794
|
+
if (isDark && (isHov || isSel || isCF)) { mapCtx.shadowColor = fillColor; mapCtx.shadowBlur = isSel ? 16 : 8; }
|
|
2795
|
+
mapCtx.fillStyle = fillColor; mapCtx.fill();
|
|
2796
|
+
if (isSel || isCF) { mapCtx.strokeStyle = isCF ? '#f59e0b' : '#fff'; mapCtx.lineWidth = 2.5; mapCtx.stroke(); }
|
|
2797
|
+
else if (isHov) { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)'; mapCtx.lineWidth = 1.5; mapCtx.stroke(); }
|
|
2798
|
+
else { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)'; mapCtx.lineWidth = 1; mapCtx.stroke(); }
|
|
2799
|
+
mapCtx.restore();
|
|
2800
|
+
|
|
2801
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined && size > 8 && asset.qualityScore < 70) {
|
|
2802
|
+
mapCtx.beginPath(); mapCtx.arc(cx + size * 0.4, cy - size * 0.4, Math.max(3, size * 0.14), 0, Math.PI * 2);
|
|
2803
|
+
mapCtx.fillStyle = asset.qualityScore < 40 ? '#ef4444' : '#f97316'; mapCtx.fill();
|
|
3146
2804
|
}
|
|
3147
2805
|
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
(
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
ctx.textAlign='center';ctx.textBaseline='middle';
|
|
3157
|
-
ctx.fillText(label, cx, cy);
|
|
3158
|
-
ctx.restore();
|
|
2806
|
+
var showAssetLabel = mShowLabels && !clusterDim && (mShowAllLabels || (mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
2807
|
+
if (showAssetLabel && size > 6) {
|
|
2808
|
+
var label = asset.name.length > 12 ? asset.name.substring(0, 11) + '...' : asset.name;
|
|
2809
|
+
mapCtx.save();
|
|
2810
|
+
mapCtx.font = Math.max(8, Math.min(11, size * 0.38)) + 'px -apple-system,sans-serif';
|
|
2811
|
+
mapCtx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
2812
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
2813
|
+
mapCtx.fillText(label, cx, cy); mapCtx.restore();
|
|
3159
2814
|
}
|
|
3160
2815
|
}
|
|
3161
2816
|
}
|
|
3162
2817
|
|
|
3163
|
-
// Cluster labels
|
|
3164
|
-
if (
|
|
3165
|
-
for (
|
|
3166
|
-
|
|
3167
|
-
if (
|
|
3168
|
-
|
|
3169
|
-
|
|
2818
|
+
// Cluster labels
|
|
2819
|
+
if (mShowLabels && mDetailLevel >= 1) {
|
|
2820
|
+
for (var cli2 = 0; cli2 < MAP.clusters.length; cli2++) {
|
|
2821
|
+
var cl = MAP.clusters[cli2];
|
|
2822
|
+
if (cl.assetIds.length === 0) continue;
|
|
2823
|
+
if (hasSearch && !cl.assetIds.some(function(id) { return matchedIds.has(id); })) continue;
|
|
2824
|
+
var sc = mW2s(cl.centroid.x, cl.centroid.y);
|
|
2825
|
+
mDrawPill(sc.x, sc.y - size * 1.2, cl.label, cl.color, 14);
|
|
3170
2826
|
}
|
|
3171
2827
|
}
|
|
3172
2828
|
|
|
3173
|
-
// Sub-domain labels
|
|
3174
|
-
if (
|
|
3175
|
-
|
|
3176
|
-
for (
|
|
3177
|
-
|
|
3178
|
-
|
|
2829
|
+
// Sub-domain labels
|
|
2830
|
+
if (mShowLabels && mDetailLevel >= 2) {
|
|
2831
|
+
var subGroups = new Map();
|
|
2832
|
+
for (var si = 0; si < MAP.assets.length; si++) {
|
|
2833
|
+
var sa = MAP.assets[si];
|
|
2834
|
+
if (!sa.subDomain) continue;
|
|
2835
|
+
var key = sa.domain + '|' + sa.subDomain;
|
|
3179
2836
|
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
3180
|
-
subGroups.get(key).push(
|
|
3181
|
-
}
|
|
3182
|
-
for (const [, group] of subGroups) {
|
|
3183
|
-
let sx=0,sy=0;
|
|
3184
|
-
for (const a of group) { sx+=htp_x(a.q,a.r); sy+=htp_y(a.q,a.r); }
|
|
3185
|
-
const cx=sx/group.length, cy=sy/group.length;
|
|
3186
|
-
const s = w2s(cx, cy);
|
|
3187
|
-
drawPill(s.x, s.y+size*1.5, group[0].subDomain, '#64748b', 11);
|
|
2837
|
+
subGroups.get(key).push(sa);
|
|
3188
2838
|
}
|
|
2839
|
+
subGroups.forEach(function(group) {
|
|
2840
|
+
var sx = 0, sy = 0;
|
|
2841
|
+
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); }
|
|
2842
|
+
var cxs = sx / group.length, cys = sy / group.length;
|
|
2843
|
+
var spt = mW2s(cxs, cys);
|
|
2844
|
+
mDrawPill(spt.x, spt.y + size * 1.5, group[0].subDomain, '#64748b', 11);
|
|
2845
|
+
});
|
|
3189
2846
|
}
|
|
3190
2847
|
}
|
|
3191
2848
|
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
else { ctx.rect(x-pw/2, y-ph/2, pw, ph); }
|
|
3201
|
-
ctx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
3202
|
-
ctx.shadowColor='rgba(0,0,0,0.15)'; ctx.shadowBlur=6;
|
|
3203
|
-
ctx.fill(); ctx.shadowBlur=0;
|
|
3204
|
-
ctx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
3205
|
-
ctx.textAlign='center'; ctx.textBaseline='middle';
|
|
3206
|
-
ctx.fillText(text, x, y);
|
|
3207
|
-
ctx.restore();
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
|
-
// \u2500\u2500 Hit testing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3211
|
-
function getAssetAt(sx, sy) {
|
|
3212
|
-
const w=s2w(sx,sy);
|
|
3213
|
-
for (const a of MAP.assets) {
|
|
3214
|
-
const wx=htp_x(a.q,a.r), wy=htp_y(a.q,a.r);
|
|
3215
|
-
const dx=Math.abs(w.x-wx), dy=Math.abs(w.y-wy);
|
|
3216
|
-
if (dx>HEX_SIZE||dy>HEX_SIZE) continue;
|
|
3217
|
-
if (dx*dx+dy*dy < HEX_SIZE*HEX_SIZE) return a;
|
|
2849
|
+
// \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
|
|
2850
|
+
function mGetAssetAt(sx, sy) {
|
|
2851
|
+
var w = mS2w(sx, sy);
|
|
2852
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
2853
|
+
var a = MAP.assets[i], wx = mHtp_x(a.q, a.r), wy = mHtp_y(a.q, a.r);
|
|
2854
|
+
var dx = Math.abs(w.x - wx), dy = Math.abs(w.y - wy);
|
|
2855
|
+
if (dx > MAP_HEX || dy > MAP_HEX) continue;
|
|
2856
|
+
if (dx * dx + dy * dy < MAP_HEX * MAP_HEX) return a;
|
|
3218
2857
|
}
|
|
3219
2858
|
return null;
|
|
3220
2859
|
}
|
|
3221
2860
|
|
|
3222
|
-
// \u2500\u2500
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
if(a.name.toLowerCase().includes(q)||(a.domain&&a.domain.toLowerCase().includes(q))||
|
|
3229
|
-
(a.subDomain&&a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
3230
|
-
}
|
|
3231
|
-
return m;
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
// \u2500\u2500 Pan & Zoom \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3235
|
-
let dragging=false, lastMX=0, lastMY=0;
|
|
3236
|
-
|
|
3237
|
-
wrap.addEventListener('mousedown', e=>{
|
|
3238
|
-
if(e.button!==0)return;
|
|
3239
|
-
dragging=true; lastMX=e.clientX; lastMY=e.clientY;
|
|
3240
|
-
wrap.classList.add('dragging');
|
|
2861
|
+
// \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
|
|
2862
|
+
var mDragging = false, mLastMX = 0, mLastMY = 0;
|
|
2863
|
+
mapWrap.addEventListener('mousedown', function(e) {
|
|
2864
|
+
if (e.button !== 0) return;
|
|
2865
|
+
mDragging = true; mLastMX = e.clientX; mLastMY = e.clientY;
|
|
2866
|
+
mapWrap.classList.add('dragging');
|
|
3241
2867
|
});
|
|
3242
|
-
window.addEventListener('mouseup', ()
|
|
3243
|
-
window.addEventListener('mousemove', e
|
|
3244
|
-
if(
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
document.getElementById('
|
|
3257
|
-
document.getElementById('
|
|
3258
|
-
|
|
3259
|
-
|
|
2868
|
+
window.addEventListener('mouseup', function() { mDragging = false; mapWrap.classList.remove('dragging'); });
|
|
2869
|
+
window.addEventListener('mousemove', function(e) {
|
|
2870
|
+
if (currentTab !== 'map') return;
|
|
2871
|
+
if (mDragging) {
|
|
2872
|
+
mvx += e.clientX - mLastMX; mvy += e.clientY - mLastMY;
|
|
2873
|
+
mLastMX = e.clientX; mLastMY = e.clientY; drawMap(); return;
|
|
2874
|
+
}
|
|
2875
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
2876
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
2877
|
+
var asset = mGetAssetAt(sx, sy);
|
|
2878
|
+
var newId = asset ? asset.id : null;
|
|
2879
|
+
if (newId !== mHoveredId) { mHoveredId = newId; drawMap(); }
|
|
2880
|
+
var tt = document.getElementById('map-tooltip');
|
|
2881
|
+
if (asset) {
|
|
2882
|
+
document.getElementById('mtt-name').textContent = asset.name;
|
|
2883
|
+
document.getElementById('mtt-domain').textContent = asset.domain + (asset.subDomain ? ' > ' + asset.subDomain : '');
|
|
2884
|
+
document.getElementById('mtt-quality').textContent = asset.qualityScore !== null ? 'Quality: ' + asset.qualityScore + '/100' : '';
|
|
2885
|
+
tt.style.display = 'block'; tt.style.left = (e.clientX + 12) + 'px'; tt.style.top = (e.clientY - 8) + 'px';
|
|
2886
|
+
} else { tt.style.display = 'none'; }
|
|
3260
2887
|
});
|
|
3261
2888
|
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
if(
|
|
3267
|
-
if(!asset) return;
|
|
3268
|
-
if(!
|
|
3269
|
-
else if(
|
|
3270
|
-
|
|
3271
|
-
|
|
2889
|
+
mapWrap.addEventListener('click', function(e) {
|
|
2890
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
2891
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
2892
|
+
var asset = mGetAssetAt(sx, sy);
|
|
2893
|
+
if (mConnectMode) {
|
|
2894
|
+
if (!asset) return;
|
|
2895
|
+
if (!mConnectFirst) { mConnectFirst = asset.id; drawMap(); }
|
|
2896
|
+
else if (mConnectFirst !== asset.id) {
|
|
2897
|
+
mLocalConns.push({ id: crypto.randomUUID(), sourceAssetId: mConnectFirst, targetAssetId: asset.id, type: 'connection' });
|
|
2898
|
+
mConnectFirst = null; drawMap();
|
|
3272
2899
|
}
|
|
3273
2900
|
return;
|
|
3274
2901
|
}
|
|
3275
|
-
if(asset){
|
|
3276
|
-
else{
|
|
3277
|
-
|
|
2902
|
+
if (asset) { mSelectedId = asset.id; mShowDetail(asset); }
|
|
2903
|
+
else { mSelectedId = null; document.getElementById('map-detail').classList.remove('open'); }
|
|
2904
|
+
drawMap();
|
|
3278
2905
|
});
|
|
3279
2906
|
|
|
3280
|
-
|
|
3281
|
-
let lastTouches=[];
|
|
3282
|
-
wrap.addEventListener('touchstart',e=>{lastTouches=[...e.touches];},{passive:true});
|
|
3283
|
-
wrap.addEventListener('touchmove',e=>{
|
|
3284
|
-
if(e.touches.length===1){
|
|
3285
|
-
vx+=e.touches[0].clientX-lastTouches[0].clientX;
|
|
3286
|
-
vy+=e.touches[0].clientY-lastTouches[0].clientY;draw();
|
|
3287
|
-
} else if(e.touches.length===2){
|
|
3288
|
-
const d0=Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX,lastTouches[0].clientY-lastTouches[1].clientY);
|
|
3289
|
-
const d1=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
|
|
3290
|
-
const mx=(e.touches[0].clientX+e.touches[1].clientX)/2;
|
|
3291
|
-
const my=(e.touches[0].clientY+e.touches[1].clientY)/2;
|
|
3292
|
-
applyZoom(d1/d0,mx,my);
|
|
3293
|
-
}
|
|
3294
|
-
lastTouches=[...e.touches];
|
|
3295
|
-
},{passive:true});
|
|
3296
|
-
|
|
3297
|
-
wrap.addEventListener('wheel',e=>{
|
|
2907
|
+
mapWrap.addEventListener('wheel', function(e) {
|
|
3298
2908
|
e.preventDefault();
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
},{passive:false});
|
|
3302
|
-
|
|
3303
|
-
function
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
document.getElementById('zoom-pct').textContent=Math.round(
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
2909
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
2910
|
+
mApplyZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, e.clientX - rect.left, e.clientY - rect.top);
|
|
2911
|
+
}, { passive: false });
|
|
2912
|
+
|
|
2913
|
+
function mApplyZoom(factor, sx, sy) {
|
|
2914
|
+
var ns = Math.max(0.05, Math.min(8, mScale * factor));
|
|
2915
|
+
var wx = (sx - mvx) / mScale, wy = (sy - mvy) / mScale;
|
|
2916
|
+
mScale = ns; mvx = sx - wx * mScale; mvy = sy - wy * mScale;
|
|
2917
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
2918
|
+
drawMap();
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
document.getElementById('mz-in').addEventListener('click', function() { mApplyZoom(1.25, mW / 2, mH / 2); });
|
|
2922
|
+
document.getElementById('mz-out').addEventListener('click', function() { mApplyZoom(1 / 1.25, mW / 2, mH / 2); });
|
|
2923
|
+
|
|
2924
|
+
// \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
|
|
2925
|
+
function mEsc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
2926
|
+
function mRenderQ(s) {
|
|
2927
|
+
var c = s >= 70 ? '#22c55e' : s >= 40 ? '#f97316' : '#ef4444';
|
|
2928
|
+
return s + '/100 <div class="quality-bar"><div class="quality-fill" style="width:' + s + '%;background:' + c + '"></div></div>';
|
|
2929
|
+
}
|
|
2930
|
+
function mShowDetail(asset) {
|
|
2931
|
+
document.getElementById('md-name').textContent = asset.name;
|
|
2932
|
+
var body = document.getElementById('md-body');
|
|
2933
|
+
var rows = [['Domain', asset.domain], ['Sub-domain', asset.subDomain],
|
|
2934
|
+
['Quality', asset.qualityScore !== null ? mRenderQ(asset.qualityScore) : null]
|
|
2935
|
+
].concat(Object.entries(asset.metadata || {}).slice(0, 8).map(function(kv) { return [kv[0], String(kv[1])]; }))
|
|
2936
|
+
.filter(function(r) { return r[1] !== null && r[1] !== undefined && r[1] !== ''; });
|
|
2937
|
+
body.innerHTML = rows.map(function(r) {
|
|
2938
|
+
return '<div class="meta-row"><div class="meta-label">' + mEsc(String(r[0])) + '</div><div class="meta-value">' + r[1] + '</div></div>';
|
|
2939
|
+
}).join('');
|
|
2940
|
+
var related = mLocalConns.filter(function(cn) { return cn.sourceAssetId === asset.id || cn.targetAssetId === asset.id; });
|
|
2941
|
+
if (related.length > 0) {
|
|
2942
|
+
body.innerHTML += '<div class="meta-row"><div class="meta-label">Connections (' + related.length + ')</div><div>' +
|
|
2943
|
+
related.map(function(cn) {
|
|
2944
|
+
var oid = cn.sourceAssetId === asset.id ? cn.targetAssetId : cn.sourceAssetId;
|
|
2945
|
+
var o = mapAssetIndex.get(oid);
|
|
2946
|
+
return '<div class="meta-value" style="margin-top:4px;font-size:12px">' + (o ? mEsc(o.name) : oid) + '</div>';
|
|
2947
|
+
}).join('') + '</div></div>';
|
|
2948
|
+
}
|
|
2949
|
+
document.getElementById('map-detail').classList.add('open');
|
|
2950
|
+
}
|
|
2951
|
+
document.getElementById('md-close').addEventListener('click', function() {
|
|
2952
|
+
document.getElementById('map-detail').classList.remove('open'); mSelectedId = null; drawMap();
|
|
3325
2953
|
});
|
|
3326
2954
|
|
|
3327
|
-
// \u2500\u2500
|
|
3328
|
-
function
|
|
3329
|
-
|
|
3330
|
-
const body=document.getElementById('dp-body');
|
|
3331
|
-
const rows=[['Domain',asset.domain],['Sub-domain',asset.subDomain],
|
|
3332
|
-
['Quality Score',asset.qualityScore!==null?renderQuality(asset.qualityScore):null],
|
|
3333
|
-
...Object.entries(asset.metadata||{}).slice(0,8).map(([k,v])=>[k,String(v)])
|
|
3334
|
-
].filter(([,v])=>v!==null&&v!==undefined&&v!=='');
|
|
3335
|
-
body.innerHTML=rows.map(([l,v])=>'<div class="meta-row"><div class="meta-label">'+esc(String(l))+'</div><div class="meta-value">'+v+'</div></div>').join('');
|
|
3336
|
-
const related=localConnections.filter(c=>c.sourceAssetId===asset.id||c.targetAssetId===asset.id);
|
|
3337
|
-
if(related.length>0){
|
|
3338
|
-
body.innerHTML+='<div class="meta-row"><div class="meta-label">Connections ('+related.length+')</div><div>'+
|
|
3339
|
-
related.map(c=>{const oid=c.sourceAssetId===asset.id?c.targetAssetId:c.sourceAssetId;
|
|
3340
|
-
const o=assetIndex.get(oid);return '<div class="meta-value" style="margin-top:4px;font-size:12px">'+(o?esc(o.name):oid)+'</div>';}).join('')+'</div></div>';
|
|
3341
|
-
}
|
|
3342
|
-
document.getElementById('detail-panel').classList.add('open');
|
|
3343
|
-
}
|
|
3344
|
-
function renderQuality(s){
|
|
3345
|
-
const c=s>=70?'#22c55e':s>=40?'#f97316':'#ef4444';
|
|
3346
|
-
return s+'/100 <div class="quality-bar"><div class="quality-fill" style="width:'+s+'%;background:'+c+'"></div></div>';
|
|
3347
|
-
}
|
|
3348
|
-
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
3349
|
-
document.getElementById('dp-close').addEventListener('click',()=>{
|
|
3350
|
-
document.getElementById('detail-panel').classList.remove('open');selectedAssetId=null;draw();
|
|
2955
|
+
// \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
|
|
2956
|
+
document.getElementById('btn-labels').addEventListener('click', function() {
|
|
2957
|
+
mShowLabels = !mShowLabels; this.classList.toggle('active', mShowLabels); drawMap();
|
|
3351
2958
|
});
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
|
|
3357
|
-
document.getElementById('dl-'+n).classList.add('active');draw();
|
|
3358
|
-
});
|
|
2959
|
+
document.getElementById('btn-all-labels').addEventListener('click', function() {
|
|
2960
|
+
mShowAllLabels = !mShowAllLabels; this.classList.toggle('active', mShowAllLabels);
|
|
2961
|
+
if (mShowAllLabels && !mShowLabels) { mShowLabels = true; document.getElementById('btn-labels').classList.add('active'); }
|
|
2962
|
+
drawMap();
|
|
3359
2963
|
});
|
|
3360
|
-
document.getElementById('btn-
|
|
3361
|
-
|
|
2964
|
+
document.getElementById('btn-quality').addEventListener('click', function() {
|
|
2965
|
+
mShowQuality = !mShowQuality; this.classList.toggle('active', mShowQuality); drawMap();
|
|
3362
2966
|
});
|
|
3363
|
-
document.getElementById('btn-
|
|
3364
|
-
|
|
2967
|
+
document.getElementById('btn-connect').addEventListener('click', function() {
|
|
2968
|
+
mConnectMode = !mConnectMode; mConnectFirst = null;
|
|
2969
|
+
this.classList.toggle('active', mConnectMode);
|
|
2970
|
+
mapWrap.classList.toggle('connecting', mConnectMode);
|
|
2971
|
+
document.getElementById('map-connect-hint').style.display = mConnectMode ? 'block' : 'none'; drawMap();
|
|
3365
2972
|
});
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
}
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
2973
|
+
|
|
2974
|
+
// Map keyboard
|
|
2975
|
+
mapWrap.addEventListener('keydown', function(e) {
|
|
2976
|
+
if (e.key === 'ArrowLeft') { mvx += 40; drawMap(); }
|
|
2977
|
+
else if (e.key === 'ArrowRight') { mvx -= 40; drawMap(); }
|
|
2978
|
+
else if (e.key === 'ArrowUp') { mvy += 40; drawMap(); }
|
|
2979
|
+
else if (e.key === 'ArrowDown') { mvy -= 40; drawMap(); }
|
|
2980
|
+
else if (e.key === '+' || e.key === '=') mApplyZoom(1.2, mW / 2, mH / 2);
|
|
2981
|
+
else if (e.key === '-') mApplyZoom(1 / 1.2, mW / 2, mH / 2);
|
|
2982
|
+
else if (e.key === 'Escape') {
|
|
2983
|
+
mSelectedId = null; document.getElementById('map-detail').classList.remove('open');
|
|
2984
|
+
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'); }
|
|
2985
|
+
drawMap();
|
|
2986
|
+
}
|
|
3377
2987
|
});
|
|
3378
|
-
document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
|
|
3379
2988
|
|
|
3380
|
-
//
|
|
3381
|
-
|
|
3382
|
-
document.getElementById('zoom-pct').textContent=Math.round(
|
|
3383
|
-
|
|
3384
|
-
|
|
2989
|
+
// Map init
|
|
2990
|
+
resizeMap(); mapFitToView();
|
|
2991
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
2992
|
+
drawMap();
|
|
2993
|
+
|
|
2994
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2995
|
+
// TOPOLOGY VIEW (lazy init)
|
|
2996
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2997
|
+
var TOPO = ${graphData};
|
|
2998
|
+
|
|
2999
|
+
var TYPE_COLORS = {
|
|
3000
|
+
host:'#4a9eff',database_server:'#ff6b6b',database:'#ff8c42',
|
|
3001
|
+
web_service:'#6bcb77',api_endpoint:'#4d96ff',cache_server:'#ffd93d',
|
|
3002
|
+
message_broker:'#c77dff',queue:'#e0aaff',topic:'#9d4edd',
|
|
3003
|
+
container:'#48cae4',pod:'#00b4d8',k8s_cluster:'#0077b6',
|
|
3004
|
+
config_file:'#adb5bd',saas_tool:'#c084fc',table:'#f97316',unknown:'#6c757d'
|
|
3005
|
+
};
|
|
3006
|
+
var LAYER_COLORS = { saas:'#c084fc',web:'#6bcb77',data:'#ff6b6b',messaging:'#c77dff',infra:'#4a9eff',config:'#adb5bd',other:'#6c757d' };
|
|
3007
|
+
var LAYER_NAMES = { saas:'SaaS Tools',web:'Web / API',data:'Data Layer',messaging:'Messaging',infra:'Infrastructure',config:'Config',other:'Other' };
|
|
3008
|
+
|
|
3009
|
+
var topoSelectedId = null;
|
|
3010
|
+
|
|
3011
|
+
function setTopoSearch(q) {
|
|
3012
|
+
var el = document.getElementById('topo-search');
|
|
3013
|
+
if (el) { el.value = q; buildTopoList(q); }
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
function buildTopoList(filter) {
|
|
3017
|
+
var listEl = document.getElementById('topo-list');
|
|
3018
|
+
var q = (filter || '').toLowerCase();
|
|
3019
|
+
listEl.innerHTML = '';
|
|
3020
|
+
var sorted = TOPO.nodes.slice().sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
3021
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
3022
|
+
var d = sorted[i];
|
|
3023
|
+
if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
|
|
3024
|
+
var item = document.createElement('div');
|
|
3025
|
+
item.className = 'topo-item' + (d.id === topoSelectedId ? ' active' : '');
|
|
3026
|
+
item.dataset.id = d.id;
|
|
3027
|
+
var color = TYPE_COLORS[d.type] || '#aaa';
|
|
3028
|
+
item.innerHTML = '<span class="topo-dot" style="background:' + color + '"></span>' +
|
|
3029
|
+
'<span class="topo-name" title="' + d.id + '">' + d.name + '</span>' +
|
|
3030
|
+
'<span class="topo-type">' + d.type.replace(/_/g, ' ') + '</span>';
|
|
3031
|
+
(function(dd) { item.onclick = function() { selectTopoNode(dd); focusTopoNode(dd); }; })(d);
|
|
3032
|
+
listEl.appendChild(item);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
document.getElementById('topo-search').addEventListener('input', function(e) { buildTopoList(e.target.value); });
|
|
3037
|
+
|
|
3038
|
+
var topoSidebar = document.getElementById('topo-sidebar');
|
|
3039
|
+
|
|
3040
|
+
function selectTopoNode(d) {
|
|
3041
|
+
topoSelectedId = d.id;
|
|
3042
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3043
|
+
showTopoNode(d);
|
|
3044
|
+
if (typeof d3 !== 'undefined') d3.selectAll('.node-hex').classed('selected', function(nd) { return nd.id === d.id; });
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
function showTopoNode(d) {
|
|
3048
|
+
var c = TYPE_COLORS[d.type] || '#aaa';
|
|
3049
|
+
var confPct = Math.round(d.confidence * 100);
|
|
3050
|
+
var tags = (d.tags || []).map(function(t) { return '<span class="tag">' + t + '</span>'; }).join('');
|
|
3051
|
+
var metaRows = Object.entries(d.metadata || {})
|
|
3052
|
+
.filter(function(kv) { return kv[1] !== null && kv[1] !== undefined && String(kv[1]).length > 0; })
|
|
3053
|
+
.map(function(kv) { return '<tr><td>' + kv[0] + '</td><td>' + JSON.stringify(kv[1]) + '</td></tr>'; }).join('');
|
|
3054
|
+
var related = TOPO.links.filter(function(l) {
|
|
3055
|
+
return (l.source.id || l.source) === d.id || (l.target.id || l.target) === d.id;
|
|
3056
|
+
});
|
|
3057
|
+
var edgeItems = related.map(function(l) {
|
|
3058
|
+
var isOut = (l.source.id || l.source) === d.id;
|
|
3059
|
+
var other = isOut ? (l.target.id || l.target) : (l.source.id || l.source);
|
|
3060
|
+
return '<div class="edge-item">' + (isOut ? '\\u2192' : '\\u2190') + ' <span>' + other + '</span> <small>[' + l.relationship + ']</small></div>';
|
|
3061
|
+
}).join('');
|
|
3062
|
+
|
|
3063
|
+
topoSidebar.innerHTML =
|
|
3064
|
+
'<h2>' + d.name + '</h2>' +
|
|
3065
|
+
'<table class="meta-table">' +
|
|
3066
|
+
'<tr><td>ID</td><td style="font-size:10px;word-break:break-all">' + d.id + '</td></tr>' +
|
|
3067
|
+
'<tr><td>Type</td><td><span style="color:' + c + '">' + d.type + '</span></td></tr>' +
|
|
3068
|
+
'<tr><td>Layer</td><td>' + d.layer + '</td></tr>' +
|
|
3069
|
+
'<tr><td>Confidence</td><td>' + confPct + '% <div class="conf-bar"><div class="conf-fill" style="width:' + confPct + '%;background:' + c + '"></div></div></td></tr>' +
|
|
3070
|
+
'<tr><td>Via</td><td>' + (d.discoveredVia || '\\u2014') + '</td></tr>' +
|
|
3071
|
+
'<tr><td>Timestamp</td><td>' + (d.discoveredAt ? d.discoveredAt.substring(0, 19).replace('T', ' ') : '\\u2014') + '</td></tr>' +
|
|
3072
|
+
(tags ? '<tr><td>Tags</td><td>' + tags + '</td></tr>' : '') +
|
|
3073
|
+
metaRows + '</table>' +
|
|
3074
|
+
(related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>' + edgeItems + '</div>' : '') +
|
|
3075
|
+
'<div style="margin-top:14px"><button class="export-btn" style="width:100%" onclick="deleteTopoNode(\\'' + d.id.replace(/'/g, "\\\\'") + '\\')">Delete node</button></div>';
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
function deleteTopoNode(id) {
|
|
3079
|
+
var idx = TOPO.nodes.findIndex(function(n) { return n.id === id; });
|
|
3080
|
+
if (idx === -1) return;
|
|
3081
|
+
TOPO.nodes.splice(idx, 1);
|
|
3082
|
+
TOPO.links = TOPO.links.filter(function(l) {
|
|
3083
|
+
return (l.source.id || l.source) !== id && (l.target.id || l.target) !== id;
|
|
3084
|
+
});
|
|
3085
|
+
topoSelectedId = null;
|
|
3086
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
|
|
3087
|
+
if (typeof rebuildTopoGraph === 'function') rebuildTopoGraph();
|
|
3088
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
function initTopology() {
|
|
3092
|
+
if (typeof d3 === 'undefined') return;
|
|
3093
|
+
|
|
3094
|
+
var svgEl = d3.select('#topo-graph svg');
|
|
3095
|
+
var graphDiv = document.getElementById('topo-graph');
|
|
3096
|
+
var gW = function() { return graphDiv.clientWidth; };
|
|
3097
|
+
var gH = function() { return graphDiv.clientHeight; };
|
|
3098
|
+
var g = svgEl.append('g');
|
|
3099
|
+
|
|
3100
|
+
svgEl.append('defs').append('marker')
|
|
3101
|
+
.attr('id', 'arrow').attr('viewBox', '0 0 10 6')
|
|
3102
|
+
.attr('refX', 10).attr('refY', 3)
|
|
3103
|
+
.attr('markerWidth', 8).attr('markerHeight', 6)
|
|
3104
|
+
.attr('orient', 'auto')
|
|
3105
|
+
.append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
|
|
3106
|
+
|
|
3107
|
+
var currentZoom = 1;
|
|
3108
|
+
var zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', function(e) {
|
|
3109
|
+
g.attr('transform', e.transform); currentZoom = e.transform.k; updateTopoLOD(currentZoom);
|
|
3110
|
+
});
|
|
3111
|
+
svgEl.call(zoomBehavior);
|
|
3112
|
+
|
|
3113
|
+
// Layer filters
|
|
3114
|
+
var layers = Array.from(new Set(TOPO.nodes.map(function(d) { return d.layer; })));
|
|
3115
|
+
var layerVisible = {};
|
|
3116
|
+
layers.forEach(function(l) { layerVisible[l] = true; });
|
|
3117
|
+
|
|
3118
|
+
var toolbarEl = document.getElementById('topo-toolbar');
|
|
3119
|
+
layers.forEach(function(layer) {
|
|
3120
|
+
var btn = document.createElement('button');
|
|
3121
|
+
btn.className = 'filter-btn';
|
|
3122
|
+
btn.innerHTML = '<span class="filter-dot" style="background:' + (LAYER_COLORS[layer] || '#666') + '"></span>' + (LAYER_NAMES[layer] || layer);
|
|
3123
|
+
btn.onclick = function() { layerVisible[layer] = !layerVisible[layer]; btn.classList.toggle('off', !layerVisible[layer]); updateTopoVisibility(); };
|
|
3124
|
+
toolbarEl.appendChild(btn);
|
|
3125
|
+
});
|
|
3126
|
+
|
|
3127
|
+
// JGF export button
|
|
3128
|
+
var jgfBtn = document.createElement('button');
|
|
3129
|
+
jgfBtn.className = 'export-btn'; jgfBtn.textContent = '\\u2193 JGF'; jgfBtn.title = 'Export JSON Graph Format';
|
|
3130
|
+
jgfBtn.onclick = function() {
|
|
3131
|
+
var jgf = { graph: { directed: true, type: 'cartography', label: 'Infrastructure Map',
|
|
3132
|
+
metadata: { exportedAt: new Date().toISOString() },
|
|
3133
|
+
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 } }]; })),
|
|
3134
|
+
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 } }; })
|
|
3135
|
+
}};
|
|
3136
|
+
var blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
|
|
3137
|
+
var url = URL.createObjectURL(blob);
|
|
3138
|
+
var a = document.createElement('a'); a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
|
|
3139
|
+
URL.revokeObjectURL(url);
|
|
3140
|
+
};
|
|
3141
|
+
toolbarEl.appendChild(jgfBtn);
|
|
3142
|
+
|
|
3143
|
+
// Hex helpers
|
|
3144
|
+
var T_HEX = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
|
|
3145
|
+
function tHexSize(d) { return T_HEX[d.type] || T_HEX.default; }
|
|
3146
|
+
function tHexPath(size) {
|
|
3147
|
+
var pts = [];
|
|
3148
|
+
for (var i = 0; i < 6; i++) {
|
|
3149
|
+
var angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
3150
|
+
pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
|
|
3151
|
+
}
|
|
3152
|
+
return 'M' + pts.map(function(p) { return p.join(','); }).join('L') + 'Z';
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// Cluster force
|
|
3156
|
+
function clusterForce(alpha) {
|
|
3157
|
+
var centroids = {}, counts = {};
|
|
3158
|
+
TOPO.nodes.forEach(function(d) {
|
|
3159
|
+
if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
|
|
3160
|
+
centroids[d.layer].x += d.x || 0; centroids[d.layer].y += d.y || 0; counts[d.layer]++;
|
|
3161
|
+
});
|
|
3162
|
+
for (var l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
|
|
3163
|
+
var strength = alpha * 0.15;
|
|
3164
|
+
TOPO.nodes.forEach(function(d) {
|
|
3165
|
+
var cn = centroids[d.layer];
|
|
3166
|
+
if (cn) { d.vx += (cn.x - d.x) * strength; d.vy += (cn.y - d.y) * strength; }
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// Hulls
|
|
3171
|
+
var hullGroup = g.append('g').attr('class', 'hulls');
|
|
3172
|
+
var hullPaths = {}, hullLabels = {};
|
|
3173
|
+
layers.forEach(function(layer) {
|
|
3174
|
+
hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
|
|
3175
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
|
|
3176
|
+
hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
|
|
3177
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
|
|
3178
|
+
});
|
|
3179
|
+
|
|
3180
|
+
function updateHulls() {
|
|
3181
|
+
layers.forEach(function(layer) {
|
|
3182
|
+
if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
|
|
3183
|
+
var pts = TOPO.nodes.filter(function(d) { return d.layer === layer && layerVisible[d.layer]; }).map(function(d) { return [d.x, d.y]; });
|
|
3184
|
+
if (pts.length < 3) {
|
|
3185
|
+
hullPaths[layer].attr('d', null);
|
|
3186
|
+
if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
|
|
3187
|
+
else hullLabels[layer].attr('x', -9999);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
var hull = d3.polygonHull(pts);
|
|
3191
|
+
if (!hull) { hullPaths[layer].attr('d', null); return; }
|
|
3192
|
+
var cx = d3.mean(hull, function(p) { return p[0]; });
|
|
3193
|
+
var cy = d3.mean(hull, function(p) { return p[1]; });
|
|
3194
|
+
var padded = hull.map(function(p) {
|
|
3195
|
+
var dx = p[0] - cx, dy = p[1] - cy;
|
|
3196
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3197
|
+
return [p[0] + dx / len * 40, p[1] + dy / len * 40];
|
|
3198
|
+
});
|
|
3199
|
+
hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
|
|
3200
|
+
hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, function(p) { return Math.abs(p[1] - cy); }) - 30);
|
|
3201
|
+
});
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// Graph
|
|
3205
|
+
var linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
|
|
3206
|
+
var linkGroup = g.append('g');
|
|
3207
|
+
var nodeGroup = g.append('g');
|
|
3208
|
+
|
|
3209
|
+
function focusTopoNode(d) {
|
|
3210
|
+
if (!d.x || !d.y) return;
|
|
3211
|
+
var w = gW(), h = gH();
|
|
3212
|
+
svgEl.transition().duration(500).call(
|
|
3213
|
+
zoomBehavior.transform,
|
|
3214
|
+
d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
window.focusTopoNode = focusTopoNode;
|
|
3218
|
+
|
|
3219
|
+
function rebuildTopoGraph() {
|
|
3220
|
+
if (sim) sim.stop();
|
|
3221
|
+
|
|
3222
|
+
linkSel = linkGroup.selectAll('line').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
3223
|
+
linkSel.exit().remove();
|
|
3224
|
+
var linkEnter = linkSel.enter().append('line').attr('class', 'link');
|
|
3225
|
+
linkSel = linkEnter.merge(linkSel)
|
|
3226
|
+
.attr('stroke', function(d) { return d.confidence < 0.6 ? '#2a2e35' : '#3d434b'; })
|
|
3227
|
+
.attr('stroke-dasharray', function(d) { return d.confidence < 0.6 ? '4 3' : null; })
|
|
3228
|
+
.attr('stroke-width', function(d) { return d.confidence < 0.6 ? 0.8 : 1.2; })
|
|
3229
|
+
.attr('marker-end', 'url(#arrow)');
|
|
3230
|
+
linkSel.select('title').remove();
|
|
3231
|
+
linkSel.append('title').text(function(d) { return d.relationship + ' (' + Math.round(d.confidence * 100) + '%)\\n' + (d.evidence || ''); });
|
|
3232
|
+
|
|
3233
|
+
linkLabelSel = linkGroup.selectAll('text').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
3234
|
+
linkLabelSel.exit().remove();
|
|
3235
|
+
linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel).text(function(d) { return d.relationship; });
|
|
3236
|
+
|
|
3237
|
+
nodeSel = nodeGroup.selectAll('g').data(TOPO.nodes, function(d) { return d.id; });
|
|
3238
|
+
nodeSel.exit().remove();
|
|
3239
|
+
var nodeEnter = nodeSel.enter().append('g')
|
|
3240
|
+
.call(d3.drag()
|
|
3241
|
+
.on('start', function(e, d) { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
3242
|
+
.on('drag', function(e, d) { d.fx = e.x; d.fy = e.y; })
|
|
3243
|
+
.on('end', function(e, d) { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
3244
|
+
)
|
|
3245
|
+
.on('click', function(e, d) { e.stopPropagation(); selectTopoNode(d); });
|
|
3246
|
+
nodeEnter.append('path').attr('class', 'node-hex');
|
|
3247
|
+
nodeEnter.append('title');
|
|
3248
|
+
nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
|
|
3249
|
+
|
|
3250
|
+
nodeSel = nodeEnter.merge(nodeSel);
|
|
3251
|
+
nodeSel.select('.node-hex')
|
|
3252
|
+
.attr('d', function(d) { return tHexPath(tHexSize(d)); })
|
|
3253
|
+
.attr('fill', function(d) { return TYPE_COLORS[d.type] || '#aaa'; })
|
|
3254
|
+
.attr('stroke', function(d) { var c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
|
|
3255
|
+
.attr('fill-opacity', function(d) { return 0.6 + d.confidence * 0.4; })
|
|
3256
|
+
.classed('selected', function(d) { return d.id === topoSelectedId; });
|
|
3257
|
+
nodeSel.select('title').text(function(d) { return d.name + ' (' + d.type + ')\\nconf: ' + Math.round(d.confidence * 100) + '%'; });
|
|
3258
|
+
nodeLabelSel = nodeSel.select('.node-label')
|
|
3259
|
+
.attr('dy', function(d) { return tHexSize(d) + 13; })
|
|
3260
|
+
.text(function(d) { return d.name.length > 20 ? d.name.substring(0, 18) + '\\u2026' : d.name; });
|
|
3261
|
+
|
|
3262
|
+
sim = d3.forceSimulation(TOPO.nodes)
|
|
3263
|
+
.force('link', d3.forceLink(TOPO.links).id(function(d) { return d.id; }).distance(function(d) { return d.relationship === 'contains' ? 50 : 100; }).strength(0.4))
|
|
3264
|
+
.force('charge', d3.forceManyBody().strength(-280))
|
|
3265
|
+
.force('center', d3.forceCenter(gW() / 2, gH() / 2))
|
|
3266
|
+
.force('collision', d3.forceCollide().radius(function(d) { return tHexSize(d) + 10; }))
|
|
3267
|
+
.force('cluster', clusterForce)
|
|
3268
|
+
.on('tick', function() {
|
|
3269
|
+
updateHulls();
|
|
3270
|
+
linkSel.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; })
|
|
3271
|
+
.attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; });
|
|
3272
|
+
linkLabelSel.attr('x', function(d) { return (d.source.x + d.target.x) / 2; })
|
|
3273
|
+
.attr('y', function(d) { return (d.source.y + d.target.y) / 2 - 4; });
|
|
3274
|
+
nodeSel.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
window.rebuildTopoGraph = rebuildTopoGraph;
|
|
3278
|
+
|
|
3279
|
+
function updateTopoLOD(k) {
|
|
3280
|
+
if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
|
|
3281
|
+
if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
|
|
3282
|
+
d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function updateTopoVisibility() {
|
|
3286
|
+
if (!nodeSel) return;
|
|
3287
|
+
nodeSel.style('display', function(d) { return layerVisible[d.layer] ? null : 'none'; });
|
|
3288
|
+
linkSel.style('display', function(d) {
|
|
3289
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
3290
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
3291
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
3292
|
+
});
|
|
3293
|
+
linkLabelSel.style('display', function(d) {
|
|
3294
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
3295
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
3296
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
rebuildTopoGraph();
|
|
3301
|
+
buildTopoList();
|
|
3302
|
+
updateTopoLOD(1);
|
|
3303
|
+
|
|
3304
|
+
svgEl.on('click', function() {
|
|
3305
|
+
topoSelectedId = null;
|
|
3306
|
+
d3.selectAll('.node-hex').classed('selected', false);
|
|
3307
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3308
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// Init topology node list (non-D3 part)
|
|
3313
|
+
buildTopoList();
|
|
3385
3314
|
</script>
|
|
3386
3315
|
</body>
|
|
3387
3316
|
</html>`;
|
|
3388
3317
|
}
|
|
3389
|
-
function
|
|
3318
|
+
function exportJGF(nodes, edges) {
|
|
3319
|
+
const jgf = {
|
|
3320
|
+
graph: {
|
|
3321
|
+
directed: true,
|
|
3322
|
+
type: "cartography",
|
|
3323
|
+
label: "Infrastructure Map",
|
|
3324
|
+
metadata: { exportedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3325
|
+
nodes: Object.fromEntries(nodes.map((n) => [n.id, {
|
|
3326
|
+
label: n.name,
|
|
3327
|
+
metadata: {
|
|
3328
|
+
type: n.type,
|
|
3329
|
+
layer: nodeLayer(n.type),
|
|
3330
|
+
confidence: n.confidence,
|
|
3331
|
+
discoveredVia: n.discoveredVia,
|
|
3332
|
+
discoveredAt: n.discoveredAt,
|
|
3333
|
+
tags: n.tags,
|
|
3334
|
+
metadata: n.metadata
|
|
3335
|
+
}
|
|
3336
|
+
}])),
|
|
3337
|
+
edges: edges.map((e) => ({
|
|
3338
|
+
source: e.sourceId,
|
|
3339
|
+
target: e.targetId,
|
|
3340
|
+
relation: e.relationship,
|
|
3341
|
+
metadata: { confidence: e.confidence, evidence: e.evidence }
|
|
3342
|
+
}))
|
|
3343
|
+
}
|
|
3344
|
+
};
|
|
3345
|
+
return JSON.stringify(jgf, null, 2);
|
|
3346
|
+
}
|
|
3347
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "discovery", "sops"]) {
|
|
3390
3348
|
mkdirSync2(outputDir, { recursive: true });
|
|
3391
3349
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
3392
3350
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
3393
3351
|
const nodes = db.getNodes(sessionId);
|
|
3394
3352
|
const edges = db.getEdges(sessionId);
|
|
3353
|
+
const jgfPath = join2(outputDir, "cartography-graph.jgf.json");
|
|
3354
|
+
writeFileSync(jgfPath, exportJGF(nodes, edges));
|
|
3355
|
+
process.stderr.write("\u2713 cartography-graph.jgf.json\n");
|
|
3395
3356
|
if (formats.includes("mermaid")) {
|
|
3396
3357
|
writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
3397
3358
|
writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
@@ -3405,13 +3366,9 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
3405
3366
|
writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
3406
3367
|
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
3407
3368
|
}
|
|
3408
|
-
if (formats.includes("html")) {
|
|
3409
|
-
writeFileSync(join2(outputDir, "
|
|
3410
|
-
process.stderr.write("\u2713
|
|
3411
|
-
}
|
|
3412
|
-
if (formats.includes("map")) {
|
|
3413
|
-
writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
|
|
3414
|
-
process.stderr.write("\u2713 cartography-map.html\n");
|
|
3369
|
+
if (formats.includes("html") || formats.includes("map") || formats.includes("discovery")) {
|
|
3370
|
+
writeFileSync(join2(outputDir, "discovery.html"), exportDiscoveryApp(nodes, edges));
|
|
3371
|
+
process.stderr.write("\u2713 discovery.html\n");
|
|
3415
3372
|
}
|
|
3416
3373
|
if (formats.includes("sops")) {
|
|
3417
3374
|
const sops = db.getSOPs(sessionId);
|
|
@@ -3477,8 +3434,8 @@ export {
|
|
|
3477
3434
|
edgesToConnections,
|
|
3478
3435
|
exportAll,
|
|
3479
3436
|
exportBackstageYAML,
|
|
3480
|
-
|
|
3481
|
-
|
|
3437
|
+
exportDiscoveryApp,
|
|
3438
|
+
exportJGF,
|
|
3482
3439
|
exportJSON,
|
|
3483
3440
|
exportSOPDashboard,
|
|
3484
3441
|
exportSOPMarkdown,
|