@datasynx/agentic-ai-cartography 0.5.0 → 0.6.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 +1092 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1087 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2810,7 +2810,1088 @@ draw();
|
|
|
2810
2810
|
</body>
|
|
2811
2811
|
</html>`;
|
|
2812
2812
|
}
|
|
2813
|
-
function
|
|
2813
|
+
function exportDiscoveryApp(nodes, edges, options) {
|
|
2814
|
+
const theme = options?.theme ?? "dark";
|
|
2815
|
+
const graphData = JSON.stringify({
|
|
2816
|
+
nodes: nodes.map((n) => ({
|
|
2817
|
+
id: n.id,
|
|
2818
|
+
name: n.name,
|
|
2819
|
+
type: n.type,
|
|
2820
|
+
layer: nodeLayer(n.type),
|
|
2821
|
+
confidence: n.confidence,
|
|
2822
|
+
discoveredVia: n.discoveredVia,
|
|
2823
|
+
discoveredAt: n.discoveredAt,
|
|
2824
|
+
tags: n.tags,
|
|
2825
|
+
metadata: n.metadata
|
|
2826
|
+
})),
|
|
2827
|
+
links: edges.map((e) => ({
|
|
2828
|
+
source: e.sourceId,
|
|
2829
|
+
target: e.targetId,
|
|
2830
|
+
relationship: e.relationship,
|
|
2831
|
+
confidence: e.confidence,
|
|
2832
|
+
evidence: e.evidence
|
|
2833
|
+
}))
|
|
2834
|
+
});
|
|
2835
|
+
const { assets, clusters, connections } = buildMapData(nodes, edges, { theme });
|
|
2836
|
+
const isEmpty = assets.length === 0;
|
|
2837
|
+
const HEX_SIZE2 = 24;
|
|
2838
|
+
const mapJson = JSON.stringify({
|
|
2839
|
+
assets: assets.map((a) => ({
|
|
2840
|
+
id: a.id,
|
|
2841
|
+
name: a.name,
|
|
2842
|
+
domain: a.domain,
|
|
2843
|
+
subDomain: a.subDomain ?? null,
|
|
2844
|
+
qualityScore: a.qualityScore ?? null,
|
|
2845
|
+
metadata: a.metadata,
|
|
2846
|
+
q: a.position.q,
|
|
2847
|
+
r: a.position.r
|
|
2848
|
+
})),
|
|
2849
|
+
clusters: clusters.map((c) => ({
|
|
2850
|
+
id: c.id,
|
|
2851
|
+
label: c.label,
|
|
2852
|
+
domain: c.domain,
|
|
2853
|
+
color: c.color,
|
|
2854
|
+
assetIds: c.assetIds,
|
|
2855
|
+
centroid: c.centroid
|
|
2856
|
+
})),
|
|
2857
|
+
connections: connections.map((c) => ({
|
|
2858
|
+
id: c.id,
|
|
2859
|
+
sourceAssetId: c.sourceAssetId,
|
|
2860
|
+
targetAssetId: c.targetAssetId,
|
|
2861
|
+
type: c.type ?? "connection"
|
|
2862
|
+
}))
|
|
2863
|
+
});
|
|
2864
|
+
const nodeCount = nodes.length;
|
|
2865
|
+
const edgeCount = edges.length;
|
|
2866
|
+
const assetCount = assets.length;
|
|
2867
|
+
const clusterCount = clusters.length;
|
|
2868
|
+
return `<!DOCTYPE html>
|
|
2869
|
+
<html lang="en" data-theme="${theme}">
|
|
2870
|
+
<head>
|
|
2871
|
+
<meta charset="UTF-8"/>
|
|
2872
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
2873
|
+
<title>Cartography \u2014 Datasynx Discovery</title>
|
|
2874
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
2875
|
+
<style>
|
|
2876
|
+
/* \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 */
|
|
2877
|
+
:root{
|
|
2878
|
+
--bg-base:#0f172a;--bg-surface:#1e293b;--bg-elevated:#273148;
|
|
2879
|
+
--border:#334155;--border-dim:#1e293b;
|
|
2880
|
+
--text:#e2e8f0;--text-muted:#94a3b8;--text-dim:#475569;
|
|
2881
|
+
--accent:#3b82f6;--accent-hover:#2563eb;--accent-dim:rgba(59,130,246,.12);
|
|
2882
|
+
}
|
|
2883
|
+
[data-theme="light"]{
|
|
2884
|
+
--bg-base:#f8fafc;--bg-surface:#ffffff;--bg-elevated:#f1f5f9;
|
|
2885
|
+
--border:#e2e8f0;--border-dim:#f1f5f9;
|
|
2886
|
+
--text:#0f172a;--text-muted:#64748b;--text-dim:#94a3b8;
|
|
2887
|
+
--accent:#2563eb;--accent-hover:#1d4ed8;--accent-dim:rgba(37,99,235,.08);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
/* \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 */
|
|
2891
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
2892
|
+
html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',sans-serif}
|
|
2893
|
+
body{display:flex;flex-direction:column;background:var(--bg-base);color:var(--text)}
|
|
2894
|
+
|
|
2895
|
+
/* \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 */
|
|
2896
|
+
#topbar{
|
|
2897
|
+
height:56px;display:flex;align-items:center;gap:16px;padding:0 20px;
|
|
2898
|
+
background:var(--bg-surface);border-bottom:1px solid var(--border);z-index:100;flex-shrink:0;
|
|
2899
|
+
}
|
|
2900
|
+
.tb-left{display:flex;align-items:center;gap:10px}
|
|
2901
|
+
.brand-logo{flex-shrink:0}
|
|
2902
|
+
.brand-name{font-size:15px;font-weight:700;color:var(--accent);letter-spacing:-.02em}
|
|
2903
|
+
.brand-product{font-size:14px;font-weight:500;color:var(--text-muted);margin-left:2px}
|
|
2904
|
+
.brand-sep{width:1px;height:24px;background:var(--border);margin:0 6px}
|
|
2905
|
+
.tb-center{display:flex;align-items:center;gap:2px;margin-left:auto;
|
|
2906
|
+
background:var(--bg-elevated);border-radius:8px;padding:3px}
|
|
2907
|
+
.tab-btn{
|
|
2908
|
+
padding:6px 16px;border:none;border-radius:6px;font-size:13px;font-weight:500;
|
|
2909
|
+
cursor:pointer;color:var(--text-muted);background:transparent;font-family:inherit;
|
|
2910
|
+
transition:all .15s;
|
|
2911
|
+
}
|
|
2912
|
+
.tab-btn:hover{color:var(--text)}
|
|
2913
|
+
.tab-btn.active{background:var(--accent);color:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2)}
|
|
2914
|
+
.tb-right{display:flex;align-items:center;gap:8px;margin-left:auto}
|
|
2915
|
+
.tb-search{
|
|
2916
|
+
display:flex;align-items:center;gap:6px;background:var(--bg-elevated);
|
|
2917
|
+
border:1px solid var(--border);border-radius:8px;padding:5px 10px;
|
|
2918
|
+
}
|
|
2919
|
+
.tb-search input{
|
|
2920
|
+
border:none;background:transparent;font-size:13px;outline:none;width:160px;
|
|
2921
|
+
color:var(--text);font-family:inherit;
|
|
2922
|
+
}
|
|
2923
|
+
.tb-search input::placeholder{color:var(--text-dim)}
|
|
2924
|
+
.tb-search svg{flex-shrink:0;color:var(--text-dim)}
|
|
2925
|
+
.icon-btn{
|
|
2926
|
+
width:36px;height:36px;border-radius:8px;border:1px solid var(--border);
|
|
2927
|
+
background:var(--bg-surface);cursor:pointer;display:flex;align-items:center;
|
|
2928
|
+
justify-content:center;color:var(--text-muted);text-decoration:none;transition:all .15s;font-size:16px;
|
|
2929
|
+
}
|
|
2930
|
+
.icon-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
|
|
2931
|
+
.tb-stats{font-size:11px;color:var(--text-dim);white-space:nowrap}
|
|
2932
|
+
|
|
2933
|
+
/* \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 */
|
|
2934
|
+
.view{flex:1;display:none;overflow:hidden;position:relative}
|
|
2935
|
+
.view.active{display:flex}
|
|
2936
|
+
|
|
2937
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
2938
|
+
MAP VIEW
|
|
2939
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
2940
|
+
#map-wrap{flex:1;position:relative;overflow:hidden;cursor:grab}
|
|
2941
|
+
#map-wrap.dragging{cursor:grabbing}
|
|
2942
|
+
#map-wrap.connecting{cursor:crosshair}
|
|
2943
|
+
#map-wrap canvas{display:block;width:100%;height:100%}
|
|
2944
|
+
#map-detail{
|
|
2945
|
+
width:280px;background:var(--bg-surface);border-left:1px solid var(--border);
|
|
2946
|
+
display:flex;flex-direction:column;transform:translateX(100%);
|
|
2947
|
+
transition:transform .2s ease;z-index:5;flex-shrink:0;overflow-y:auto;
|
|
2948
|
+
}
|
|
2949
|
+
#map-detail.open{transform:translateX(0)}
|
|
2950
|
+
#map-detail .panel-header{
|
|
2951
|
+
padding:16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px;
|
|
2952
|
+
}
|
|
2953
|
+
#map-detail .panel-header h3{font-size:14px;font-weight:600;flex:1;word-break:break-word}
|
|
2954
|
+
.close-btn{
|
|
2955
|
+
width:24px;height:24px;border:none;background:transparent;cursor:pointer;
|
|
2956
|
+
color:var(--text-muted);border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:16px;
|
|
2957
|
+
}
|
|
2958
|
+
.close-btn:hover{background:var(--bg-elevated)}
|
|
2959
|
+
.panel-body{padding:12px 16px;display:flex;flex-direction:column;gap:12px}
|
|
2960
|
+
.meta-row{display:flex;flex-direction:column;gap:3px}
|
|
2961
|
+
.meta-label{font-size:11px;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
|
|
2962
|
+
.meta-value{font-size:13px;word-break:break-all}
|
|
2963
|
+
.quality-bar{height:6px;border-radius:3px;background:var(--bg-elevated);margin-top:4px}
|
|
2964
|
+
.quality-fill{height:6px;border-radius:3px;transition:width .3s}
|
|
2965
|
+
|
|
2966
|
+
/* Map toolbars */
|
|
2967
|
+
#map-tb-left{position:absolute;bottom:20px;left:20px;display:flex;gap:8px;z-index:10}
|
|
2968
|
+
#map-tb-right{position:absolute;bottom:20px;right:20px;display:flex;flex-direction:column;align-items:flex-end;gap:8px;z-index:10}
|
|
2969
|
+
.tb-tool{
|
|
2970
|
+
width:40px;height:40px;border-radius:10px;border:1px solid var(--border);
|
|
2971
|
+
background:var(--bg-surface);box-shadow:0 1px 4px rgba(0,0,0,.08);cursor:pointer;
|
|
2972
|
+
display:flex;align-items:center;justify-content:center;font-size:18px;
|
|
2973
|
+
transition:all .15s;color:var(--text);
|
|
2974
|
+
}
|
|
2975
|
+
.tb-tool:hover{border-color:var(--text-muted)}
|
|
2976
|
+
.tb-tool.active{background:var(--accent-dim);border-color:var(--accent)}
|
|
2977
|
+
.map-zoom{display:flex;align-items:center;gap:6px}
|
|
2978
|
+
.zoom-btn{
|
|
2979
|
+
width:34px;height:34px;border-radius:8px;border:1px solid var(--border);
|
|
2980
|
+
background:var(--bg-surface);cursor:pointer;font-size:18px;color:var(--text);
|
|
2981
|
+
display:flex;align-items:center;justify-content:center;
|
|
2982
|
+
}
|
|
2983
|
+
.zoom-btn:hover{background:var(--bg-elevated)}
|
|
2984
|
+
#map-zoom-pct{font-size:12px;font-weight:500;color:var(--text-dim);min-width:38px;text-align:center}
|
|
2985
|
+
#map-connect-hint{
|
|
2986
|
+
position:absolute;top:12px;left:50%;transform:translateX(-50%);
|
|
2987
|
+
background:#fef3c7;border:1px solid #f59e0b;color:#92400e;
|
|
2988
|
+
padding:6px 14px;border-radius:20px;font-size:12px;font-weight:500;
|
|
2989
|
+
display:none;z-index:20;pointer-events:none;
|
|
2990
|
+
}
|
|
2991
|
+
#map-tooltip{
|
|
2992
|
+
position:fixed;background:var(--bg-surface);color:var(--text);border-radius:8px;
|
|
2993
|
+
padding:8px 12px;font-size:12px;pointer-events:none;z-index:200;
|
|
2994
|
+
display:none;max-width:220px;box-shadow:0 4px 12px rgba(0,0,0,.25);border:1px solid var(--border);
|
|
2995
|
+
}
|
|
2996
|
+
#map-tooltip .tt-name{font-weight:600;margin-bottom:2px}
|
|
2997
|
+
#map-tooltip .tt-domain{color:var(--text-muted);font-size:11px}
|
|
2998
|
+
#map-tooltip .tt-quality{font-size:11px;margin-top:2px}
|
|
2999
|
+
#map-empty{
|
|
3000
|
+
position:absolute;inset:0;display:flex;flex-direction:column;
|
|
3001
|
+
align-items:center;justify-content:center;gap:12px;color:var(--text-muted);
|
|
3002
|
+
}
|
|
3003
|
+
#map-empty p{font-size:14px}
|
|
3004
|
+
|
|
3005
|
+
/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3006
|
+
TOPOLOGY VIEW
|
|
3007
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 */
|
|
3008
|
+
#topo-panel{
|
|
3009
|
+
width:220px;min-width:220px;height:100%;overflow:hidden;
|
|
3010
|
+
background:var(--bg-surface);border-right:1px solid var(--border);
|
|
3011
|
+
display:flex;flex-direction:column;
|
|
3012
|
+
}
|
|
3013
|
+
#topo-panel-header{
|
|
3014
|
+
padding:10px 12px 8px;border-bottom:1px solid var(--border);
|
|
3015
|
+
font-size:11px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.6px;
|
|
3016
|
+
}
|
|
3017
|
+
#topo-search{
|
|
3018
|
+
width:calc(100% - 16px);margin:8px;padding:5px 8px;
|
|
3019
|
+
background:var(--bg-elevated);border:1px solid var(--border);border-radius:5px;
|
|
3020
|
+
color:var(--text);font-size:11px;font-family:inherit;outline:none;
|
|
3021
|
+
}
|
|
3022
|
+
#topo-search:focus{border-color:var(--accent)}
|
|
3023
|
+
#topo-list{flex:1;overflow-y:auto;padding-bottom:8px}
|
|
3024
|
+
.topo-item{
|
|
3025
|
+
padding:5px 12px;cursor:pointer;font-size:11px;
|
|
3026
|
+
display:flex;align-items:center;gap:6px;border-left:2px solid transparent;
|
|
3027
|
+
}
|
|
3028
|
+
.topo-item:hover{background:var(--bg-elevated)}
|
|
3029
|
+
.topo-item.active{background:var(--accent-dim);border-left-color:var(--accent)}
|
|
3030
|
+
.topo-dot{width:7px;height:7px;border-radius:2px;flex-shrink:0}
|
|
3031
|
+
.topo-name{color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
|
|
3032
|
+
.topo-type{color:var(--text-dim);font-size:9px;flex-shrink:0}
|
|
3033
|
+
|
|
3034
|
+
#topo-graph{flex:1;height:100%;position:relative}
|
|
3035
|
+
#topo-graph svg{width:100%;height:100%}
|
|
3036
|
+
.hull{opacity:.12;stroke-width:1.5;stroke-opacity:.25}
|
|
3037
|
+
.hull-label{font-size:13px;font-weight:700;letter-spacing:1px;text-transform:uppercase;fill-opacity:.5;pointer-events:none}
|
|
3038
|
+
.link{stroke-opacity:.4}
|
|
3039
|
+
.link-label{font-size:8px;fill:var(--text-dim);pointer-events:none;opacity:0}
|
|
3040
|
+
.node-hex{stroke-width:1.8;cursor:pointer;transition:opacity .15s}
|
|
3041
|
+
.node-hex:hover{filter:brightness(1.3);stroke-width:3}
|
|
3042
|
+
.node-hex.selected{stroke-width:3.5;filter:brightness(1.5)}
|
|
3043
|
+
.node-label{font-size:10px;fill:var(--text);pointer-events:none;opacity:0}
|
|
3044
|
+
|
|
3045
|
+
#topo-sidebar{
|
|
3046
|
+
width:300px;min-width:300px;height:100%;overflow-y:auto;
|
|
3047
|
+
background:var(--bg-surface);border-left:1px solid var(--border);
|
|
3048
|
+
padding:16px;font-size:12px;line-height:1.6;
|
|
3049
|
+
}
|
|
3050
|
+
#topo-sidebar h2{margin:0 0 8px;font-size:14px;color:var(--accent)}
|
|
3051
|
+
#topo-sidebar .meta-table{width:100%;border-collapse:collapse}
|
|
3052
|
+
#topo-sidebar .meta-table td{padding:3px 6px;border-bottom:1px solid var(--border-dim);vertical-align:top}
|
|
3053
|
+
#topo-sidebar .meta-table td:first-child{color:var(--text-dim);white-space:nowrap;width:90px}
|
|
3054
|
+
#topo-sidebar .tag{display:inline-block;background:var(--bg-elevated);border-radius:3px;padding:1px 5px;margin:1px;font-size:10px}
|
|
3055
|
+
#topo-sidebar .conf-bar{height:5px;border-radius:3px;background:var(--bg-elevated);margin-top:3px}
|
|
3056
|
+
#topo-sidebar .conf-fill{height:100%;border-radius:3px}
|
|
3057
|
+
#topo-sidebar .edges-list{margin-top:12px}
|
|
3058
|
+
#topo-sidebar .edge-item{padding:4px 0;border-bottom:1px solid var(--border-dim);color:var(--text-dim);font-size:11px}
|
|
3059
|
+
#topo-sidebar .edge-item span{color:var(--text)}
|
|
3060
|
+
.hint{color:var(--text-dim);font-size:11px;margin-top:8px}
|
|
3061
|
+
|
|
3062
|
+
#topo-hud{
|
|
3063
|
+
position:absolute;top:10px;left:10px;background:rgba(15,23,42,.88);
|
|
3064
|
+
padding:10px 14px;border-radius:8px;font-size:12px;border:1px solid var(--border);pointer-events:none;
|
|
3065
|
+
}
|
|
3066
|
+
#topo-hud strong{color:var(--accent)}
|
|
3067
|
+
#topo-hud .stats{color:var(--text-dim)}
|
|
3068
|
+
#topo-hud .zoom-level{color:var(--text-dim);font-size:10px;margin-top:2px}
|
|
3069
|
+
|
|
3070
|
+
#topo-toolbar{position:absolute;top:10px;right:10px;display:flex;flex-wrap:wrap;gap:4px;pointer-events:auto;align-items:center}
|
|
3071
|
+
.filter-btn{
|
|
3072
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
3073
|
+
color:var(--text);padding:4px 10px;font-size:11px;cursor:pointer;
|
|
3074
|
+
font-family:inherit;display:flex;align-items:center;gap:5px;
|
|
3075
|
+
}
|
|
3076
|
+
.filter-btn:hover{border-color:var(--text-dim)}
|
|
3077
|
+
.filter-btn.off{opacity:.35}
|
|
3078
|
+
.filter-dot{width:8px;height:8px;border-radius:2px;display:inline-block}
|
|
3079
|
+
.export-btn{
|
|
3080
|
+
background:rgba(15,23,42,.85);border:1px solid var(--border);border-radius:6px;
|
|
3081
|
+
color:var(--accent);padding:4px 12px;font-size:11px;cursor:pointer;font-family:inherit;
|
|
3082
|
+
}
|
|
3083
|
+
.export-btn:hover{border-color:var(--accent);background:var(--accent-dim)}
|
|
3084
|
+
</style>
|
|
3085
|
+
</head>
|
|
3086
|
+
<body>
|
|
3087
|
+
|
|
3088
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3089
|
+
TOPBAR
|
|
3090
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
3091
|
+
<header id="topbar">
|
|
3092
|
+
<div class="tb-left">
|
|
3093
|
+
<svg class="brand-logo" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
3094
|
+
<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"/>
|
|
3095
|
+
<circle cx="10" cy="16" r="2.8" fill="#60A5FA"/><circle cx="22" cy="10.5" r="2.2" fill="#38BDF8"/>
|
|
3096
|
+
<circle cx="22" cy="21.5" r="2.2" fill="#38BDF8"/>
|
|
3097
|
+
<line x1="12.5" y1="14.8" x2="19.8" y2="11.2" stroke="#93C5FD" stroke-width="1.2"/>
|
|
3098
|
+
<line x1="12.5" y1="17.2" x2="19.8" y2="20.8" stroke="#93C5FD" stroke-width="1.2"/>
|
|
3099
|
+
<line x1="22" y1="12.7" x2="22" y2="19.3" stroke="#93C5FD" stroke-width="1" stroke-dasharray="2 1.5"/>
|
|
3100
|
+
</svg>
|
|
3101
|
+
<span class="brand-name">datasynx</span>
|
|
3102
|
+
<span class="brand-sep"></span>
|
|
3103
|
+
<span class="brand-product">Cartography</span>
|
|
3104
|
+
</div>
|
|
3105
|
+
<div class="tb-center">
|
|
3106
|
+
<button class="tab-btn active" id="tab-map-btn" data-tab="map">Map</button>
|
|
3107
|
+
<button class="tab-btn" id="tab-topo-btn" data-tab="topo">Topology</button>
|
|
3108
|
+
</div>
|
|
3109
|
+
<div class="tb-right">
|
|
3110
|
+
<span class="tb-stats">${nodeCount} nodes · ${edgeCount} edges</span>
|
|
3111
|
+
<div class="tb-search">
|
|
3112
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
3113
|
+
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
|
|
3114
|
+
</svg>
|
|
3115
|
+
<input id="global-search" type="text" placeholder="Search..." autocomplete="off" spellcheck="false"/>
|
|
3116
|
+
</div>
|
|
3117
|
+
<a href="https://www.linkedin.com/company/datasynx-ai/" target="_blank" rel="noopener noreferrer"
|
|
3118
|
+
class="icon-btn" title="Datasynx on LinkedIn" aria-label="LinkedIn">
|
|
3119
|
+
<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
|
3120
|
+
<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"/>
|
|
3121
|
+
</svg>
|
|
3122
|
+
</a>
|
|
3123
|
+
<button id="theme-btn" class="icon-btn" title="Toggle theme" aria-label="Toggle theme">
|
|
3124
|
+
${theme === "dark" ? "☼" : "☾"}
|
|
3125
|
+
</button>
|
|
3126
|
+
</div>
|
|
3127
|
+
</header>
|
|
3128
|
+
|
|
3129
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3130
|
+
MAP VIEW
|
|
3131
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
3132
|
+
<div id="view-map" class="view active">
|
|
3133
|
+
<div id="map-wrap" tabindex="0" aria-label="Data cartography hex map">
|
|
3134
|
+
<canvas id="hexmap" aria-hidden="true"></canvas>
|
|
3135
|
+
${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>' : ""}
|
|
3136
|
+
</div>
|
|
3137
|
+
<div id="map-detail">
|
|
3138
|
+
<div class="panel-header">
|
|
3139
|
+
<h3 id="md-name">—</h3>
|
|
3140
|
+
<button class="close-btn" id="md-close" aria-label="Close">✕</button>
|
|
3141
|
+
</div>
|
|
3142
|
+
<div class="panel-body" id="md-body"></div>
|
|
3143
|
+
</div>
|
|
3144
|
+
<div id="map-tb-left">
|
|
3145
|
+
<button class="tb-tool active" id="btn-labels" title="Toggle labels">🏷</button>
|
|
3146
|
+
<button class="tb-tool" id="btn-quality" title="Quality layer">👁</button>
|
|
3147
|
+
<button class="tb-tool" id="btn-connect" title="Connection tool">🔗</button>
|
|
3148
|
+
</div>
|
|
3149
|
+
<div id="map-tb-right">
|
|
3150
|
+
<div class="map-zoom">
|
|
3151
|
+
<button class="zoom-btn" id="mz-out">−</button>
|
|
3152
|
+
<span id="map-zoom-pct">100%</span>
|
|
3153
|
+
<button class="zoom-btn" id="mz-in">+</button>
|
|
3154
|
+
</div>
|
|
3155
|
+
</div>
|
|
3156
|
+
<div id="map-connect-hint">Click two assets to create a connection</div>
|
|
3157
|
+
<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>
|
|
3158
|
+
</div>
|
|
3159
|
+
|
|
3160
|
+
<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3161
|
+
TOPOLOGY VIEW
|
|
3162
|
+
\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->
|
|
3163
|
+
<div id="view-topo" class="view">
|
|
3164
|
+
<div id="topo-panel">
|
|
3165
|
+
<div id="topo-panel-header">Nodes (${nodeCount})</div>
|
|
3166
|
+
<input id="topo-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false"/>
|
|
3167
|
+
<div id="topo-list"></div>
|
|
3168
|
+
</div>
|
|
3169
|
+
<div id="topo-graph">
|
|
3170
|
+
<div id="topo-hud">
|
|
3171
|
+
<strong>Topology</strong>
|
|
3172
|
+
<span class="stats">${nodeCount} nodes · ${edgeCount} edges</span><br/>
|
|
3173
|
+
<span class="zoom-level">Scroll = zoom · Drag = pan · Click = details</span>
|
|
3174
|
+
</div>
|
|
3175
|
+
<div id="topo-toolbar"></div>
|
|
3176
|
+
<svg></svg>
|
|
3177
|
+
</div>
|
|
3178
|
+
<div id="topo-sidebar">
|
|
3179
|
+
<h2>Infrastructure Map</h2>
|
|
3180
|
+
<p class="hint">Click a node to view details.</p>
|
|
3181
|
+
</div>
|
|
3182
|
+
</div>
|
|
3183
|
+
|
|
3184
|
+
<script>
|
|
3185
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3186
|
+
// SHARED STATE
|
|
3187
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3188
|
+
let isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
3189
|
+
let currentTab = 'map';
|
|
3190
|
+
let topoInited = false;
|
|
3191
|
+
|
|
3192
|
+
// \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
|
|
3193
|
+
document.getElementById('theme-btn').addEventListener('click', function() {
|
|
3194
|
+
isDark = !isDark;
|
|
3195
|
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
|
3196
|
+
this.innerHTML = isDark ? '\\u2606' : '\\u263E';
|
|
3197
|
+
if (typeof drawMap === 'function') drawMap();
|
|
3198
|
+
});
|
|
3199
|
+
|
|
3200
|
+
// \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
|
|
3201
|
+
document.querySelectorAll('.tab-btn').forEach(function(btn) {
|
|
3202
|
+
btn.addEventListener('click', function() {
|
|
3203
|
+
var tab = this.getAttribute('data-tab');
|
|
3204
|
+
if (tab === currentTab) return;
|
|
3205
|
+
currentTab = tab;
|
|
3206
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
3207
|
+
this.classList.add('active');
|
|
3208
|
+
document.querySelectorAll('.view').forEach(function(v) { v.classList.remove('active'); });
|
|
3209
|
+
document.getElementById('view-' + tab).classList.add('active');
|
|
3210
|
+
if (tab === 'topo' && !topoInited) { initTopology(); topoInited = true; }
|
|
3211
|
+
if (tab === 'map' && typeof drawMap === 'function') { resizeMap(); }
|
|
3212
|
+
});
|
|
3213
|
+
});
|
|
3214
|
+
|
|
3215
|
+
// \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
|
|
3216
|
+
document.getElementById('global-search').addEventListener('input', function(e) {
|
|
3217
|
+
var q = e.target.value.trim();
|
|
3218
|
+
if (typeof setMapSearch === 'function') setMapSearch(q);
|
|
3219
|
+
if (typeof setTopoSearch === 'function') setTopoSearch(q);
|
|
3220
|
+
});
|
|
3221
|
+
|
|
3222
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3223
|
+
// MAP VIEW
|
|
3224
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3225
|
+
var MAP = ${mapJson};
|
|
3226
|
+
var MAP_HEX = ${HEX_SIZE2};
|
|
3227
|
+
var MAP_EMPTY = ${isEmpty};
|
|
3228
|
+
|
|
3229
|
+
var mapAssetIndex = new Map();
|
|
3230
|
+
var mapClusterByAsset = new Map();
|
|
3231
|
+
for (var ci = 0; ci < MAP.clusters.length; ci++) {
|
|
3232
|
+
var c = MAP.clusters[ci];
|
|
3233
|
+
for (var ai = 0; ai < c.assetIds.length; ai++) mapClusterByAsset.set(c.assetIds[ai], c);
|
|
3234
|
+
}
|
|
3235
|
+
for (var ni = 0; ni < MAP.assets.length; ni++) mapAssetIndex.set(MAP.assets[ni].id, MAP.assets[ni]);
|
|
3236
|
+
|
|
3237
|
+
var mapCanvas = document.getElementById('hexmap');
|
|
3238
|
+
var mapCtx = mapCanvas.getContext('2d');
|
|
3239
|
+
var mapWrap = document.getElementById('map-wrap');
|
|
3240
|
+
var mW = 0, mH = 0;
|
|
3241
|
+
var mvx = 0, mvy = 0, mScale = 1;
|
|
3242
|
+
var mDetailLevel = 2, mShowLabels = true, mShowQuality = false;
|
|
3243
|
+
var mConnectMode = false, mConnectFirst = null;
|
|
3244
|
+
var mHoveredId = null, mSelectedId = null;
|
|
3245
|
+
var mSearchQuery = '';
|
|
3246
|
+
var mLocalConns = MAP.connections.slice();
|
|
3247
|
+
|
|
3248
|
+
function setMapSearch(q) { mSearchQuery = q; drawMap(); }
|
|
3249
|
+
|
|
3250
|
+
function resizeMap() {
|
|
3251
|
+
var dpr = window.devicePixelRatio || 1;
|
|
3252
|
+
mW = mapWrap.clientWidth; mH = mapWrap.clientHeight;
|
|
3253
|
+
mapCanvas.width = mW * dpr; mapCanvas.height = mH * dpr;
|
|
3254
|
+
mapCanvas.style.width = mW + 'px'; mapCanvas.style.height = mH + 'px';
|
|
3255
|
+
mapCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3256
|
+
drawMap();
|
|
3257
|
+
}
|
|
3258
|
+
window.addEventListener('resize', function() { if (currentTab === 'map') resizeMap(); });
|
|
3259
|
+
|
|
3260
|
+
function mHtp_x(q, r) { return MAP_HEX * (1.5 * q); }
|
|
3261
|
+
function mHtp_y(q, r) { return MAP_HEX * (Math.sqrt(3) / 2 * q + Math.sqrt(3) * r); }
|
|
3262
|
+
function mW2s(wx, wy) { return { x: wx * mScale + mvx, y: wy * mScale + mvy }; }
|
|
3263
|
+
function mS2w(sx, sy) { return { x: (sx - mvx) / mScale, y: (sy - mvy) / mScale }; }
|
|
3264
|
+
|
|
3265
|
+
function mapFitToView() {
|
|
3266
|
+
if (MAP_EMPTY || MAP.assets.length === 0) { mvx = 0; mvy = 0; mScale = 1; return; }
|
|
3267
|
+
var mnx = Infinity, mny = Infinity, mxx = -Infinity, mxy = -Infinity;
|
|
3268
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
3269
|
+
var a = MAP.assets[i], px = mHtp_x(a.q, a.r), py = mHtp_y(a.q, a.r);
|
|
3270
|
+
if (px < mnx) mnx = px; if (py < mny) mny = py; if (px > mxx) mxx = px; if (py > mxy) mxy = py;
|
|
3271
|
+
}
|
|
3272
|
+
var pw = mxx - mnx + MAP_HEX * 4, ph = mxy - mny + MAP_HEX * 4;
|
|
3273
|
+
mScale = Math.min(mW / pw, mH / ph, 2) * 0.85;
|
|
3274
|
+
mvx = mW / 2 - ((mnx + mxx) / 2) * mScale;
|
|
3275
|
+
mvy = mH / 2 - ((mny + mxy) / 2) * mScale;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
function mHexPath(cx, cy, r) {
|
|
3279
|
+
mapCtx.beginPath();
|
|
3280
|
+
for (var i = 0; i < 6; i++) {
|
|
3281
|
+
var angle = Math.PI / 180 * (60 * i);
|
|
3282
|
+
var x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle);
|
|
3283
|
+
i === 0 ? mapCtx.moveTo(x, y) : mapCtx.lineTo(x, y);
|
|
3284
|
+
}
|
|
3285
|
+
mapCtx.closePath();
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
function mShadeV(hex, amt) {
|
|
3289
|
+
if (!hex || hex.length < 7) return hex;
|
|
3290
|
+
var n = parseInt(hex.replace('#', ''), 16);
|
|
3291
|
+
var r = Math.min(255, (n >> 16) + amt), g = Math.min(255, ((n >> 8) & 0xff) + amt), b = Math.min(255, (n & 0xff) + amt);
|
|
3292
|
+
return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
function mGetSearchMatches() {
|
|
3296
|
+
if (!mSearchQuery) return new Set();
|
|
3297
|
+
var q = mSearchQuery.toLowerCase(), m = new Set();
|
|
3298
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
3299
|
+
var a = MAP.assets[i];
|
|
3300
|
+
if (a.name.toLowerCase().includes(q) || (a.domain && a.domain.toLowerCase().includes(q)) ||
|
|
3301
|
+
(a.subDomain && a.subDomain.toLowerCase().includes(q))) m.add(a.id);
|
|
3302
|
+
}
|
|
3303
|
+
return m;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
function mDrawPill(x, y, text, color, fontSize) {
|
|
3307
|
+
if (!text) return;
|
|
3308
|
+
mapCtx.save();
|
|
3309
|
+
mapCtx.font = '600 ' + fontSize + 'px -apple-system,sans-serif';
|
|
3310
|
+
var tw = mapCtx.measureText(text).width;
|
|
3311
|
+
var ph = fontSize + 8, pw = tw + 20;
|
|
3312
|
+
mapCtx.beginPath();
|
|
3313
|
+
if (mapCtx.roundRect) mapCtx.roundRect(x - pw / 2, y - ph / 2, pw, ph, ph / 2);
|
|
3314
|
+
else mapCtx.rect(x - pw / 2, y - ph / 2, pw, ph);
|
|
3315
|
+
mapCtx.fillStyle = isDark ? 'rgba(30,41,59,0.9)' : 'rgba(255,255,255,0.92)';
|
|
3316
|
+
mapCtx.shadowColor = 'rgba(0,0,0,0.15)'; mapCtx.shadowBlur = 6;
|
|
3317
|
+
mapCtx.fill(); mapCtx.shadowBlur = 0;
|
|
3318
|
+
mapCtx.fillStyle = isDark ? '#e2e8f0' : '#0f172a';
|
|
3319
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
3320
|
+
mapCtx.fillText(text, x, y);
|
|
3321
|
+
mapCtx.restore();
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function drawMap() {
|
|
3325
|
+
mapCtx.clearRect(0, 0, mW, mH);
|
|
3326
|
+
var bg = getComputedStyle(document.documentElement).getPropertyValue('--bg-base').trim();
|
|
3327
|
+
mapCtx.fillStyle = bg || (isDark ? '#0f172a' : '#f8fafc');
|
|
3328
|
+
mapCtx.fillRect(0, 0, mW, mH);
|
|
3329
|
+
if (MAP_EMPTY) return;
|
|
3330
|
+
|
|
3331
|
+
var size = MAP_HEX * mScale;
|
|
3332
|
+
var matchedIds = mGetSearchMatches();
|
|
3333
|
+
var hasSearch = mSearchQuery.length > 0;
|
|
3334
|
+
|
|
3335
|
+
// Connections
|
|
3336
|
+
mapCtx.save();
|
|
3337
|
+
mapCtx.strokeStyle = isDark ? 'rgba(148,163,184,0.35)' : 'rgba(100,116,139,0.25)';
|
|
3338
|
+
mapCtx.lineWidth = 1.5; mapCtx.setLineDash([4, 4]);
|
|
3339
|
+
for (var ci = 0; ci < mLocalConns.length; ci++) {
|
|
3340
|
+
var conn = mLocalConns[ci];
|
|
3341
|
+
var src = mapAssetIndex.get(conn.sourceAssetId), tgt = mapAssetIndex.get(conn.targetAssetId);
|
|
3342
|
+
if (!src || !tgt) continue;
|
|
3343
|
+
var sp = mW2s(mHtp_x(src.q, src.r), mHtp_y(src.q, src.r));
|
|
3344
|
+
var tp = mW2s(mHtp_x(tgt.q, tgt.r), mHtp_y(tgt.q, tgt.r));
|
|
3345
|
+
mapCtx.beginPath(); mapCtx.moveTo(sp.x, sp.y); mapCtx.lineTo(tp.x, tp.y); mapCtx.stroke();
|
|
3346
|
+
}
|
|
3347
|
+
mapCtx.setLineDash([]); mapCtx.restore();
|
|
3348
|
+
|
|
3349
|
+
// Hexagons per cluster
|
|
3350
|
+
for (var cli = 0; cli < MAP.clusters.length; cli++) {
|
|
3351
|
+
var cluster = MAP.clusters[cli];
|
|
3352
|
+
var baseColor = cluster.color;
|
|
3353
|
+
var clusterAssets = cluster.assetIds.map(function(id) { return mapAssetIndex.get(id); }).filter(Boolean);
|
|
3354
|
+
var isClusterMatch = !hasSearch || clusterAssets.some(function(a) { return matchedIds.has(a.id); });
|
|
3355
|
+
var clusterDim = hasSearch && !isClusterMatch;
|
|
3356
|
+
|
|
3357
|
+
for (var ai = 0; ai < clusterAssets.length; ai++) {
|
|
3358
|
+
var asset = clusterAssets[ai];
|
|
3359
|
+
var wx = mHtp_x(asset.q, asset.r), wy = mHtp_y(asset.q, asset.r);
|
|
3360
|
+
var s = mW2s(wx, wy), cx = s.x, cy = s.y;
|
|
3361
|
+
if (cx + size < 0 || cx - size > mW || cy + size < 0 || cy - size > mH) continue;
|
|
3362
|
+
|
|
3363
|
+
var shade = ai % 3 === 0 ? 18 : ai % 3 === 1 ? 8 : 0;
|
|
3364
|
+
var fillColor = mShadeV(baseColor, shade);
|
|
3365
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined) {
|
|
3366
|
+
if (asset.qualityScore < 40) fillColor = '#ef4444';
|
|
3367
|
+
else if (asset.qualityScore < 70) fillColor = '#f97316';
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
var alpha = clusterDim ? 0.18 : 1;
|
|
3371
|
+
var isHov = asset.id === mHoveredId, isSel = asset.id === mSelectedId, isCF = asset.id === mConnectFirst;
|
|
3372
|
+
|
|
3373
|
+
mapCtx.save(); mapCtx.globalAlpha = alpha;
|
|
3374
|
+
mHexPath(cx, cy, size * 0.92);
|
|
3375
|
+
if (isDark && (isHov || isSel || isCF)) { mapCtx.shadowColor = fillColor; mapCtx.shadowBlur = isSel ? 16 : 8; }
|
|
3376
|
+
mapCtx.fillStyle = fillColor; mapCtx.fill();
|
|
3377
|
+
if (isSel || isCF) { mapCtx.strokeStyle = isCF ? '#f59e0b' : '#fff'; mapCtx.lineWidth = 2.5; mapCtx.stroke(); }
|
|
3378
|
+
else if (isHov) { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)'; mapCtx.lineWidth = 1.5; mapCtx.stroke(); }
|
|
3379
|
+
else { mapCtx.strokeStyle = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.4)'; mapCtx.lineWidth = 1; mapCtx.stroke(); }
|
|
3380
|
+
mapCtx.restore();
|
|
3381
|
+
|
|
3382
|
+
if (mShowQuality && asset.qualityScore !== null && asset.qualityScore !== undefined && size > 8 && asset.qualityScore < 70) {
|
|
3383
|
+
mapCtx.beginPath(); mapCtx.arc(cx + size * 0.4, cy - size * 0.4, Math.max(3, size * 0.14), 0, Math.PI * 2);
|
|
3384
|
+
mapCtx.fillStyle = asset.qualityScore < 40 ? '#ef4444' : '#f97316'; mapCtx.fill();
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
var showAssetLabel = mShowLabels && !clusterDim && ((mDetailLevel >= 4) || (mDetailLevel === 3 && mScale >= 0.8));
|
|
3388
|
+
if (showAssetLabel && size > 14) {
|
|
3389
|
+
var label = asset.name.length > 12 ? asset.name.substring(0, 11) + '...' : asset.name;
|
|
3390
|
+
mapCtx.save();
|
|
3391
|
+
mapCtx.font = Math.max(8, Math.min(11, size * 0.38)) + 'px -apple-system,sans-serif';
|
|
3392
|
+
mapCtx.fillStyle = isDark ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.9)';
|
|
3393
|
+
mapCtx.textAlign = 'center'; mapCtx.textBaseline = 'middle';
|
|
3394
|
+
mapCtx.fillText(label, cx, cy); mapCtx.restore();
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// Cluster labels
|
|
3400
|
+
if (mShowLabels && mDetailLevel >= 1) {
|
|
3401
|
+
for (var cli2 = 0; cli2 < MAP.clusters.length; cli2++) {
|
|
3402
|
+
var cl = MAP.clusters[cli2];
|
|
3403
|
+
if (cl.assetIds.length === 0) continue;
|
|
3404
|
+
if (hasSearch && !cl.assetIds.some(function(id) { return matchedIds.has(id); })) continue;
|
|
3405
|
+
var sc = mW2s(cl.centroid.x, cl.centroid.y);
|
|
3406
|
+
mDrawPill(sc.x, sc.y - size * 1.2, cl.label, cl.color, 14);
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
// Sub-domain labels
|
|
3411
|
+
if (mShowLabels && mDetailLevel >= 2) {
|
|
3412
|
+
var subGroups = new Map();
|
|
3413
|
+
for (var si = 0; si < MAP.assets.length; si++) {
|
|
3414
|
+
var sa = MAP.assets[si];
|
|
3415
|
+
if (!sa.subDomain) continue;
|
|
3416
|
+
var key = sa.domain + '|' + sa.subDomain;
|
|
3417
|
+
if (!subGroups.has(key)) subGroups.set(key, []);
|
|
3418
|
+
subGroups.get(key).push(sa);
|
|
3419
|
+
}
|
|
3420
|
+
subGroups.forEach(function(group) {
|
|
3421
|
+
var sx = 0, sy = 0;
|
|
3422
|
+
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); }
|
|
3423
|
+
var cxs = sx / group.length, cys = sy / group.length;
|
|
3424
|
+
var spt = mW2s(cxs, cys);
|
|
3425
|
+
mDrawPill(spt.x, spt.y + size * 1.5, group[0].subDomain, '#64748b', 11);
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// \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
|
|
3431
|
+
function mGetAssetAt(sx, sy) {
|
|
3432
|
+
var w = mS2w(sx, sy);
|
|
3433
|
+
for (var i = 0; i < MAP.assets.length; i++) {
|
|
3434
|
+
var a = MAP.assets[i], wx = mHtp_x(a.q, a.r), wy = mHtp_y(a.q, a.r);
|
|
3435
|
+
var dx = Math.abs(w.x - wx), dy = Math.abs(w.y - wy);
|
|
3436
|
+
if (dx > MAP_HEX || dy > MAP_HEX) continue;
|
|
3437
|
+
if (dx * dx + dy * dy < MAP_HEX * MAP_HEX) return a;
|
|
3438
|
+
}
|
|
3439
|
+
return null;
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// \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
|
|
3443
|
+
var mDragging = false, mLastMX = 0, mLastMY = 0;
|
|
3444
|
+
mapWrap.addEventListener('mousedown', function(e) {
|
|
3445
|
+
if (e.button !== 0) return;
|
|
3446
|
+
mDragging = true; mLastMX = e.clientX; mLastMY = e.clientY;
|
|
3447
|
+
mapWrap.classList.add('dragging');
|
|
3448
|
+
});
|
|
3449
|
+
window.addEventListener('mouseup', function() { mDragging = false; mapWrap.classList.remove('dragging'); });
|
|
3450
|
+
window.addEventListener('mousemove', function(e) {
|
|
3451
|
+
if (currentTab !== 'map') return;
|
|
3452
|
+
if (mDragging) {
|
|
3453
|
+
mvx += e.clientX - mLastMX; mvy += e.clientY - mLastMY;
|
|
3454
|
+
mLastMX = e.clientX; mLastMY = e.clientY; drawMap(); return;
|
|
3455
|
+
}
|
|
3456
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
3457
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
3458
|
+
var asset = mGetAssetAt(sx, sy);
|
|
3459
|
+
var newId = asset ? asset.id : null;
|
|
3460
|
+
if (newId !== mHoveredId) { mHoveredId = newId; drawMap(); }
|
|
3461
|
+
var tt = document.getElementById('map-tooltip');
|
|
3462
|
+
if (asset) {
|
|
3463
|
+
document.getElementById('mtt-name').textContent = asset.name;
|
|
3464
|
+
document.getElementById('mtt-domain').textContent = asset.domain + (asset.subDomain ? ' > ' + asset.subDomain : '');
|
|
3465
|
+
document.getElementById('mtt-quality').textContent = asset.qualityScore !== null ? 'Quality: ' + asset.qualityScore + '/100' : '';
|
|
3466
|
+
tt.style.display = 'block'; tt.style.left = (e.clientX + 12) + 'px'; tt.style.top = (e.clientY - 8) + 'px';
|
|
3467
|
+
} else { tt.style.display = 'none'; }
|
|
3468
|
+
});
|
|
3469
|
+
|
|
3470
|
+
mapWrap.addEventListener('click', function(e) {
|
|
3471
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
3472
|
+
var sx = e.clientX - rect.left, sy = e.clientY - rect.top;
|
|
3473
|
+
var asset = mGetAssetAt(sx, sy);
|
|
3474
|
+
if (mConnectMode) {
|
|
3475
|
+
if (!asset) return;
|
|
3476
|
+
if (!mConnectFirst) { mConnectFirst = asset.id; drawMap(); }
|
|
3477
|
+
else if (mConnectFirst !== asset.id) {
|
|
3478
|
+
mLocalConns.push({ id: crypto.randomUUID(), sourceAssetId: mConnectFirst, targetAssetId: asset.id, type: 'connection' });
|
|
3479
|
+
mConnectFirst = null; drawMap();
|
|
3480
|
+
}
|
|
3481
|
+
return;
|
|
3482
|
+
}
|
|
3483
|
+
if (asset) { mSelectedId = asset.id; mShowDetail(asset); }
|
|
3484
|
+
else { mSelectedId = null; document.getElementById('map-detail').classList.remove('open'); }
|
|
3485
|
+
drawMap();
|
|
3486
|
+
});
|
|
3487
|
+
|
|
3488
|
+
mapWrap.addEventListener('wheel', function(e) {
|
|
3489
|
+
e.preventDefault();
|
|
3490
|
+
var rect = mapWrap.getBoundingClientRect();
|
|
3491
|
+
mApplyZoom(e.deltaY < 0 ? 1.12 : 1 / 1.12, e.clientX - rect.left, e.clientY - rect.top);
|
|
3492
|
+
}, { passive: false });
|
|
3493
|
+
|
|
3494
|
+
function mApplyZoom(factor, sx, sy) {
|
|
3495
|
+
var ns = Math.max(0.05, Math.min(8, mScale * factor));
|
|
3496
|
+
var wx = (sx - mvx) / mScale, wy = (sy - mvy) / mScale;
|
|
3497
|
+
mScale = ns; mvx = sx - wx * mScale; mvy = sy - wy * mScale;
|
|
3498
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
3499
|
+
drawMap();
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
document.getElementById('mz-in').addEventListener('click', function() { mApplyZoom(1.25, mW / 2, mH / 2); });
|
|
3503
|
+
document.getElementById('mz-out').addEventListener('click', function() { mApplyZoom(1 / 1.25, mW / 2, mH / 2); });
|
|
3504
|
+
|
|
3505
|
+
// \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
|
|
3506
|
+
function mEsc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
3507
|
+
function mRenderQ(s) {
|
|
3508
|
+
var c = s >= 70 ? '#22c55e' : s >= 40 ? '#f97316' : '#ef4444';
|
|
3509
|
+
return s + '/100 <div class="quality-bar"><div class="quality-fill" style="width:' + s + '%;background:' + c + '"></div></div>';
|
|
3510
|
+
}
|
|
3511
|
+
function mShowDetail(asset) {
|
|
3512
|
+
document.getElementById('md-name').textContent = asset.name;
|
|
3513
|
+
var body = document.getElementById('md-body');
|
|
3514
|
+
var rows = [['Domain', asset.domain], ['Sub-domain', asset.subDomain],
|
|
3515
|
+
['Quality', asset.qualityScore !== null ? mRenderQ(asset.qualityScore) : null]
|
|
3516
|
+
].concat(Object.entries(asset.metadata || {}).slice(0, 8).map(function(kv) { return [kv[0], String(kv[1])]; }))
|
|
3517
|
+
.filter(function(r) { return r[1] !== null && r[1] !== undefined && r[1] !== ''; });
|
|
3518
|
+
body.innerHTML = rows.map(function(r) {
|
|
3519
|
+
return '<div class="meta-row"><div class="meta-label">' + mEsc(String(r[0])) + '</div><div class="meta-value">' + r[1] + '</div></div>';
|
|
3520
|
+
}).join('');
|
|
3521
|
+
var related = mLocalConns.filter(function(cn) { return cn.sourceAssetId === asset.id || cn.targetAssetId === asset.id; });
|
|
3522
|
+
if (related.length > 0) {
|
|
3523
|
+
body.innerHTML += '<div class="meta-row"><div class="meta-label">Connections (' + related.length + ')</div><div>' +
|
|
3524
|
+
related.map(function(cn) {
|
|
3525
|
+
var oid = cn.sourceAssetId === asset.id ? cn.targetAssetId : cn.sourceAssetId;
|
|
3526
|
+
var o = mapAssetIndex.get(oid);
|
|
3527
|
+
return '<div class="meta-value" style="margin-top:4px;font-size:12px">' + (o ? mEsc(o.name) : oid) + '</div>';
|
|
3528
|
+
}).join('') + '</div></div>';
|
|
3529
|
+
}
|
|
3530
|
+
document.getElementById('map-detail').classList.add('open');
|
|
3531
|
+
}
|
|
3532
|
+
document.getElementById('md-close').addEventListener('click', function() {
|
|
3533
|
+
document.getElementById('map-detail').classList.remove('open'); mSelectedId = null; drawMap();
|
|
3534
|
+
});
|
|
3535
|
+
|
|
3536
|
+
// \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
|
|
3537
|
+
document.getElementById('btn-labels').addEventListener('click', function() {
|
|
3538
|
+
mShowLabels = !mShowLabels; this.classList.toggle('active', mShowLabels); drawMap();
|
|
3539
|
+
});
|
|
3540
|
+
document.getElementById('btn-quality').addEventListener('click', function() {
|
|
3541
|
+
mShowQuality = !mShowQuality; this.classList.toggle('active', mShowQuality); drawMap();
|
|
3542
|
+
});
|
|
3543
|
+
document.getElementById('btn-connect').addEventListener('click', function() {
|
|
3544
|
+
mConnectMode = !mConnectMode; mConnectFirst = null;
|
|
3545
|
+
this.classList.toggle('active', mConnectMode);
|
|
3546
|
+
mapWrap.classList.toggle('connecting', mConnectMode);
|
|
3547
|
+
document.getElementById('map-connect-hint').style.display = mConnectMode ? 'block' : 'none'; drawMap();
|
|
3548
|
+
});
|
|
3549
|
+
|
|
3550
|
+
// Map keyboard
|
|
3551
|
+
mapWrap.addEventListener('keydown', function(e) {
|
|
3552
|
+
if (e.key === 'ArrowLeft') { mvx += 40; drawMap(); }
|
|
3553
|
+
else if (e.key === 'ArrowRight') { mvx -= 40; drawMap(); }
|
|
3554
|
+
else if (e.key === 'ArrowUp') { mvy += 40; drawMap(); }
|
|
3555
|
+
else if (e.key === 'ArrowDown') { mvy -= 40; drawMap(); }
|
|
3556
|
+
else if (e.key === '+' || e.key === '=') mApplyZoom(1.2, mW / 2, mH / 2);
|
|
3557
|
+
else if (e.key === '-') mApplyZoom(1 / 1.2, mW / 2, mH / 2);
|
|
3558
|
+
else if (e.key === 'Escape') {
|
|
3559
|
+
mSelectedId = null; document.getElementById('map-detail').classList.remove('open');
|
|
3560
|
+
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'); }
|
|
3561
|
+
drawMap();
|
|
3562
|
+
}
|
|
3563
|
+
});
|
|
3564
|
+
|
|
3565
|
+
// Map init
|
|
3566
|
+
resizeMap(); mapFitToView();
|
|
3567
|
+
document.getElementById('map-zoom-pct').textContent = Math.round(mScale * 100) + '%';
|
|
3568
|
+
drawMap();
|
|
3569
|
+
|
|
3570
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3571
|
+
// TOPOLOGY VIEW (lazy init)
|
|
3572
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
3573
|
+
var TOPO = ${graphData};
|
|
3574
|
+
|
|
3575
|
+
var TYPE_COLORS = {
|
|
3576
|
+
host:'#4a9eff',database_server:'#ff6b6b',database:'#ff8c42',
|
|
3577
|
+
web_service:'#6bcb77',api_endpoint:'#4d96ff',cache_server:'#ffd93d',
|
|
3578
|
+
message_broker:'#c77dff',queue:'#e0aaff',topic:'#9d4edd',
|
|
3579
|
+
container:'#48cae4',pod:'#00b4d8',k8s_cluster:'#0077b6',
|
|
3580
|
+
config_file:'#adb5bd',saas_tool:'#c084fc',table:'#f97316',unknown:'#6c757d'
|
|
3581
|
+
};
|
|
3582
|
+
var LAYER_COLORS = { saas:'#c084fc',web:'#6bcb77',data:'#ff6b6b',messaging:'#c77dff',infra:'#4a9eff',config:'#adb5bd',other:'#6c757d' };
|
|
3583
|
+
var LAYER_NAMES = { saas:'SaaS Tools',web:'Web / API',data:'Data Layer',messaging:'Messaging',infra:'Infrastructure',config:'Config',other:'Other' };
|
|
3584
|
+
|
|
3585
|
+
var topoSelectedId = null;
|
|
3586
|
+
|
|
3587
|
+
function setTopoSearch(q) {
|
|
3588
|
+
var el = document.getElementById('topo-search');
|
|
3589
|
+
if (el) { el.value = q; buildTopoList(q); }
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
function buildTopoList(filter) {
|
|
3593
|
+
var listEl = document.getElementById('topo-list');
|
|
3594
|
+
var q = (filter || '').toLowerCase();
|
|
3595
|
+
listEl.innerHTML = '';
|
|
3596
|
+
var sorted = TOPO.nodes.slice().sort(function(a, b) { return a.name.localeCompare(b.name); });
|
|
3597
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
3598
|
+
var d = sorted[i];
|
|
3599
|
+
if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
|
|
3600
|
+
var item = document.createElement('div');
|
|
3601
|
+
item.className = 'topo-item' + (d.id === topoSelectedId ? ' active' : '');
|
|
3602
|
+
item.dataset.id = d.id;
|
|
3603
|
+
var color = TYPE_COLORS[d.type] || '#aaa';
|
|
3604
|
+
item.innerHTML = '<span class="topo-dot" style="background:' + color + '"></span>' +
|
|
3605
|
+
'<span class="topo-name" title="' + d.id + '">' + d.name + '</span>' +
|
|
3606
|
+
'<span class="topo-type">' + d.type.replace(/_/g, ' ') + '</span>';
|
|
3607
|
+
(function(dd) { item.onclick = function() { selectTopoNode(dd); focusTopoNode(dd); }; })(d);
|
|
3608
|
+
listEl.appendChild(item);
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
document.getElementById('topo-search').addEventListener('input', function(e) { buildTopoList(e.target.value); });
|
|
3613
|
+
|
|
3614
|
+
var topoSidebar = document.getElementById('topo-sidebar');
|
|
3615
|
+
|
|
3616
|
+
function selectTopoNode(d) {
|
|
3617
|
+
topoSelectedId = d.id;
|
|
3618
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3619
|
+
showTopoNode(d);
|
|
3620
|
+
if (typeof d3 !== 'undefined') d3.selectAll('.node-hex').classed('selected', function(nd) { return nd.id === d.id; });
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
function showTopoNode(d) {
|
|
3624
|
+
var c = TYPE_COLORS[d.type] || '#aaa';
|
|
3625
|
+
var confPct = Math.round(d.confidence * 100);
|
|
3626
|
+
var tags = (d.tags || []).map(function(t) { return '<span class="tag">' + t + '</span>'; }).join('');
|
|
3627
|
+
var metaRows = Object.entries(d.metadata || {})
|
|
3628
|
+
.filter(function(kv) { return kv[1] !== null && kv[1] !== undefined && String(kv[1]).length > 0; })
|
|
3629
|
+
.map(function(kv) { return '<tr><td>' + kv[0] + '</td><td>' + JSON.stringify(kv[1]) + '</td></tr>'; }).join('');
|
|
3630
|
+
var related = TOPO.links.filter(function(l) {
|
|
3631
|
+
return (l.source.id || l.source) === d.id || (l.target.id || l.target) === d.id;
|
|
3632
|
+
});
|
|
3633
|
+
var edgeItems = related.map(function(l) {
|
|
3634
|
+
var isOut = (l.source.id || l.source) === d.id;
|
|
3635
|
+
var other = isOut ? (l.target.id || l.target) : (l.source.id || l.source);
|
|
3636
|
+
return '<div class="edge-item">' + (isOut ? '\\u2192' : '\\u2190') + ' <span>' + other + '</span> <small>[' + l.relationship + ']</small></div>';
|
|
3637
|
+
}).join('');
|
|
3638
|
+
|
|
3639
|
+
topoSidebar.innerHTML =
|
|
3640
|
+
'<h2>' + d.name + '</h2>' +
|
|
3641
|
+
'<table class="meta-table">' +
|
|
3642
|
+
'<tr><td>ID</td><td style="font-size:10px;word-break:break-all">' + d.id + '</td></tr>' +
|
|
3643
|
+
'<tr><td>Type</td><td><span style="color:' + c + '">' + d.type + '</span></td></tr>' +
|
|
3644
|
+
'<tr><td>Layer</td><td>' + d.layer + '</td></tr>' +
|
|
3645
|
+
'<tr><td>Confidence</td><td>' + confPct + '% <div class="conf-bar"><div class="conf-fill" style="width:' + confPct + '%;background:' + c + '"></div></div></td></tr>' +
|
|
3646
|
+
'<tr><td>Via</td><td>' + (d.discoveredVia || '\\u2014') + '</td></tr>' +
|
|
3647
|
+
'<tr><td>Timestamp</td><td>' + (d.discoveredAt ? d.discoveredAt.substring(0, 19).replace('T', ' ') : '\\u2014') + '</td></tr>' +
|
|
3648
|
+
(tags ? '<tr><td>Tags</td><td>' + tags + '</td></tr>' : '') +
|
|
3649
|
+
metaRows + '</table>' +
|
|
3650
|
+
(related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>' + edgeItems + '</div>' : '') +
|
|
3651
|
+
'<div style="margin-top:14px"><button class="export-btn" style="width:100%" onclick="deleteTopoNode(\\'' + d.id.replace(/'/g, "\\\\'") + '\\')">Delete node</button></div>';
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
function deleteTopoNode(id) {
|
|
3655
|
+
var idx = TOPO.nodes.findIndex(function(n) { return n.id === id; });
|
|
3656
|
+
if (idx === -1) return;
|
|
3657
|
+
TOPO.nodes.splice(idx, 1);
|
|
3658
|
+
TOPO.links = TOPO.links.filter(function(l) {
|
|
3659
|
+
return (l.source.id || l.source) !== id && (l.target.id || l.target) !== id;
|
|
3660
|
+
});
|
|
3661
|
+
topoSelectedId = null;
|
|
3662
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
|
|
3663
|
+
if (typeof rebuildTopoGraph === 'function') rebuildTopoGraph();
|
|
3664
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
function initTopology() {
|
|
3668
|
+
if (typeof d3 === 'undefined') return;
|
|
3669
|
+
|
|
3670
|
+
var svgEl = d3.select('#topo-graph svg');
|
|
3671
|
+
var graphDiv = document.getElementById('topo-graph');
|
|
3672
|
+
var gW = function() { return graphDiv.clientWidth; };
|
|
3673
|
+
var gH = function() { return graphDiv.clientHeight; };
|
|
3674
|
+
var g = svgEl.append('g');
|
|
3675
|
+
|
|
3676
|
+
svgEl.append('defs').append('marker')
|
|
3677
|
+
.attr('id', 'arrow').attr('viewBox', '0 0 10 6')
|
|
3678
|
+
.attr('refX', 10).attr('refY', 3)
|
|
3679
|
+
.attr('markerWidth', 8).attr('markerHeight', 6)
|
|
3680
|
+
.attr('orient', 'auto')
|
|
3681
|
+
.append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
|
|
3682
|
+
|
|
3683
|
+
var currentZoom = 1;
|
|
3684
|
+
var zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', function(e) {
|
|
3685
|
+
g.attr('transform', e.transform); currentZoom = e.transform.k; updateTopoLOD(currentZoom);
|
|
3686
|
+
});
|
|
3687
|
+
svgEl.call(zoomBehavior);
|
|
3688
|
+
|
|
3689
|
+
// Layer filters
|
|
3690
|
+
var layers = Array.from(new Set(TOPO.nodes.map(function(d) { return d.layer; })));
|
|
3691
|
+
var layerVisible = {};
|
|
3692
|
+
layers.forEach(function(l) { layerVisible[l] = true; });
|
|
3693
|
+
|
|
3694
|
+
var toolbarEl = document.getElementById('topo-toolbar');
|
|
3695
|
+
layers.forEach(function(layer) {
|
|
3696
|
+
var btn = document.createElement('button');
|
|
3697
|
+
btn.className = 'filter-btn';
|
|
3698
|
+
btn.innerHTML = '<span class="filter-dot" style="background:' + (LAYER_COLORS[layer] || '#666') + '"></span>' + (LAYER_NAMES[layer] || layer);
|
|
3699
|
+
btn.onclick = function() { layerVisible[layer] = !layerVisible[layer]; btn.classList.toggle('off', !layerVisible[layer]); updateTopoVisibility(); };
|
|
3700
|
+
toolbarEl.appendChild(btn);
|
|
3701
|
+
});
|
|
3702
|
+
|
|
3703
|
+
// JGF export button
|
|
3704
|
+
var jgfBtn = document.createElement('button');
|
|
3705
|
+
jgfBtn.className = 'export-btn'; jgfBtn.textContent = '\\u2193 JGF'; jgfBtn.title = 'Export JSON Graph Format';
|
|
3706
|
+
jgfBtn.onclick = function() {
|
|
3707
|
+
var jgf = { graph: { directed: true, type: 'cartography', label: 'Infrastructure Map',
|
|
3708
|
+
metadata: { exportedAt: new Date().toISOString() },
|
|
3709
|
+
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 } }]; })),
|
|
3710
|
+
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 } }; })
|
|
3711
|
+
}};
|
|
3712
|
+
var blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
|
|
3713
|
+
var url = URL.createObjectURL(blob);
|
|
3714
|
+
var a = document.createElement('a'); a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
|
|
3715
|
+
URL.revokeObjectURL(url);
|
|
3716
|
+
};
|
|
3717
|
+
toolbarEl.appendChild(jgfBtn);
|
|
3718
|
+
|
|
3719
|
+
// Hex helpers
|
|
3720
|
+
var T_HEX = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
|
|
3721
|
+
function tHexSize(d) { return T_HEX[d.type] || T_HEX.default; }
|
|
3722
|
+
function tHexPath(size) {
|
|
3723
|
+
var pts = [];
|
|
3724
|
+
for (var i = 0; i < 6; i++) {
|
|
3725
|
+
var angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
3726
|
+
pts.push([size * Math.cos(angle), size * Math.sin(angle)]);
|
|
3727
|
+
}
|
|
3728
|
+
return 'M' + pts.map(function(p) { return p.join(','); }).join('L') + 'Z';
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// Cluster force
|
|
3732
|
+
function clusterForce(alpha) {
|
|
3733
|
+
var centroids = {}, counts = {};
|
|
3734
|
+
TOPO.nodes.forEach(function(d) {
|
|
3735
|
+
if (!centroids[d.layer]) { centroids[d.layer] = { x: 0, y: 0 }; counts[d.layer] = 0; }
|
|
3736
|
+
centroids[d.layer].x += d.x || 0; centroids[d.layer].y += d.y || 0; counts[d.layer]++;
|
|
3737
|
+
});
|
|
3738
|
+
for (var l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
|
|
3739
|
+
var strength = alpha * 0.15;
|
|
3740
|
+
TOPO.nodes.forEach(function(d) {
|
|
3741
|
+
var cn = centroids[d.layer];
|
|
3742
|
+
if (cn) { d.vx += (cn.x - d.x) * strength; d.vy += (cn.y - d.y) * strength; }
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
// Hulls
|
|
3747
|
+
var hullGroup = g.append('g').attr('class', 'hulls');
|
|
3748
|
+
var hullPaths = {}, hullLabels = {};
|
|
3749
|
+
layers.forEach(function(layer) {
|
|
3750
|
+
hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
|
|
3751
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
|
|
3752
|
+
hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
|
|
3753
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
|
|
3754
|
+
});
|
|
3755
|
+
|
|
3756
|
+
function updateHulls() {
|
|
3757
|
+
layers.forEach(function(layer) {
|
|
3758
|
+
if (!layerVisible[layer]) { hullPaths[layer].attr('d', null); hullLabels[layer].attr('x', -9999); return; }
|
|
3759
|
+
var pts = TOPO.nodes.filter(function(d) { return d.layer === layer && layerVisible[d.layer]; }).map(function(d) { return [d.x, d.y]; });
|
|
3760
|
+
if (pts.length < 3) {
|
|
3761
|
+
hullPaths[layer].attr('d', null);
|
|
3762
|
+
if (pts.length > 0) hullLabels[layer].attr('x', pts[0][0]).attr('y', pts[0][1] - 30);
|
|
3763
|
+
else hullLabels[layer].attr('x', -9999);
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
var hull = d3.polygonHull(pts);
|
|
3767
|
+
if (!hull) { hullPaths[layer].attr('d', null); return; }
|
|
3768
|
+
var cx = d3.mean(hull, function(p) { return p[0]; });
|
|
3769
|
+
var cy = d3.mean(hull, function(p) { return p[1]; });
|
|
3770
|
+
var padded = hull.map(function(p) {
|
|
3771
|
+
var dx = p[0] - cx, dy = p[1] - cy;
|
|
3772
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
3773
|
+
return [p[0] + dx / len * 40, p[1] + dy / len * 40];
|
|
3774
|
+
});
|
|
3775
|
+
hullPaths[layer].attr('d', 'M' + padded.join('L') + 'Z');
|
|
3776
|
+
hullLabels[layer].attr('x', cx).attr('y', cy - d3.max(hull, function(p) { return Math.abs(p[1] - cy); }) - 30);
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// Graph
|
|
3781
|
+
var linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
|
|
3782
|
+
var linkGroup = g.append('g');
|
|
3783
|
+
var nodeGroup = g.append('g');
|
|
3784
|
+
|
|
3785
|
+
function focusTopoNode(d) {
|
|
3786
|
+
if (!d.x || !d.y) return;
|
|
3787
|
+
var w = gW(), h = gH();
|
|
3788
|
+
svgEl.transition().duration(500).call(
|
|
3789
|
+
zoomBehavior.transform,
|
|
3790
|
+
d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
window.focusTopoNode = focusTopoNode;
|
|
3794
|
+
|
|
3795
|
+
function rebuildTopoGraph() {
|
|
3796
|
+
if (sim) sim.stop();
|
|
3797
|
+
|
|
3798
|
+
linkSel = linkGroup.selectAll('line').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
3799
|
+
linkSel.exit().remove();
|
|
3800
|
+
var linkEnter = linkSel.enter().append('line').attr('class', 'link');
|
|
3801
|
+
linkSel = linkEnter.merge(linkSel)
|
|
3802
|
+
.attr('stroke', function(d) { return d.confidence < 0.6 ? '#2a2e35' : '#3d434b'; })
|
|
3803
|
+
.attr('stroke-dasharray', function(d) { return d.confidence < 0.6 ? '4 3' : null; })
|
|
3804
|
+
.attr('stroke-width', function(d) { return d.confidence < 0.6 ? 0.8 : 1.2; })
|
|
3805
|
+
.attr('marker-end', 'url(#arrow)');
|
|
3806
|
+
linkSel.select('title').remove();
|
|
3807
|
+
linkSel.append('title').text(function(d) { return d.relationship + ' (' + Math.round(d.confidence * 100) + '%)\\n' + (d.evidence || ''); });
|
|
3808
|
+
|
|
3809
|
+
linkLabelSel = linkGroup.selectAll('text').data(TOPO.links, function(d) { return (d.source.id || d.source) + '>' + (d.target.id || d.target); });
|
|
3810
|
+
linkLabelSel.exit().remove();
|
|
3811
|
+
linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel).text(function(d) { return d.relationship; });
|
|
3812
|
+
|
|
3813
|
+
nodeSel = nodeGroup.selectAll('g').data(TOPO.nodes, function(d) { return d.id; });
|
|
3814
|
+
nodeSel.exit().remove();
|
|
3815
|
+
var nodeEnter = nodeSel.enter().append('g')
|
|
3816
|
+
.call(d3.drag()
|
|
3817
|
+
.on('start', function(e, d) { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
3818
|
+
.on('drag', function(e, d) { d.fx = e.x; d.fy = e.y; })
|
|
3819
|
+
.on('end', function(e, d) { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
3820
|
+
)
|
|
3821
|
+
.on('click', function(e, d) { e.stopPropagation(); selectTopoNode(d); });
|
|
3822
|
+
nodeEnter.append('path').attr('class', 'node-hex');
|
|
3823
|
+
nodeEnter.append('title');
|
|
3824
|
+
nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
|
|
3825
|
+
|
|
3826
|
+
nodeSel = nodeEnter.merge(nodeSel);
|
|
3827
|
+
nodeSel.select('.node-hex')
|
|
3828
|
+
.attr('d', function(d) { return tHexPath(tHexSize(d)); })
|
|
3829
|
+
.attr('fill', function(d) { return TYPE_COLORS[d.type] || '#aaa'; })
|
|
3830
|
+
.attr('stroke', function(d) { var c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
|
|
3831
|
+
.attr('fill-opacity', function(d) { return 0.6 + d.confidence * 0.4; })
|
|
3832
|
+
.classed('selected', function(d) { return d.id === topoSelectedId; });
|
|
3833
|
+
nodeSel.select('title').text(function(d) { return d.name + ' (' + d.type + ')\\nconf: ' + Math.round(d.confidence * 100) + '%'; });
|
|
3834
|
+
nodeLabelSel = nodeSel.select('.node-label')
|
|
3835
|
+
.attr('dy', function(d) { return tHexSize(d) + 13; })
|
|
3836
|
+
.text(function(d) { return d.name.length > 20 ? d.name.substring(0, 18) + '\\u2026' : d.name; });
|
|
3837
|
+
|
|
3838
|
+
sim = d3.forceSimulation(TOPO.nodes)
|
|
3839
|
+
.force('link', d3.forceLink(TOPO.links).id(function(d) { return d.id; }).distance(function(d) { return d.relationship === 'contains' ? 50 : 100; }).strength(0.4))
|
|
3840
|
+
.force('charge', d3.forceManyBody().strength(-280))
|
|
3841
|
+
.force('center', d3.forceCenter(gW() / 2, gH() / 2))
|
|
3842
|
+
.force('collision', d3.forceCollide().radius(function(d) { return tHexSize(d) + 10; }))
|
|
3843
|
+
.force('cluster', clusterForce)
|
|
3844
|
+
.on('tick', function() {
|
|
3845
|
+
updateHulls();
|
|
3846
|
+
linkSel.attr('x1', function(d) { return d.source.x; }).attr('y1', function(d) { return d.source.y; })
|
|
3847
|
+
.attr('x2', function(d) { return d.target.x; }).attr('y2', function(d) { return d.target.y; });
|
|
3848
|
+
linkLabelSel.attr('x', function(d) { return (d.source.x + d.target.x) / 2; })
|
|
3849
|
+
.attr('y', function(d) { return (d.source.y + d.target.y) / 2 - 4; });
|
|
3850
|
+
nodeSel.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
window.rebuildTopoGraph = rebuildTopoGraph;
|
|
3854
|
+
|
|
3855
|
+
function updateTopoLOD(k) {
|
|
3856
|
+
if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
|
|
3857
|
+
if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
|
|
3858
|
+
d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
function updateTopoVisibility() {
|
|
3862
|
+
if (!nodeSel) return;
|
|
3863
|
+
nodeSel.style('display', function(d) { return layerVisible[d.layer] ? null : 'none'; });
|
|
3864
|
+
linkSel.style('display', function(d) {
|
|
3865
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
3866
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
3867
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
3868
|
+
});
|
|
3869
|
+
linkLabelSel.style('display', function(d) {
|
|
3870
|
+
var s = TOPO.nodes.find(function(n) { return n.id === (d.source.id || d.source); });
|
|
3871
|
+
var t = TOPO.nodes.find(function(n) { return n.id === (d.target.id || d.target); });
|
|
3872
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
rebuildTopoGraph();
|
|
3877
|
+
buildTopoList();
|
|
3878
|
+
updateTopoLOD(1);
|
|
3879
|
+
|
|
3880
|
+
svgEl.on('click', function() {
|
|
3881
|
+
topoSelectedId = null;
|
|
3882
|
+
d3.selectAll('.node-hex').classed('selected', false);
|
|
3883
|
+
buildTopoList(document.getElementById('topo-search').value);
|
|
3884
|
+
topoSidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// Init topology node list (non-D3 part)
|
|
3889
|
+
buildTopoList();
|
|
3890
|
+
</script>
|
|
3891
|
+
</body>
|
|
3892
|
+
</html>`;
|
|
3893
|
+
}
|
|
3894
|
+
function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "discovery", "sops"]) {
|
|
2814
3895
|
mkdirSync2(outputDir, { recursive: true });
|
|
2815
3896
|
mkdirSync2(join2(outputDir, "sops"), { recursive: true });
|
|
2816
3897
|
mkdirSync2(join2(outputDir, "workflows"), { recursive: true });
|
|
@@ -2837,6 +3918,10 @@ function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml
|
|
|
2837
3918
|
writeFileSync(join2(outputDir, "cartography-map.html"), exportCartographyMap(nodes, edges));
|
|
2838
3919
|
process.stderr.write("\u2713 cartography-map.html\n");
|
|
2839
3920
|
}
|
|
3921
|
+
if (formats.includes("discovery")) {
|
|
3922
|
+
writeFileSync(join2(outputDir, "discovery.html"), exportDiscoveryApp(nodes, edges));
|
|
3923
|
+
process.stderr.write("\u2713 discovery.html\n");
|
|
3924
|
+
}
|
|
2840
3925
|
if (formats.includes("sops")) {
|
|
2841
3926
|
const sops = db.getSOPs(sessionId);
|
|
2842
3927
|
for (const sop of sops) {
|
|
@@ -3082,8 +4167,13 @@ function main() {
|
|
|
3082
4167
|
const osc8 = (url, label) => `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
|
|
3083
4168
|
const htmlPath = resolve(config.outputDir, "topology.html");
|
|
3084
4169
|
const mapPath = resolve(config.outputDir, "cartography-map.html");
|
|
4170
|
+
const discoveryPath = resolve(config.outputDir, "discovery.html");
|
|
3085
4171
|
const topoPath = resolve(config.outputDir, "topology.mermaid");
|
|
3086
4172
|
w("\n");
|
|
4173
|
+
if (existsSync2(discoveryPath)) {
|
|
4174
|
+
w(` ${green("\u2192")} ${osc8(`file://${discoveryPath}`, bold("Open discovery.html"))} ${dim("\u2190 Enterprise Discovery Frontend")}
|
|
4175
|
+
`);
|
|
4176
|
+
}
|
|
3087
4177
|
if (existsSync2(mapPath)) {
|
|
3088
4178
|
w(` ${green("\u2192")} ${osc8(`file://${mapPath}`, bold("Open cartography-map.html"))} ${dim("\u2190 Hex Map")}
|
|
3089
4179
|
`);
|
|
@@ -3440,6 +4530,7 @@ ${infraSummary.substring(0, 12e3)}`;
|
|
|
3440
4530
|
out(dim(" catalog-info.yaml Backstage service catalog\n"));
|
|
3441
4531
|
out(dim(" topology.mermaid Infrastructure topology (graph TB)\n"));
|
|
3442
4532
|
out(dim(" dependencies.mermaid Service dependencies (graph LR)\n"));
|
|
4533
|
+
out(dim(" discovery.html Enterprise discovery frontend (Map + Topology)\n"));
|
|
3443
4534
|
out(dim(" topology.html Interactive D3.js force graph\n"));
|
|
3444
4535
|
out(dim(" cartography-map.html Hex grid data cartography map\n"));
|
|
3445
4536
|
out(dim(" sops/ Generated SOPs as Markdown\n"));
|