@datasynx/agentic-ai-cartography 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +65 -1247
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.js +45 -1174
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/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,646 +2227,6 @@ function toggle(i) {
|
|
|
2746
2227
|
</body>
|
|
2747
2228
|
</html>`;
|
|
2748
2229
|
}
|
|
2749
|
-
function exportCartographyMap(nodes, edges, options) {
|
|
2750
|
-
const mapData = buildMapData(nodes, edges, options);
|
|
2751
|
-
const { assets, clusters, connections, meta } = mapData;
|
|
2752
|
-
const isEmpty = assets.length === 0;
|
|
2753
|
-
const HEX_SIZE2 = 24;
|
|
2754
|
-
const dataJson = JSON.stringify({
|
|
2755
|
-
assets: assets.map((a) => ({
|
|
2756
|
-
id: a.id,
|
|
2757
|
-
name: a.name,
|
|
2758
|
-
domain: a.domain,
|
|
2759
|
-
subDomain: a.subDomain ?? null,
|
|
2760
|
-
qualityScore: a.qualityScore ?? null,
|
|
2761
|
-
metadata: a.metadata,
|
|
2762
|
-
q: a.position.q,
|
|
2763
|
-
r: a.position.r
|
|
2764
|
-
})),
|
|
2765
|
-
clusters: clusters.map((c) => ({
|
|
2766
|
-
id: c.id,
|
|
2767
|
-
label: c.label,
|
|
2768
|
-
domain: c.domain,
|
|
2769
|
-
color: c.color,
|
|
2770
|
-
assetIds: c.assetIds,
|
|
2771
|
-
centroid: c.centroid
|
|
2772
|
-
})),
|
|
2773
|
-
connections: connections.map((c) => ({
|
|
2774
|
-
id: c.id,
|
|
2775
|
-
sourceAssetId: c.sourceAssetId,
|
|
2776
|
-
targetAssetId: c.targetAssetId,
|
|
2777
|
-
type: c.type ?? "connection"
|
|
2778
|
-
}))
|
|
2779
|
-
});
|
|
2780
|
-
return `<!DOCTYPE html>
|
|
2781
|
-
<html lang="en">
|
|
2782
|
-
<head>
|
|
2783
|
-
<meta charset="UTF-8"/>
|
|
2784
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
2785
|
-
<title>Data Cartography Map</title>
|
|
2786
|
-
<style>
|
|
2787
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
2788
|
-
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
2789
|
-
body{display:flex;flex-direction:column;background:${meta.theme === "dark" ? "#0f172a" : "#f8fafc"};color:${meta.theme === "dark" ? "#e2e8f0" : "#1e293b"}}
|
|
2790
|
-
#topbar{
|
|
2791
|
-
height:48px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2792
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};z-index:10;flex-shrink:0;
|
|
2793
|
-
}
|
|
2794
|
-
#topbar h1{font-size:15px;font-weight:600;letter-spacing:-0.01em}
|
|
2795
|
-
#search-box{
|
|
2796
|
-
display:flex;align-items:center;gap:8px;background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"};
|
|
2797
|
-
border-radius:8px;padding:5px 10px;margin-left:auto;
|
|
2798
|
-
}
|
|
2799
|
-
#search-box input{
|
|
2800
|
-
border:none;background:transparent;font-size:13px;outline:none;width:180px;color:inherit;
|
|
2801
|
-
}
|
|
2802
|
-
#search-box input::placeholder{color:#94a3b8}
|
|
2803
|
-
#main{flex:1;display:flex;overflow:hidden;position:relative}
|
|
2804
|
-
#canvas-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2805
|
-
#canvas-wrap.dragging{cursor:grabbing}
|
|
2806
|
-
#canvas-wrap.connecting{cursor:crosshair}
|
|
2807
|
-
canvas{display:block;width:100%;height:100%}
|
|
2808
|
-
/* Detail panel */
|
|
2809
|
-
#detail-panel{
|
|
2810
|
-
width:280px;background:${meta.theme === "dark" ? "#1e293b" : "#fff"};border-left:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2811
|
-
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2812
|
-
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2813
|
-
}
|
|
2814
|
-
#detail-panel.open{transform:translateX(0)}
|
|
2815
|
-
#detail-panel .panel-header{
|
|
2816
|
-
padding:16px;border-bottom:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};display:flex;align-items:center;gap:10px;
|
|
2817
|
-
}
|
|
2818
|
-
#detail-panel .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2819
|
-
#detail-panel .close-btn{
|
|
2820
|
-
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2821
|
-
color:#94a3b8;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2822
|
-
}
|
|
2823
|
-
#detail-panel .close-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2824
|
-
#detail-panel .panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2825
|
-
#detail-panel .meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2826
|
-
#detail-panel .meta-label{font-size:11px;font-weight:500;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
|
|
2827
|
-
#detail-panel .meta-value{font-size:13px;word-break:break-all}
|
|
2828
|
-
#detail-panel .quality-bar{height:6px;border-radius:3px;background:${meta.theme === "dark" ? "#334155" : "#e2e8f0"};margin-top:4px}
|
|
2829
|
-
#detail-panel .quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2830
|
-
/* Bottom-left toolbar */
|
|
2831
|
-
#toolbar-left{
|
|
2832
|
-
position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10;
|
|
2833
|
-
}
|
|
2834
|
-
.tb-btn{
|
|
2835
|
-
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2836
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2837
|
-
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2838
|
-
transition:all .15s;color:inherit;
|
|
2839
|
-
}
|
|
2840
|
-
.tb-btn:hover{border-color:#94a3b8}
|
|
2841
|
-
.tb-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6}
|
|
2842
|
-
/* Bottom-right toolbar */
|
|
2843
|
-
#toolbar-right{
|
|
2844
|
-
position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;
|
|
2845
|
-
align-items:flex-end;gap:8px;z-index:10;
|
|
2846
|
-
}
|
|
2847
|
-
#zoom-controls{display:flex;align-items:center;gap:6px}
|
|
2848
|
-
.zoom-btn{
|
|
2849
|
-
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2850
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2851
|
-
font-size:18px;color:inherit;display:flex;align-items:center;justify-content:center;
|
|
2852
|
-
}
|
|
2853
|
-
.zoom-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2854
|
-
#zoom-pct{font-size:12px;font-weight:500;color:#64748b;min-width:38px;text-align:center}
|
|
2855
|
-
#detail-selector{display:flex;flex-direction:column;gap:4px}
|
|
2856
|
-
.detail-btn{
|
|
2857
|
-
width:34px;height:34px;border-radius:8px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2858
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2859
|
-
font-size:12px;font-weight:600;color:#64748b;display:flex;align-items:center;justify-content:center;
|
|
2860
|
-
}
|
|
2861
|
-
.detail-btn:hover{background:${meta.theme === "dark" ? "#334155" : "#f1f5f9"}}
|
|
2862
|
-
.detail-btn.active{background:${meta.theme === "dark" ? "#1e3a5f" : "#eff6ff"};border-color:#3b82f6;color:#2563eb}
|
|
2863
|
-
#connect-btn{
|
|
2864
|
-
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2865
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2866
|
-
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2867
|
-
}
|
|
2868
|
-
#connect-btn.active{background:#fef3c7;border-color:#f59e0b}
|
|
2869
|
-
/* Tooltip */
|
|
2870
|
-
#tooltip{
|
|
2871
|
-
position:fixed;background:#1e293b;color:#fff;border-radius:8px;
|
|
2872
|
-
padding:8px 12px;font-size:12px;pointer-events:none;z-index:100;
|
|
2873
|
-
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.15);
|
|
2874
|
-
}
|
|
2875
|
-
#tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2876
|
-
#tooltip .tt-domain{color:#94a3b8;font-size:11px}
|
|
2877
|
-
#tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2878
|
-
/* Empty state */
|
|
2879
|
-
#empty-state{
|
|
2880
|
-
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
2881
|
-
align-items:center;justify-content:center;gap:12px;color:#94a3b8;
|
|
2882
|
-
}
|
|
2883
|
-
#empty-state p{font-size:14px}
|
|
2884
|
-
/* Theme toggle */
|
|
2885
|
-
#theme-btn{
|
|
2886
|
-
width:40px;height:40px;border-radius:10px;border:1px solid ${meta.theme === "dark" ? "#334155" : "#e2e8f0"};
|
|
2887
|
-
background:${meta.theme === "dark" ? "#1e293b" : "#fff"};cursor:pointer;
|
|
2888
|
-
font-size:18px;display:flex;align-items:center;justify-content:center;color:inherit;
|
|
2889
|
-
}
|
|
2890
|
-
/* Dark mode overrides (toggled via JS) */
|
|
2891
|
-
body.dark{background:#0f172a;color:#e2e8f0}
|
|
2892
|
-
body.dark #topbar{background:#1e293b;border-color:#334155}
|
|
2893
|
-
body.dark #search-box{background:#334155}
|
|
2894
|
-
body.dark #detail-panel{background:#1e293b;border-color:#334155}
|
|
2895
|
-
body.dark .tb-btn,body.dark .zoom-btn,body.dark .detail-btn,body.dark #connect-btn,body.dark #theme-btn{
|
|
2896
|
-
background:#1e293b;border-color:#334155;color:#e2e8f0;
|
|
2897
|
-
}
|
|
2898
|
-
/* Light mode overrides */
|
|
2899
|
-
body.light{background:#f8fafc;color:#1e293b}
|
|
2900
|
-
body.light #topbar{background:#fff;border-color:#e2e8f0}
|
|
2901
|
-
/* Connection hint */
|
|
2902
|
-
#connect-hint{
|
|
2903
|
-
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2904
|
-
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2905
|
-
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2906
|
-
display:none;z-index:20;pointer-events:none;
|
|
2907
|
-
}
|
|
2908
|
-
/* Screen reader only */
|
|
2909
|
-
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
|
|
2910
|
-
</style>
|
|
2911
|
-
</head>
|
|
2912
|
-
<body class="${meta.theme}">
|
|
2913
|
-
<!-- Top bar -->
|
|
2914
|
-
<div id="topbar">
|
|
2915
|
-
<h1>Data Cartography Map</h1>
|
|
2916
|
-
<div id="search-box">
|
|
2917
|
-
<span style="color:#94a3b8;font-size:14px">⌕</span>
|
|
2918
|
-
<input id="search-input" type="text" placeholder="Search assets..." aria-label="Search data assets"/>
|
|
2919
|
-
</div>
|
|
2920
|
-
<button id="theme-btn" title="Toggle dark/light mode" aria-label="Toggle theme">${meta.theme === "dark" ? "☼" : "☾"}</button>
|
|
2921
|
-
</div>
|
|
2922
|
-
<!-- SR summary -->
|
|
2923
|
-
<div class="sr-only" role="status" aria-live="polite" id="sr-summary">
|
|
2924
|
-
Data cartography map with ${assets.length} assets in ${clusters.length} clusters.
|
|
2925
|
-
</div>
|
|
2926
|
-
<!-- Main area -->
|
|
2927
|
-
<div id="main">
|
|
2928
|
-
<div id="canvas-wrap" role="application" aria-label="Data cartography hex map" tabindex="0">
|
|
2929
|
-
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
2930
|
-
${isEmpty ? '<div id="empty-state"><p style="font-size:48px">🗺</p><p>No data assets available</p><p style="font-size:12px">Run <code>datasynx-cartography discover</code> to populate the map</p></div>' : ""}
|
|
2931
|
-
</div>
|
|
2932
|
-
<div id="detail-panel" role="complementary" aria-label="Asset details">
|
|
2933
|
-
<div class="panel-header">
|
|
2934
|
-
<h3 id="dp-name">—</h3>
|
|
2935
|
-
<button class="close-btn" id="dp-close" aria-label="Close panel">✕</button>
|
|
2936
|
-
</div>
|
|
2937
|
-
<div class="panel-body" id="dp-body"></div>
|
|
2938
|
-
</div>
|
|
2939
|
-
</div>
|
|
2940
|
-
<!-- Bottom-left toolbar -->
|
|
2941
|
-
<div id="toolbar-left">
|
|
2942
|
-
<button class="tb-btn active" id="btn-labels" title="Show labels" aria-pressed="true" aria-label="Toggle labels">🏷</button>
|
|
2943
|
-
<button class="tb-btn" id="btn-quality" title="Quality layer" aria-pressed="false" aria-label="Toggle quality layer">👁</button>
|
|
2944
|
-
</div>
|
|
2945
|
-
<!-- Bottom-right toolbar -->
|
|
2946
|
-
<div id="toolbar-right">
|
|
2947
|
-
<div id="zoom-controls">
|
|
2948
|
-
<button class="zoom-btn" id="zoom-out" aria-label="Zoom out">−</button>
|
|
2949
|
-
<span id="zoom-pct">100%</span>
|
|
2950
|
-
<button class="zoom-btn" id="zoom-in" aria-label="Zoom in">+</button>
|
|
2951
|
-
</div>
|
|
2952
|
-
<div id="detail-selector">
|
|
2953
|
-
<button class="detail-btn" id="dl-1" aria-label="Detail level 1">1</button>
|
|
2954
|
-
<button class="detail-btn active" id="dl-2" aria-label="Detail level 2">2</button>
|
|
2955
|
-
<button class="detail-btn" id="dl-3" aria-label="Detail level 3">3</button>
|
|
2956
|
-
<button class="detail-btn" id="dl-4" aria-label="Detail level 4">4</button>
|
|
2957
|
-
</div>
|
|
2958
|
-
<button id="connect-btn" title="Connection tool" aria-label="Toggle connection tool">🔗</button>
|
|
2959
|
-
</div>
|
|
2960
|
-
<!-- Connection hint -->
|
|
2961
|
-
<div id="connect-hint">Click two assets to create a connection</div>
|
|
2962
|
-
<!-- Tooltip -->
|
|
2963
|
-
<div id="tooltip" role="tooltip">
|
|
2964
|
-
<div class="tt-name" id="tt-name"></div>
|
|
2965
|
-
<div class="tt-domain" id="tt-domain"></div>
|
|
2966
|
-
<div class="tt-quality" id="tt-quality"></div>
|
|
2967
|
-
</div>
|
|
2968
|
-
|
|
2969
|
-
<script>
|
|
2970
|
-
(function() {
|
|
2971
|
-
'use strict';
|
|
2972
|
-
|
|
2973
|
-
// \u2500\u2500 Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2974
|
-
const MAP = ${dataJson};
|
|
2975
|
-
const HEX_SIZE = ${HEX_SIZE2};
|
|
2976
|
-
const IS_EMPTY = ${isEmpty};
|
|
2977
|
-
|
|
2978
|
-
// Build asset index
|
|
2979
|
-
const assetIndex = new Map();
|
|
2980
|
-
const clusterByAsset = new Map();
|
|
2981
|
-
for (const c of MAP.clusters) {
|
|
2982
|
-
for (const aid of c.assetIds) {
|
|
2983
|
-
clusterByAsset.set(aid, c);
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
for (const a of MAP.assets) {
|
|
2987
|
-
assetIndex.set(a.id, a);
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
// \u2500\u2500 Canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2991
|
-
const canvas = document.getElementById('hexmap');
|
|
2992
|
-
const ctx = canvas.getContext('2d');
|
|
2993
|
-
const wrap = document.getElementById('canvas-wrap');
|
|
2994
|
-
let W = 0, H = 0;
|
|
2995
|
-
|
|
2996
|
-
function resize() {
|
|
2997
|
-
const dpr = window.devicePixelRatio || 1;
|
|
2998
|
-
W = wrap.clientWidth; H = wrap.clientHeight;
|
|
2999
|
-
canvas.width = W * dpr; canvas.height = H * dpr;
|
|
3000
|
-
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
|
3001
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3002
|
-
draw();
|
|
3003
|
-
}
|
|
3004
|
-
window.addEventListener('resize', resize);
|
|
3005
|
-
|
|
3006
|
-
// \u2500\u2500 Viewport \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3007
|
-
let vx = 0, vy = 0, scale = 1;
|
|
3008
|
-
let detailLevel = 2, showLabels = true, showQuality = false;
|
|
3009
|
-
let isDark = document.body.classList.contains('dark');
|
|
3010
|
-
let connectMode = false, connectFirst = null;
|
|
3011
|
-
let hoveredAssetId = null, selectedAssetId = null;
|
|
3012
|
-
let searchQuery = '';
|
|
3013
|
-
let localConnections = [...MAP.connections];
|
|
3014
|
-
|
|
3015
|
-
// Flat-top hex math
|
|
3016
|
-
function htp_x(q, r) { return HEX_SIZE * (3/2 * q); }
|
|
3017
|
-
function htp_y(q, r) { return HEX_SIZE * (Math.sqrt(3)/2 * q + Math.sqrt(3) * r); }
|
|
3018
|
-
function w2s(wx, wy) { return { x: wx*scale+vx, y: wy*scale+vy }; }
|
|
3019
|
-
function s2w(sx, sy) { return { x: (sx-vx)/scale, y: (sy-vy)/scale }; }
|
|
3020
|
-
|
|
3021
|
-
function fitToView() {
|
|
3022
|
-
if (IS_EMPTY || MAP.assets.length === 0) { vx = 0; vy = 0; scale = 1; return; }
|
|
3023
|
-
let mnx=Infinity,mny=Infinity,mxx=-Infinity,mxy=-Infinity;
|
|
3024
|
-
for (const a of MAP.assets) {
|
|
3025
|
-
const px=htp_x(a.q,a.r), py=htp_y(a.q,a.r);
|
|
3026
|
-
if(px<mnx)mnx=px;if(py<mny)mny=py;if(px>mxx)mxx=px;if(py>mxy)mxy=py;
|
|
3027
|
-
}
|
|
3028
|
-
const pw=mxx-mnx+HEX_SIZE*4, ph=mxy-mny+HEX_SIZE*4;
|
|
3029
|
-
scale = Math.min(W/pw, H/ph, 2) * 0.85;
|
|
3030
|
-
vx = W/2 - ((mnx+mxx)/2)*scale;
|
|
3031
|
-
vy = H/2 - ((mny+mxy)/2)*scale;
|
|
3032
|
-
}
|
|
3033
|
-
|
|
3034
|
-
// \u2500\u2500 Drawing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3035
|
-
function hexPath(cx, cy, r) {
|
|
3036
|
-
ctx.beginPath();
|
|
3037
|
-
for (let i=0;i<6;i++) {
|
|
3038
|
-
const angle = Math.PI/180*(60*i);
|
|
3039
|
-
const x=cx+r*Math.cos(angle), y=cy+r*Math.sin(angle);
|
|
3040
|
-
i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
|
|
3041
|
-
}
|
|
3042
|
-
ctx.closePath();
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
function shadeV(hex, amt) {
|
|
3046
|
-
if(!hex||hex.length<7)return hex;
|
|
3047
|
-
const n=parseInt(hex.replace('#',''),16);
|
|
3048
|
-
const r=Math.min(255,(n>>16)+amt), g=Math.min(255,((n>>8)&0xff)+amt), b=Math.min(255,(n&0xff)+amt);
|
|
3049
|
-
return '#'+r.toString(16).padStart(2,'0')+g.toString(16).padStart(2,'0')+b.toString(16).padStart(2,'0');
|
|
3050
|
-
}
|
|
3051
|
-
|
|
3052
|
-
function draw() {
|
|
3053
|
-
ctx.clearRect(0,0,W,H);
|
|
3054
|
-
ctx.fillStyle = isDark ? '#0f172a' : '#f8fafc';
|
|
3055
|
-
ctx.fillRect(0,0,W,H);
|
|
3056
|
-
if (IS_EMPTY) return;
|
|
3057
|
-
|
|
3058
|
-
const size = HEX_SIZE * scale;
|
|
3059
|
-
const matchedIds = getSearchMatches();
|
|
3060
|
-
const hasSearch = searchQuery.length > 0;
|
|
3061
|
-
|
|
3062
|
-
// Draw connections
|
|
3063
|
-
ctx.save();
|
|
3064
|
-
ctx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
3065
|
-
ctx.lineWidth = 1.5;
|
|
3066
|
-
ctx.setLineDash([4,4]);
|
|
3067
|
-
for (const conn of localConnections) {
|
|
3068
|
-
const src = assetIndex.get(conn.sourceAssetId);
|
|
3069
|
-
const tgt = assetIndex.get(conn.targetAssetId);
|
|
3070
|
-
if (!src||!tgt) continue;
|
|
3071
|
-
const sp=w2s(htp_x(src.q,src.r),htp_y(src.q,src.r));
|
|
3072
|
-
const tp=w2s(htp_x(tgt.q,tgt.r),htp_y(tgt.q,tgt.r));
|
|
3073
|
-
ctx.beginPath();ctx.moveTo(sp.x,sp.y);ctx.lineTo(tp.x,tp.y);ctx.stroke();
|
|
3074
|
-
}
|
|
3075
|
-
ctx.setLineDash([]);
|
|
3076
|
-
ctx.restore();
|
|
3077
|
-
|
|
3078
|
-
// Draw hexagons per cluster
|
|
3079
|
-
for (const cluster of MAP.clusters) {
|
|
3080
|
-
const baseColor = cluster.color;
|
|
3081
|
-
const clusterAssets = cluster.assetIds.map(id=>assetIndex.get(id)).filter(Boolean);
|
|
3082
|
-
const isClusterMatch = !hasSearch || clusterAssets.some(a => matchedIds.has(a.id));
|
|
3083
|
-
const clusterDim = hasSearch && !isClusterMatch;
|
|
3084
|
-
|
|
3085
|
-
for (let ai=0; ai<clusterAssets.length; ai++) {
|
|
3086
|
-
const asset = clusterAssets[ai];
|
|
3087
|
-
const wx=htp_x(asset.q,asset.r), wy=htp_y(asset.q,asset.r);
|
|
3088
|
-
const s=w2s(wx,wy);
|
|
3089
|
-
const cx=s.x, cy=s.y;
|
|
3090
|
-
|
|
3091
|
-
// Frustum cull
|
|
3092
|
-
if(cx+size<0||cx-size>W||cy+size<0||cy-size>H) continue;
|
|
3093
|
-
|
|
3094
|
-
// Shade variation
|
|
3095
|
-
const shade = ai%3===0?18:ai%3===1?8:0;
|
|
3096
|
-
let fillColor = shadeV(baseColor, shade);
|
|
3097
|
-
|
|
3098
|
-
// Quality overlay
|
|
3099
|
-
if (showQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
3100
|
-
const q = asset.qualityScore;
|
|
3101
|
-
if (q < 40) fillColor = '#ef4444';
|
|
3102
|
-
else if (q < 70) fillColor = '#f97316';
|
|
3103
|
-
}
|
|
3104
|
-
|
|
3105
|
-
const alpha = clusterDim ? 0.18 : 1;
|
|
3106
|
-
const isHovered = asset.id === hoveredAssetId;
|
|
3107
|
-
const isSelected = asset.id === selectedAssetId;
|
|
3108
|
-
const isConnectFirst = asset.id === connectFirst;
|
|
3109
|
-
|
|
3110
|
-
ctx.save();
|
|
3111
|
-
ctx.globalAlpha = alpha;
|
|
3112
|
-
hexPath(cx, cy, size*0.92);
|
|
3113
|
-
|
|
3114
|
-
if (isDark && (isHovered||isSelected||isConnectFirst)) {
|
|
3115
|
-
ctx.shadowColor = fillColor;
|
|
3116
|
-
ctx.shadowBlur = isSelected ? 16 : 8;
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
ctx.fillStyle = fillColor;
|
|
3120
|
-
ctx.fill();
|
|
3121
|
-
|
|
3122
|
-
if (isSelected||isConnectFirst) {
|
|
3123
|
-
ctx.strokeStyle = isConnectFirst ? '#f59e0b' : '#fff';
|
|
3124
|
-
ctx.lineWidth = 2.5;
|
|
3125
|
-
ctx.stroke();
|
|
3126
|
-
} else if (isHovered) {
|
|
3127
|
-
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
|
|
3128
|
-
ctx.lineWidth = 1.5;
|
|
3129
|
-
ctx.stroke();
|
|
3130
|
-
} else {
|
|
3131
|
-
ctx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)';
|
|
3132
|
-
ctx.lineWidth = 1;
|
|
3133
|
-
ctx.stroke();
|
|
3134
|
-
}
|
|
3135
|
-
ctx.restore();
|
|
3136
|
-
|
|
3137
|
-
// Quality dot
|
|
3138
|
-
if (showQuality && asset.qualityScore!==null && asset.qualityScore!==undefined && size>8) {
|
|
3139
|
-
const q = asset.qualityScore;
|
|
3140
|
-
if (q < 70) {
|
|
3141
|
-
ctx.beginPath();
|
|
3142
|
-
ctx.arc(cx+size*0.4, cy-size*0.4, Math.max(3,size*0.14), 0, Math.PI*2);
|
|
3143
|
-
ctx.fillStyle = q<40?'#ef4444':'#f97316';
|
|
3144
|
-
ctx.fill();
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
// Asset labels (detail 4, or 3 at high zoom)
|
|
3149
|
-
const showAssetLabel = showLabels && !clusterDim &&
|
|
3150
|
-
((detailLevel>=4)||(detailLevel===3 && scale>=0.8));
|
|
3151
|
-
if (showAssetLabel && size>14) {
|
|
3152
|
-
const label = asset.name.length>12 ? asset.name.substring(0,11)+'...' : asset.name;
|
|
3153
|
-
ctx.save();
|
|
3154
|
-
ctx.font = Math.max(8,Math.min(11,size*0.38))+'px -apple-system,sans-serif';
|
|
3155
|
-
ctx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
3156
|
-
ctx.textAlign='center';ctx.textBaseline='middle';
|
|
3157
|
-
ctx.fillText(label, cx, cy);
|
|
3158
|
-
ctx.restore();
|
|
3159
|
-
}
|
|
3160
|
-
}
|
|
3161
|
-
}
|
|
3162
|
-
|
|
3163
|
-
// Cluster labels (pill badges)
|
|
3164
|
-
if (showLabels && detailLevel>=1) {
|
|
3165
|
-
for (const cluster of MAP.clusters) {
|
|
3166
|
-
if (cluster.assetIds.length===0) continue;
|
|
3167
|
-
if (hasSearch && !cluster.assetIds.some(id=>matchedIds.has(id))) continue;
|
|
3168
|
-
const s=w2s(cluster.centroid.x, cluster.centroid.y);
|
|
3169
|
-
drawPill(s.x, s.y-size*1.2, cluster.label, cluster.color, 14);
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
// Sub-domain labels (detail 2+)
|
|
3174
|
-
if (showLabels && detailLevel>=2) {
|
|
3175
|
-
const subGroups = new Map();
|
|
3176
|
-
for (const a of MAP.assets) {
|
|
3177
|
-
if (!a.subDomain) continue;
|
|
3178
|
-
const key = a.domain+'|'+a.subDomain;
|
|
3179
|
-
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
3180
|
-
subGroups.get(key).push(a);
|
|
3181
|
-
}
|
|
3182
|
-
for (const [, group] of subGroups) {
|
|
3183
|
-
let sx=0,sy=0;
|
|
3184
|
-
for (const a of group) { sx+=htp_x(a.q,a.r); sy+=htp_y(a.q,a.r); }
|
|
3185
|
-
const cx=sx/group.length, cy=sy/group.length;
|
|
3186
|
-
const s = w2s(cx, cy);
|
|
3187
|
-
drawPill(s.x, s.y+size*1.5, group[0].subDomain, '#64748b', 11);
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
|
|
3192
|
-
function drawPill(x, y, text, color, fontSize) {
|
|
3193
|
-
if(!text) return;
|
|
3194
|
-
ctx.save();
|
|
3195
|
-
ctx.font = '600 '+fontSize+'px -apple-system,sans-serif';
|
|
3196
|
-
const tw=ctx.measureText(text).width;
|
|
3197
|
-
const ph=fontSize+8, pw=tw+20;
|
|
3198
|
-
ctx.beginPath();
|
|
3199
|
-
if (ctx.roundRect) ctx.roundRect(x-pw/2, y-ph/2, pw, ph, ph/2);
|
|
3200
|
-
else { ctx.rect(x-pw/2, y-ph/2, pw, ph); }
|
|
3201
|
-
ctx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
3202
|
-
ctx.shadowColor='rgba(0,0,0,0.15)'; ctx.shadowBlur=6;
|
|
3203
|
-
ctx.fill(); ctx.shadowBlur=0;
|
|
3204
|
-
ctx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
3205
|
-
ctx.textAlign='center'; ctx.textBaseline='middle';
|
|
3206
|
-
ctx.fillText(text, x, y);
|
|
3207
|
-
ctx.restore();
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
|
-
// \u2500\u2500 Hit testing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3211
|
-
function getAssetAt(sx, sy) {
|
|
3212
|
-
const w=s2w(sx,sy);
|
|
3213
|
-
for (const a of MAP.assets) {
|
|
3214
|
-
const wx=htp_x(a.q,a.r), wy=htp_y(a.q,a.r);
|
|
3215
|
-
const dx=Math.abs(w.x-wx), dy=Math.abs(w.y-wy);
|
|
3216
|
-
if (dx>HEX_SIZE||dy>HEX_SIZE) continue;
|
|
3217
|
-
if (dx*dx+dy*dy < HEX_SIZE*HEX_SIZE) return a;
|
|
3218
|
-
}
|
|
3219
|
-
return null;
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
// \u2500\u2500 Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3223
|
-
function getSearchMatches() {
|
|
3224
|
-
if(!searchQuery) return new Set();
|
|
3225
|
-
const q=searchQuery.toLowerCase();
|
|
3226
|
-
const m=new Set();
|
|
3227
|
-
for(const a of MAP.assets){
|
|
3228
|
-
if(a.name.toLowerCase().includes(q)||(a.domain&&a.domain.toLowerCase().includes(q))||
|
|
3229
|
-
(a.subDomain&&a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
3230
|
-
}
|
|
3231
|
-
return m;
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
// \u2500\u2500 Pan & Zoom \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3235
|
-
let dragging=false, lastMX=0, lastMY=0;
|
|
3236
|
-
|
|
3237
|
-
wrap.addEventListener('mousedown', e=>{
|
|
3238
|
-
if(e.button!==0)return;
|
|
3239
|
-
dragging=true; lastMX=e.clientX; lastMY=e.clientY;
|
|
3240
|
-
wrap.classList.add('dragging');
|
|
3241
|
-
});
|
|
3242
|
-
window.addEventListener('mouseup', ()=>{dragging=false;wrap.classList.remove('dragging');});
|
|
3243
|
-
window.addEventListener('mousemove', e=>{
|
|
3244
|
-
if(dragging){
|
|
3245
|
-
vx+=e.clientX-lastMX; vy+=e.clientY-lastMY;
|
|
3246
|
-
lastMX=e.clientX; lastMY=e.clientY; draw(); return;
|
|
3247
|
-
}
|
|
3248
|
-
const rect=wrap.getBoundingClientRect();
|
|
3249
|
-
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
3250
|
-
const asset=getAssetAt(sx,sy);
|
|
3251
|
-
const newId=asset?asset.id:null;
|
|
3252
|
-
if(newId!==hoveredAssetId){hoveredAssetId=newId;draw();}
|
|
3253
|
-
const tt=document.getElementById('tooltip');
|
|
3254
|
-
if(asset){
|
|
3255
|
-
document.getElementById('tt-name').textContent=asset.name;
|
|
3256
|
-
document.getElementById('tt-domain').textContent=asset.domain+(asset.subDomain?' > '+asset.subDomain:'');
|
|
3257
|
-
document.getElementById('tt-quality').textContent=asset.qualityScore!==null?'Quality: '+asset.qualityScore+'/100':'';
|
|
3258
|
-
tt.style.display='block';tt.style.left=(e.clientX+12)+'px';tt.style.top=(e.clientY-8)+'px';
|
|
3259
|
-
} else { tt.style.display='none'; }
|
|
3260
|
-
});
|
|
3261
|
-
|
|
3262
|
-
wrap.addEventListener('click', e=>{
|
|
3263
|
-
const rect=wrap.getBoundingClientRect();
|
|
3264
|
-
const sx=e.clientX-rect.left, sy=e.clientY-rect.top;
|
|
3265
|
-
const asset=getAssetAt(sx,sy);
|
|
3266
|
-
if(connectMode){
|
|
3267
|
-
if(!asset) return;
|
|
3268
|
-
if(!connectFirst){connectFirst=asset.id;draw();}
|
|
3269
|
-
else if(connectFirst!==asset.id){
|
|
3270
|
-
localConnections.push({id:crypto.randomUUID(),sourceAssetId:connectFirst,targetAssetId:asset.id,type:'connection'});
|
|
3271
|
-
connectFirst=null;draw();
|
|
3272
|
-
}
|
|
3273
|
-
return;
|
|
3274
|
-
}
|
|
3275
|
-
if(asset){selectedAssetId=asset.id;showDetailPanel(asset);}
|
|
3276
|
-
else{selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');}
|
|
3277
|
-
draw();
|
|
3278
|
-
});
|
|
3279
|
-
|
|
3280
|
-
// Touch
|
|
3281
|
-
let lastTouches=[];
|
|
3282
|
-
wrap.addEventListener('touchstart',e=>{lastTouches=[...e.touches];},{passive:true});
|
|
3283
|
-
wrap.addEventListener('touchmove',e=>{
|
|
3284
|
-
if(e.touches.length===1){
|
|
3285
|
-
vx+=e.touches[0].clientX-lastTouches[0].clientX;
|
|
3286
|
-
vy+=e.touches[0].clientY-lastTouches[0].clientY;draw();
|
|
3287
|
-
} else if(e.touches.length===2){
|
|
3288
|
-
const d0=Math.hypot(lastTouches[0].clientX-lastTouches[1].clientX,lastTouches[0].clientY-lastTouches[1].clientY);
|
|
3289
|
-
const d1=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);
|
|
3290
|
-
const mx=(e.touches[0].clientX+e.touches[1].clientX)/2;
|
|
3291
|
-
const my=(e.touches[0].clientY+e.touches[1].clientY)/2;
|
|
3292
|
-
applyZoom(d1/d0,mx,my);
|
|
3293
|
-
}
|
|
3294
|
-
lastTouches=[...e.touches];
|
|
3295
|
-
},{passive:true});
|
|
3296
|
-
|
|
3297
|
-
wrap.addEventListener('wheel',e=>{
|
|
3298
|
-
e.preventDefault();
|
|
3299
|
-
const rect=wrap.getBoundingClientRect();
|
|
3300
|
-
applyZoom(e.deltaY<0?1.12:1/1.12,e.clientX-rect.left,e.clientY-rect.top);
|
|
3301
|
-
},{passive:false});
|
|
3302
|
-
|
|
3303
|
-
function applyZoom(factor,sx,sy){
|
|
3304
|
-
const ns=Math.max(0.05,Math.min(8,scale*factor));
|
|
3305
|
-
const wx=(sx-vx)/scale,wy=(sy-vy)/scale;
|
|
3306
|
-
scale=ns;vx=sx-wx*scale;vy=sy-wy*scale;
|
|
3307
|
-
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';draw();
|
|
3308
|
-
}
|
|
3309
|
-
document.getElementById('zoom-in').addEventListener('click',()=>applyZoom(1.25,W/2,H/2));
|
|
3310
|
-
document.getElementById('zoom-out').addEventListener('click',()=>applyZoom(1/1.25,W/2,H/2));
|
|
3311
|
-
|
|
3312
|
-
// Keyboard
|
|
3313
|
-
wrap.addEventListener('keydown',e=>{
|
|
3314
|
-
const step=40;
|
|
3315
|
-
if(e.key==='ArrowLeft'){vx+=step;draw();}
|
|
3316
|
-
else if(e.key==='ArrowRight'){vx-=step;draw();}
|
|
3317
|
-
else if(e.key==='ArrowUp'){vy+=step;draw();}
|
|
3318
|
-
else if(e.key==='ArrowDown'){vy-=step;draw();}
|
|
3319
|
-
else if(e.key==='+'||e.key==='=')applyZoom(1.2,W/2,H/2);
|
|
3320
|
-
else if(e.key==='-')applyZoom(1/1.2,W/2,H/2);
|
|
3321
|
-
else if(e.key==='Escape'){
|
|
3322
|
-
selectedAssetId=null;document.getElementById('detail-panel').classList.remove('open');
|
|
3323
|
-
if(connectMode)toggleConnect();draw();
|
|
3324
|
-
}
|
|
3325
|
-
});
|
|
3326
|
-
|
|
3327
|
-
// \u2500\u2500 Detail Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3328
|
-
function showDetailPanel(asset) {
|
|
3329
|
-
document.getElementById('dp-name').textContent=asset.name;
|
|
3330
|
-
const body=document.getElementById('dp-body');
|
|
3331
|
-
const rows=[['Domain',asset.domain],['Sub-domain',asset.subDomain],
|
|
3332
|
-
['Quality Score',asset.qualityScore!==null?renderQuality(asset.qualityScore):null],
|
|
3333
|
-
...Object.entries(asset.metadata||{}).slice(0,8).map(([k,v])=>[k,String(v)])
|
|
3334
|
-
].filter(([,v])=>v!==null&&v!==undefined&&v!=='');
|
|
3335
|
-
body.innerHTML=rows.map(([l,v])=>'<div class="meta-row"><div class="meta-label">'+esc(String(l))+'</div><div class="meta-value">'+v+'</div></div>').join('');
|
|
3336
|
-
const related=localConnections.filter(c=>c.sourceAssetId===asset.id||c.targetAssetId===asset.id);
|
|
3337
|
-
if(related.length>0){
|
|
3338
|
-
body.innerHTML+='<div class="meta-row"><div class="meta-label">Connections ('+related.length+')</div><div>'+
|
|
3339
|
-
related.map(c=>{const oid=c.sourceAssetId===asset.id?c.targetAssetId:c.sourceAssetId;
|
|
3340
|
-
const o=assetIndex.get(oid);return '<div class="meta-value" style="margin-top:4px;font-size:12px">'+(o?esc(o.name):oid)+'</div>';}).join('')+'</div></div>';
|
|
3341
|
-
}
|
|
3342
|
-
document.getElementById('detail-panel').classList.add('open');
|
|
3343
|
-
}
|
|
3344
|
-
function renderQuality(s){
|
|
3345
|
-
const c=s>=70?'#22c55e':s>=40?'#f97316':'#ef4444';
|
|
3346
|
-
return s+'/100 <div class="quality-bar"><div class="quality-fill" style="width:'+s+'%;background:'+c+'"></div></div>';
|
|
3347
|
-
}
|
|
3348
|
-
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
3349
|
-
document.getElementById('dp-close').addEventListener('click',()=>{
|
|
3350
|
-
document.getElementById('detail-panel').classList.remove('open');selectedAssetId=null;draw();
|
|
3351
|
-
});
|
|
3352
|
-
|
|
3353
|
-
// \u2500\u2500 Toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3354
|
-
[1,2,3,4].forEach(n=>{
|
|
3355
|
-
document.getElementById('dl-'+n).addEventListener('click',()=>{
|
|
3356
|
-
detailLevel=n;document.querySelectorAll('.detail-btn').forEach(b=>b.classList.remove('active'));
|
|
3357
|
-
document.getElementById('dl-'+n).classList.add('active');draw();
|
|
3358
|
-
});
|
|
3359
|
-
});
|
|
3360
|
-
document.getElementById('btn-labels').addEventListener('click',()=>{
|
|
3361
|
-
showLabels=!showLabels;document.getElementById('btn-labels').classList.toggle('active',showLabels);draw();
|
|
3362
|
-
});
|
|
3363
|
-
document.getElementById('btn-quality').addEventListener('click',()=>{
|
|
3364
|
-
showQuality=!showQuality;document.getElementById('btn-quality').classList.toggle('active',showQuality);draw();
|
|
3365
|
-
});
|
|
3366
|
-
function toggleConnect(){
|
|
3367
|
-
connectMode=!connectMode;connectFirst=null;
|
|
3368
|
-
document.getElementById('connect-btn').classList.toggle('active',connectMode);
|
|
3369
|
-
wrap.classList.toggle('connecting',connectMode);
|
|
3370
|
-
document.getElementById('connect-hint').style.display=connectMode?'block':'none';draw();
|
|
3371
|
-
}
|
|
3372
|
-
document.getElementById('connect-btn').addEventListener('click',toggleConnect);
|
|
3373
|
-
document.getElementById('theme-btn').addEventListener('click',()=>{
|
|
3374
|
-
isDark=!isDark;
|
|
3375
|
-
document.body.classList.toggle('dark',isDark);document.body.classList.toggle('light',!isDark);
|
|
3376
|
-
document.getElementById('theme-btn').innerHTML=isDark?'☼':'☾';draw();
|
|
3377
|
-
});
|
|
3378
|
-
document.getElementById('search-input').addEventListener('input',e=>{searchQuery=e.target.value.trim();draw();});
|
|
3379
|
-
|
|
3380
|
-
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3381
|
-
resize(); fitToView();
|
|
3382
|
-
document.getElementById('zoom-pct').textContent=Math.round(scale*100)+'%';
|
|
3383
|
-
draw();
|
|
3384
|
-
})();
|
|
3385
|
-
</script>
|
|
3386
|
-
</body>
|
|
3387
|
-
</html>`;
|
|
3388
|
-
}
|
|
3389
2230
|
function exportDiscoveryApp(nodes, edges, options) {
|
|
3390
2231
|
const theme = options?.theme ?? "dark";
|
|
3391
2232
|
const graphData = JSON.stringify({
|
|
@@ -3546,8 +2387,9 @@ body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--te
|
|
|
3546
2387
|
width:40px;height:40px;border-radius:10px;border:1px solid var(--border);
|
|
3547
2388
|
background:var(--bg-surface);box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
3548
2389
|
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
3549
|
-
transition:all .15s;color:var(--text);
|
|
2390
|
+
transition:all .15s;color:var(--text);font-family:-apple-system,sans-serif;
|
|
3550
2391
|
}
|
|
2392
|
+
#btn-all-labels{font-size:14px;font-weight:700;letter-spacing:-.02em}
|
|
3551
2393
|
.tb-tool:hover{border-color:var(--text-muted)}
|
|
3552
2394
|
.tb-tool.active{background:var(--accent-dim);border-color:var(--accent)}
|
|
3553
2395
|
.map-zoom{display:flex;align-items:center;gap:6px}
|
|
@@ -3719,6 +2561,7 @@ body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--te
|
|
|
3719
2561
|
</div>
|
|
3720
2562
|
<div id="map-tb-left">
|
|
3721
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>
|
|
3722
2565
|
<button class="tb-tool" id="btn-quality" title="Quality layer">👁</button>
|
|
3723
2566
|
<button class="tb-tool" id="btn-connect" title="Connection tool">🔗</button>
|
|
3724
2567
|
</div>
|
|
@@ -3815,7 +2658,7 @@ var mapCtx = mapCanvas.getContext('2d');
|
|
|
3815
2658
|
var mapWrap = document.getElementById('map-wrap');
|
|
3816
2659
|
var mW = 0, mH = 0;
|
|
3817
2660
|
var mvx = 0, mvy = 0, mScale = 1;
|
|
3818
|
-
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false;
|
|
2661
|
+
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false, mShowAllLabels = false;
|
|
3819
2662
|
var mConnectMode = false, mConnectFirst = null;
|
|
3820
2663
|
var mHoveredId = null, mSelectedId = null;
|
|
3821
2664
|
var mSearchQuery = '';
|
|
@@ -3960,8 +2803,8 @@ function drawMap() {
|
|
|
3960
2803
|
mapCtx.fillStyle = asset.qualityScore < 40 ? '#ef4444' : '#f97316'; mapCtx.fill();
|
|
3961
2804
|
}
|
|
3962
2805
|
|
|
3963
|
-
var showAssetLabel = mShowLabels && !clusterDim && ((mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
3964
|
-
if (showAssetLabel && size >
|
|
2806
|
+
var showAssetLabel = mShowLabels && !clusterDim && (mShowAllLabels || (mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
2807
|
+
if (showAssetLabel && size > 6) {
|
|
3965
2808
|
var label = asset.name.length > 12 ? asset.name.substring(0, 11) + '...' : asset.name;
|
|
3966
2809
|
mapCtx.save();
|
|
3967
2810
|
mapCtx.font = Math.max(8, Math.min(11, size * 0.38)) + 'px -apple-system,sans-serif';
|
|
@@ -4113,6 +2956,11 @@ document.getElementById('md-close').addEventListener('click', function() {
|
|
|
4113
2956
|
document.getElementById('btn-labels').addEventListener('click', function() {
|
|
4114
2957
|
mShowLabels = !mShowLabels; this.classList.toggle('active', mShowLabels); drawMap();
|
|
4115
2958
|
});
|
|
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();
|
|
2963
|
+
});
|
|
4116
2964
|
document.getElementById('btn-quality').addEventListener('click', function() {
|
|
4117
2965
|
mShowQuality = !mShowQuality; this.classList.toggle('active', mShowQuality); drawMap();
|
|
4118
2966
|
});
|
|
@@ -4467,12 +3315,44 @@ buildTopoList();
|
|
|
4467
3315
|
</body>
|
|
4468
3316
|
</html>`;
|
|
4469
3317
|
}
|
|
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
|
+
}
|
|
4470
3347
|
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "discovery", "sops"]) {
|
|
4471
3348
|
mkdirSync2(outputDir, { recursive: true });
|
|
4472
3349
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
4473
3350
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
4474
3351
|
const nodes = db.getNodes(sessionId);
|
|
4475
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");
|
|
4476
3356
|
if (formats.includes("mermaid")) {
|
|
4477
3357
|
writeFileSync(join2(outputDir, "topology.mermaid"), generateTopologyMermaid(nodes, edges));
|
|
4478
3358
|
writeFileSync(join2(outputDir, "dependencies.mermaid"), generateDependencyMermaid(nodes, edges));
|
|
@@ -4486,15 +3366,7 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
4486
3366
|
writeFileSync(join2(outputDir, "catalog-info.yaml"), exportBackstageYAML(nodes, edges));
|
|
4487
3367
|
process.stderr.write("\u2713 catalog-info.yaml\n");
|
|
4488
3368
|
}
|
|
4489
|
-
if (formats.includes("html")) {
|
|
4490
|
-
writeFileSync(join2(outputDir, "topology.html"), exportHTML(nodes, edges));
|
|
4491
|
-
process.stderr.write("\u2713 topology.html\n");
|
|
4492
|
-
}
|
|
4493
|
-
if (formats.includes("map")) {
|
|
4494
|
-
writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
|
|
4495
|
-
process.stderr.write("\u2713 cartography-map.html\n");
|
|
4496
|
-
}
|
|
4497
|
-
if (formats.includes("discovery")) {
|
|
3369
|
+
if (formats.includes("html") || formats.includes("map") || formats.includes("discovery")) {
|
|
4498
3370
|
writeFileSync(join2(outputDir, "discovery.html"), exportDiscoveryApp(nodes, edges));
|
|
4499
3371
|
process.stderr.write("\u2713 discovery.html\n");
|
|
4500
3372
|
}
|
|
@@ -4562,9 +3434,8 @@ export {
|
|
|
4562
3434
|
edgesToConnections,
|
|
4563
3435
|
exportAll,
|
|
4564
3436
|
exportBackstageYAML,
|
|
4565
|
-
exportCartographyMap,
|
|
4566
3437
|
exportDiscoveryApp,
|
|
4567
|
-
|
|
3438
|
+
exportJGF,
|
|
4568
3439
|
exportJSON,
|
|
4569
3440
|
exportSOPDashboard,
|
|
4570
3441
|
exportSOPMarkdown,
|