@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 CHANGED
@@ -2810,7 +2810,1088 @@ draw();
2810
2810
  </body>
2811
2811
  </html>`;
2812
2812
  }
2813
- function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "map", "sops"]) {
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 &middot; ${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" ? "&#9788;" : "&#9790;"}
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">&#128506;</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">&mdash;</h3>
3140
+ <button class="close-btn" id="md-close" aria-label="Close">&#10005;</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">&#127991;</button>
3146
+ <button class="tb-tool" id="btn-quality" title="Quality layer">&#128065;</button>
3147
+ <button class="tb-tool" id="btn-connect" title="Connection tool">&#128279;</button>
3148
+ </div>
3149
+ <div id="map-tb-right">
3150
+ <div class="map-zoom">
3151
+ <button class="zoom-btn" id="mz-out">&minus;</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>&nbsp;
3172
+ <span class="stats">${nodeCount} nodes &middot; ${edgeCount} edges</span><br/>
3173
+ <span class="zoom-level">Scroll = zoom &middot; Drag = pan &middot; 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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"));