@datasynx/agentic-ai-cartography 0.2.5 → 0.3.1
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/README.md +6 -59
- package/dist/{bookmarks-B2LUZQGG.js → bookmarks-ITLW7U5D.js} +2 -2
- package/dist/{chunk-MZUF7KGX.js → chunk-2VIAXA5T.js} +38 -12
- package/dist/chunk-2VIAXA5T.js.map +1 -0
- package/dist/{chunk-EVJP2FWQ.js → chunk-FFNOC6HF.js} +1 -33
- package/dist/chunk-FFNOC6HF.js.map +1 -0
- package/dist/cli.js +794 -989
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +24 -118
- package/dist/index.js +304 -307
- package/dist/index.js.map +1 -1
- package/dist/{types-NKF6BRMZ.js → types-ROE3Z6HY.js} +2 -8
- package/package.json +1 -2
- package/dist/chunk-EVJP2FWQ.js.map +0 -1
- package/dist/chunk-MZUF7KGX.js.map +0 -1
- package/dist/chunk-NEB52VYQ.js +0 -838
- package/dist/chunk-NEB52VYQ.js.map +0 -1
- package/dist/exporter-LRHXMSKN.js +0 -24
- package/dist/types-NKF6BRMZ.js.map +0 -1
- /package/dist/{bookmarks-B2LUZQGG.js.map → bookmarks-ITLW7U5D.js.map} +0 -0
- /package/dist/{exporter-LRHXMSKN.js.map → types-ROE3Z6HY.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ PRAGMA busy_timeout = 5000;
|
|
|
9
9
|
|
|
10
10
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
11
11
|
id TEXT PRIMARY KEY,
|
|
12
|
-
mode TEXT NOT NULL CHECK (mode IN ('discover'
|
|
12
|
+
mode TEXT NOT NULL CHECK (mode IN ('discover')),
|
|
13
13
|
started_at TEXT NOT NULL,
|
|
14
14
|
completed_at TEXT,
|
|
15
15
|
config TEXT NOT NULL DEFAULT '{}'
|
|
@@ -454,14 +454,6 @@ var EDGE_RELATIONSHIPS = [
|
|
|
454
454
|
"contains",
|
|
455
455
|
"depends_on"
|
|
456
456
|
];
|
|
457
|
-
var EVENT_TYPES = [
|
|
458
|
-
"process_start",
|
|
459
|
-
"process_end",
|
|
460
|
-
"connection_open",
|
|
461
|
-
"connection_close",
|
|
462
|
-
"window_focus",
|
|
463
|
-
"tool_switch"
|
|
464
|
-
];
|
|
465
457
|
var NodeSchema = z.object({
|
|
466
458
|
id: z.string().describe('Format: "{type}:{host}:{port}" oder "{type}:{name}"'),
|
|
467
459
|
type: z.enum(NODE_TYPES),
|
|
@@ -478,15 +470,6 @@ var EdgeSchema = z.object({
|
|
|
478
470
|
evidence: z.string(),
|
|
479
471
|
confidence: z.number().min(0).max(1).default(0.5)
|
|
480
472
|
});
|
|
481
|
-
var EventSchema = z.object({
|
|
482
|
-
eventType: z.enum(EVENT_TYPES),
|
|
483
|
-
process: z.string(),
|
|
484
|
-
pid: z.number(),
|
|
485
|
-
target: z.string().optional(),
|
|
486
|
-
targetType: z.enum(NODE_TYPES).optional(),
|
|
487
|
-
protocol: z.string().optional(),
|
|
488
|
-
port: z.number().optional()
|
|
489
|
-
});
|
|
490
473
|
var SOPStepSchema = z.object({
|
|
491
474
|
order: z.number(),
|
|
492
475
|
instruction: z.string(),
|
|
@@ -503,27 +486,15 @@ var SOPSchema = z.object({
|
|
|
503
486
|
frequency: z.string(),
|
|
504
487
|
confidence: z.number().min(0).max(1)
|
|
505
488
|
});
|
|
506
|
-
var MIN_POLL_INTERVAL_MS = 15e3;
|
|
507
489
|
function defaultConfig(overrides = {}) {
|
|
508
490
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
509
491
|
return {
|
|
510
|
-
mode: "discover",
|
|
511
492
|
maxDepth: 8,
|
|
512
493
|
maxTurns: 50,
|
|
513
494
|
entryPoints: ["localhost"],
|
|
514
495
|
agentModel: "claude-sonnet-4-5-20250929",
|
|
515
|
-
shadowMode: "daemon",
|
|
516
|
-
pollIntervalMs: 3e4,
|
|
517
|
-
inactivityTimeoutMs: 3e5,
|
|
518
|
-
promptTimeoutMs: 6e4,
|
|
519
|
-
trackWindowFocus: false,
|
|
520
|
-
autoSaveNodes: false,
|
|
521
|
-
enableNotifications: true,
|
|
522
|
-
shadowModel: "claude-haiku-4-5-20251001",
|
|
523
496
|
outputDir: "./cartography-output",
|
|
524
497
|
dbPath: `${home}/.cartography/cartography.db`,
|
|
525
|
-
socketPath: `${home}/.cartography/daemon.sock`,
|
|
526
|
-
pidFile: `${home}/.cartography/daemon.pid`,
|
|
527
498
|
verbose: false,
|
|
528
499
|
...overrides
|
|
529
500
|
};
|
|
@@ -693,24 +664,36 @@ function chromeLikeHistoryPaths(base) {
|
|
|
693
664
|
}
|
|
694
665
|
var CHROME_BASE = IS_MAC ? `${HOME}/Library/Application Support/Google/Chrome` : `${HOME}/.config/google-chrome`;
|
|
695
666
|
var CHROMIUM_BASE = IS_MAC ? `${HOME}/Library/Application Support/Chromium` : `${HOME}/.config/chromium`;
|
|
667
|
+
var CHROMIUM_SNAP_BASE = `${HOME}/snap/chromium/common/chromium`;
|
|
668
|
+
var CHROMIUM_FLATPAK_BASE = `${HOME}/.var/app/org.chromium.Chromium/config/chromium`;
|
|
669
|
+
var CHROME_FLATPAK_BASE = `${HOME}/.var/app/com.google.Chrome/config/google-chrome`;
|
|
670
|
+
var BRAVE_FLATPAK_BASE = `${HOME}/.var/app/com.brave.Browser/config/BraveSoftware/Brave-Browser`;
|
|
671
|
+
var EDGE_FLATPAK_BASE = `${HOME}/.var/app/com.microsoft.Edge/config/microsoft-edge`;
|
|
672
|
+
var FIREFOX_SNAP_BASE = `${HOME}/snap/firefox/common/.mozilla/firefox`;
|
|
673
|
+
var FIREFOX_FLATPAK_BASE = `${HOME}/.var/app/org.mozilla.firefox/.mozilla/firefox`;
|
|
696
674
|
var EDGE_BASE = IS_MAC ? `${HOME}/Library/Application Support/Microsoft Edge` : `${HOME}/.config/microsoft-edge`;
|
|
697
675
|
var BRAVE_BASE = IS_MAC ? `${HOME}/Library/Application Support/BraveSoftware/Brave-Browser` : `${HOME}/.config/BraveSoftware/Brave-Browser`;
|
|
698
676
|
var VIVALDI_BASE = IS_MAC ? `${HOME}/Library/Application Support/Vivaldi` : `${HOME}/.config/vivaldi`;
|
|
699
677
|
var OPERA_BASE = IS_MAC ? `${HOME}/Library/Application Support/com.operasoftware.Opera` : `${HOME}/.config/opera`;
|
|
700
678
|
function firefoxProfileDirs() {
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
679
|
+
const bases = IS_MAC ? [`${HOME}/Library/Application Support/Firefox/Profiles`] : [`${HOME}/.mozilla/firefox`, FIREFOX_SNAP_BASE, FIREFOX_FLATPAK_BASE];
|
|
680
|
+
const dirs = [];
|
|
681
|
+
for (const base of bases) {
|
|
682
|
+
if (!existsSync(base)) continue;
|
|
683
|
+
try {
|
|
684
|
+
for (const d of readdirSync(base)) {
|
|
685
|
+
const full = join(base, d);
|
|
686
|
+
try {
|
|
687
|
+
if (statSync(full).isDirectory() && existsSync(join(full, "places.sqlite"))) {
|
|
688
|
+
dirs.push(full);
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
709
692
|
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
return [];
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
713
695
|
}
|
|
696
|
+
return dirs;
|
|
714
697
|
}
|
|
715
698
|
async function scanAllBookmarks() {
|
|
716
699
|
const all = [];
|
|
@@ -720,6 +703,13 @@ async function scanAllBookmarks() {
|
|
|
720
703
|
for (const p of chromeLikePaths(BRAVE_BASE)) all.push(...readChromeLike(p, "brave"));
|
|
721
704
|
for (const p of chromeLikePaths(VIVALDI_BASE)) all.push(...readChromeLike(p, "vivaldi"));
|
|
722
705
|
for (const p of chromeLikePaths(OPERA_BASE)) all.push(...readChromeLike(p, "opera"));
|
|
706
|
+
if (!IS_MAC) {
|
|
707
|
+
for (const p of chromeLikePaths(CHROMIUM_SNAP_BASE)) all.push(...readChromeLike(p, "chromium-snap"));
|
|
708
|
+
for (const p of chromeLikePaths(CHROMIUM_FLATPAK_BASE)) all.push(...readChromeLike(p, "chromium-flatpak"));
|
|
709
|
+
for (const p of chromeLikePaths(CHROME_FLATPAK_BASE)) all.push(...readChromeLike(p, "chrome-flatpak"));
|
|
710
|
+
for (const p of chromeLikePaths(BRAVE_FLATPAK_BASE)) all.push(...readChromeLike(p, "brave-flatpak"));
|
|
711
|
+
for (const p of chromeLikePaths(EDGE_FLATPAK_BASE)) all.push(...readChromeLike(p, "edge-flatpak"));
|
|
712
|
+
}
|
|
723
713
|
for (const dir of firefoxProfileDirs()) {
|
|
724
714
|
all.push(...await readFirefoxBookmarks(dir));
|
|
725
715
|
}
|
|
@@ -738,6 +728,13 @@ async function scanAllHistory() {
|
|
|
738
728
|
for (const p of chromeLikeHistoryPaths(BRAVE_BASE)) all.push(...await readChromiumHistory(p, "brave"));
|
|
739
729
|
for (const p of chromeLikeHistoryPaths(VIVALDI_BASE)) all.push(...await readChromiumHistory(p, "vivaldi"));
|
|
740
730
|
for (const p of chromeLikeHistoryPaths(OPERA_BASE)) all.push(...await readChromiumHistory(p, "opera"));
|
|
731
|
+
if (!IS_MAC) {
|
|
732
|
+
for (const p of chromeLikeHistoryPaths(CHROMIUM_SNAP_BASE)) all.push(...await readChromiumHistory(p, "chromium-snap"));
|
|
733
|
+
for (const p of chromeLikeHistoryPaths(CHROMIUM_FLATPAK_BASE)) all.push(...await readChromiumHistory(p, "chromium-flatpak"));
|
|
734
|
+
for (const p of chromeLikeHistoryPaths(CHROME_FLATPAK_BASE)) all.push(...await readChromiumHistory(p, "chrome-flatpak"));
|
|
735
|
+
for (const p of chromeLikeHistoryPaths(BRAVE_FLATPAK_BASE)) all.push(...await readChromiumHistory(p, "brave-flatpak"));
|
|
736
|
+
for (const p of chromeLikeHistoryPaths(EDGE_FLATPAK_BASE)) all.push(...await readChromiumHistory(p, "edge-flatpak"));
|
|
737
|
+
}
|
|
741
738
|
for (const dir of firefoxProfileDirs()) {
|
|
742
739
|
all.push(...await readFirefoxHistory(dir));
|
|
743
740
|
}
|
|
@@ -803,24 +800,6 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
803
800
|
});
|
|
804
801
|
return { content: [{ type: "text", text: `\u2713 ${args["sourceId"]}\u2192${args["targetId"]}` }] };
|
|
805
802
|
}),
|
|
806
|
-
tool("save_event", "Save an activity event (process/connection observed)", {
|
|
807
|
-
eventType: z2.enum(EVENT_TYPES),
|
|
808
|
-
process: z2.string(),
|
|
809
|
-
pid: z2.number(),
|
|
810
|
-
target: z2.string().optional(),
|
|
811
|
-
targetType: z2.enum(NODE_TYPES).optional(),
|
|
812
|
-
port: z2.number().optional()
|
|
813
|
-
}, async (args) => {
|
|
814
|
-
db.insertEvent(sessionId, {
|
|
815
|
-
eventType: args["eventType"],
|
|
816
|
-
process: args["process"],
|
|
817
|
-
pid: args["pid"],
|
|
818
|
-
target: args["target"] ? stripSensitive(args["target"]) : void 0,
|
|
819
|
-
targetType: args["targetType"],
|
|
820
|
-
port: args["port"]
|
|
821
|
-
});
|
|
822
|
-
return { content: [{ type: "text", text: `\u2713 ${args["eventType"]}` }] };
|
|
823
|
-
}),
|
|
824
803
|
tool("get_catalog", "Get the current catalog \u2014 use before save_node to avoid duplicates", {
|
|
825
804
|
includeEdges: z2.boolean().default(true)
|
|
826
805
|
}, async (args) => {
|
|
@@ -836,22 +815,6 @@ async function createCartographyTools(db, sessionId, opts = {}) {
|
|
|
836
815
|
}]
|
|
837
816
|
};
|
|
838
817
|
}),
|
|
839
|
-
tool("manage_task", "Start, end or describe a workflow task", {
|
|
840
|
-
action: z2.enum(["start", "end", "describe"]),
|
|
841
|
-
description: z2.string().optional()
|
|
842
|
-
}, async (args) => {
|
|
843
|
-
const action = args["action"];
|
|
844
|
-
if (action === "start") {
|
|
845
|
-
const id = db.startTask(sessionId, args["description"]);
|
|
846
|
-
return { content: [{ type: "text", text: `\u2713 Task started: ${id}` }] };
|
|
847
|
-
}
|
|
848
|
-
if (action === "end") {
|
|
849
|
-
db.endCurrentTask(sessionId);
|
|
850
|
-
return { content: [{ type: "text", text: "\u2713 Task ended" }] };
|
|
851
|
-
}
|
|
852
|
-
db.updateTaskDescription(sessionId, args["description"]);
|
|
853
|
-
return { content: [{ type: "text", text: "\u2713 Description updated" }] };
|
|
854
|
-
}),
|
|
855
818
|
tool("ask_user", "Ask the user a question \u2014 for clarifications, missing context, or consent (e.g. before scanning browser history)", {
|
|
856
819
|
question: z2.string().describe("The question for the user (clear and specific)"),
|
|
857
820
|
context: z2.string().optional().describe("Optional context explaining why this is relevant")
|
|
@@ -1444,109 +1407,6 @@ Use ask_user when you need context from the user.`;
|
|
|
1444
1407
|
}
|
|
1445
1408
|
}
|
|
1446
1409
|
}
|
|
1447
|
-
async function runShadowCycle(config, db, sessionId, prevSnapshot, currSnapshot, onOutput) {
|
|
1448
|
-
const { query } = await import("@anthropic-ai/claude-code");
|
|
1449
|
-
const tools = await createCartographyTools(db, sessionId);
|
|
1450
|
-
const prompt = `Analyze the diff between these two system snapshots.
|
|
1451
|
-
Find:
|
|
1452
|
-
- New/closed TCP connections \u2192 save_event
|
|
1453
|
-
- New/terminated processes \u2192 save_event
|
|
1454
|
-
- Previously unknown services \u2192 check get_catalog, then save_node
|
|
1455
|
-
- Task boundaries (inactivity, tool switches) \u2192 manage_task
|
|
1456
|
-
target = host:port ONLY. Be concise and efficient.
|
|
1457
|
-
|
|
1458
|
-
=== BEFORE ===
|
|
1459
|
-
${prevSnapshot}
|
|
1460
|
-
|
|
1461
|
-
=== NOW ===
|
|
1462
|
-
${currSnapshot}`;
|
|
1463
|
-
for await (const msg of query({
|
|
1464
|
-
prompt,
|
|
1465
|
-
options: {
|
|
1466
|
-
model: config.shadowModel,
|
|
1467
|
-
maxTurns: 5,
|
|
1468
|
-
mcpServers: { cartography: tools },
|
|
1469
|
-
allowedTools: [
|
|
1470
|
-
"mcp__cartograph__save_event",
|
|
1471
|
-
"mcp__cartograph__save_node",
|
|
1472
|
-
"mcp__cartograph__save_edge",
|
|
1473
|
-
"mcp__cartograph__get_catalog",
|
|
1474
|
-
"mcp__cartograph__manage_task"
|
|
1475
|
-
],
|
|
1476
|
-
permissionMode: "bypassPermissions"
|
|
1477
|
-
}
|
|
1478
|
-
})) {
|
|
1479
|
-
if (onOutput) onOutput(msg);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
async function generateSOPs(db, sessionId) {
|
|
1483
|
-
const Anthropic = (await import("@anthropic-ai/sdk")).default;
|
|
1484
|
-
const client = new Anthropic();
|
|
1485
|
-
const tasks = db.getTasks(sessionId).filter((t) => t.status === "completed");
|
|
1486
|
-
if (tasks.length === 0) return 0;
|
|
1487
|
-
const clusters = clusterTasks(tasks);
|
|
1488
|
-
let generated = 0;
|
|
1489
|
-
for (const cluster of clusters) {
|
|
1490
|
-
const workflowId = crypto.randomUUID();
|
|
1491
|
-
const involved = JSON.parse(cluster[0]?.involvedServices ?? "[]");
|
|
1492
|
-
const taskDescriptions = cluster.map((t, i) => `Task ${i + 1}: ${t.description ?? "Unnamed"}
|
|
1493
|
-
Steps: ${t.steps}`).join("\n\n");
|
|
1494
|
-
const response = await client.messages.create({
|
|
1495
|
-
model: "claude-sonnet-4-5-20250929",
|
|
1496
|
-
max_tokens: 2048,
|
|
1497
|
-
messages: [{
|
|
1498
|
-
role: "user",
|
|
1499
|
-
content: `Generate a Standard Operating Procedure (SOP) for this recurring workflow.
|
|
1500
|
-
Reply ONLY with valid JSON in this format:
|
|
1501
|
-
{
|
|
1502
|
-
"title": "...",
|
|
1503
|
-
"description": "...",
|
|
1504
|
-
"steps": [{"order": 1, "instruction": "...", "tool": "...", "target": "...", "notes": "..."}],
|
|
1505
|
-
"involvedSystems": ["..."],
|
|
1506
|
-
"estimatedDuration": "~N minutes",
|
|
1507
|
-
"frequency": "X times daily",
|
|
1508
|
-
"confidence": 0.8
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
Tasks:
|
|
1512
|
-
${taskDescriptions}
|
|
1513
|
-
|
|
1514
|
-
Involved services: ${involved.join(", ")}`
|
|
1515
|
-
}]
|
|
1516
|
-
});
|
|
1517
|
-
const text = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
1518
|
-
try {
|
|
1519
|
-
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1520
|
-
if (!jsonMatch) continue;
|
|
1521
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
1522
|
-
db.insertSOP({ workflowId, ...parsed });
|
|
1523
|
-
generated++;
|
|
1524
|
-
} catch {
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
return generated;
|
|
1528
|
-
}
|
|
1529
|
-
function clusterTasks(tasks) {
|
|
1530
|
-
const clusters = [];
|
|
1531
|
-
const assigned = /* @__PURE__ */ new Set();
|
|
1532
|
-
for (const task of tasks) {
|
|
1533
|
-
if (assigned.has(task.id)) continue;
|
|
1534
|
-
const cluster = [task];
|
|
1535
|
-
assigned.add(task.id);
|
|
1536
|
-
const taskServices = new Set(JSON.parse(task.involvedServices ?? "[]"));
|
|
1537
|
-
for (const other of tasks) {
|
|
1538
|
-
if (assigned.has(other.id)) continue;
|
|
1539
|
-
const otherServices = new Set(JSON.parse(other.involvedServices ?? "[]"));
|
|
1540
|
-
const overlap = [...taskServices].filter((s) => otherServices.has(s));
|
|
1541
|
-
if (overlap.length > 0) {
|
|
1542
|
-
cluster.push(other);
|
|
1543
|
-
assigned.add(other.id);
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
clusters.push(cluster);
|
|
1547
|
-
}
|
|
1548
|
-
return clusters;
|
|
1549
|
-
}
|
|
1550
1410
|
|
|
1551
1411
|
// src/exporter.ts
|
|
1552
1412
|
import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
@@ -1783,7 +1643,36 @@ function exportHTML(nodes, edges) {
|
|
|
1783
1643
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
1784
1644
|
<style>
|
|
1785
1645
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1786
|
-
body { background: #0a0e14; color: #e6edf3; font-family: 'SF Mono','Fira Code','Cascadia Code',monospace; display: flex; overflow: hidden; }
|
|
1646
|
+
body { background: #0a0e14; color: #e6edf3; font-family: 'SF Mono','Fira Code','Cascadia Code',monospace; display: flex; overflow: hidden; height: 100vh; }
|
|
1647
|
+
|
|
1648
|
+
/* \u2500\u2500 Left node panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1649
|
+
#node-panel {
|
|
1650
|
+
width: 220px; min-width: 220px; height: 100vh; overflow: hidden;
|
|
1651
|
+
background: #0d1117; border-right: 1px solid #1b2028;
|
|
1652
|
+
display: flex; flex-direction: column;
|
|
1653
|
+
}
|
|
1654
|
+
#node-panel-header {
|
|
1655
|
+
padding: 10px 12px 8px; border-bottom: 1px solid #1b2028;
|
|
1656
|
+
font-size: 11px; color: #6e7681; text-transform: uppercase; letter-spacing: 0.6px;
|
|
1657
|
+
}
|
|
1658
|
+
#node-search {
|
|
1659
|
+
width: calc(100% - 16px); margin: 8px; padding: 5px 8px;
|
|
1660
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 5px;
|
|
1661
|
+
color: #e6edf3; font-size: 11px; font-family: inherit; outline: none;
|
|
1662
|
+
}
|
|
1663
|
+
#node-search:focus { border-color: #58a6ff; }
|
|
1664
|
+
#node-list { flex: 1; overflow-y: auto; padding-bottom: 8px; }
|
|
1665
|
+
.node-list-item {
|
|
1666
|
+
padding: 5px 12px; cursor: pointer; font-size: 11px;
|
|
1667
|
+
display: flex; align-items: center; gap: 6px; border-left: 2px solid transparent;
|
|
1668
|
+
}
|
|
1669
|
+
.node-list-item:hover { background: #161b22; }
|
|
1670
|
+
.node-list-item.active { background: #1a2436; border-left-color: #58a6ff; }
|
|
1671
|
+
.node-list-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }
|
|
1672
|
+
.node-list-name { color: #c9d1d9; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
1673
|
+
.node-list-type { color: #484f58; font-size: 9px; flex-shrink: 0; }
|
|
1674
|
+
|
|
1675
|
+
/* \u2500\u2500 Center graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1787
1676
|
#graph { flex: 1; height: 100vh; position: relative; }
|
|
1788
1677
|
svg { width: 100%; height: 100%; }
|
|
1789
1678
|
.hull { opacity: 0.12; stroke-width: 1.5; stroke-opacity: 0.25; }
|
|
@@ -1792,10 +1681,12 @@ function exportHTML(nodes, edges) {
|
|
|
1792
1681
|
.link-label { font-size: 8px; fill: #6e7681; pointer-events: none; opacity: 0; }
|
|
1793
1682
|
.node-hex { stroke-width: 1.8; cursor: pointer; transition: opacity 0.15s; }
|
|
1794
1683
|
.node-hex:hover { filter: brightness(1.3); stroke-width: 3; }
|
|
1684
|
+
.node-hex.selected { stroke-width: 3.5; filter: brightness(1.5); }
|
|
1795
1685
|
.node-label { font-size: 10px; fill: #c9d1d9; pointer-events: none; opacity: 0; }
|
|
1796
|
-
|
|
1686
|
+
|
|
1687
|
+
/* \u2500\u2500 Right sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1797
1688
|
#sidebar {
|
|
1798
|
-
width:
|
|
1689
|
+
width: 300px; min-width: 300px; height: 100vh; overflow-y: auto;
|
|
1799
1690
|
background: #0d1117; border-left: 1px solid #1b2028;
|
|
1800
1691
|
padding: 16px; font-size: 12px; line-height: 1.6;
|
|
1801
1692
|
}
|
|
@@ -1809,15 +1700,24 @@ function exportHTML(nodes, edges) {
|
|
|
1809
1700
|
#sidebar .edges-list { margin-top: 12px; }
|
|
1810
1701
|
#sidebar .edge-item { padding: 4px 0; border-bottom: 1px solid #161b22; color: #6e7681; font-size: 11px; }
|
|
1811
1702
|
#sidebar .edge-item span { color: #c9d1d9; }
|
|
1703
|
+
#sidebar .action-row { display: flex; gap: 6px; margin-top: 14px; }
|
|
1704
|
+
.btn-delete {
|
|
1705
|
+
flex: 1; padding: 6px 10px; background: transparent; border: 1px solid #6e191d;
|
|
1706
|
+
color: #f85149; border-radius: 5px; font-size: 11px; font-family: inherit;
|
|
1707
|
+
cursor: pointer; text-align: center;
|
|
1708
|
+
}
|
|
1709
|
+
.btn-delete:hover { background: #3d0c0c; }
|
|
1812
1710
|
.hint { color: #3d434b; font-size: 11px; margin-top: 8px; }
|
|
1813
|
-
|
|
1711
|
+
|
|
1712
|
+
/* \u2500\u2500 HUD \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1814
1713
|
#hud { position: absolute; top: 10px; left: 10px; background: rgba(10,14,20,0.88);
|
|
1815
1714
|
padding: 10px 14px; border-radius: 8px; font-size: 12px; border: 1px solid #1b2028; pointer-events: none; }
|
|
1816
1715
|
#hud strong { color: #58a6ff; }
|
|
1817
1716
|
#hud .stats { color: #6e7681; }
|
|
1818
1717
|
#hud .zoom-level { color: #3d434b; font-size: 10px; margin-top: 2px; }
|
|
1819
|
-
|
|
1820
|
-
|
|
1718
|
+
|
|
1719
|
+
/* \u2500\u2500 Toolbar (filters + JGF export) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1720
|
+
#toolbar { position: absolute; top: 10px; right: 10px; display: flex; flex-wrap: wrap; gap: 4px; pointer-events: auto; align-items: center; }
|
|
1821
1721
|
.filter-btn {
|
|
1822
1722
|
background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
|
|
1823
1723
|
color: #c9d1d9; padding: 4px 10px; font-size: 11px; cursor: pointer;
|
|
@@ -1826,22 +1726,40 @@ function exportHTML(nodes, edges) {
|
|
|
1826
1726
|
.filter-btn:hover { border-color: #30363d; }
|
|
1827
1727
|
.filter-btn.off { opacity: 0.35; }
|
|
1828
1728
|
.filter-dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
|
|
1729
|
+
.export-btn {
|
|
1730
|
+
background: rgba(10,14,20,0.85); border: 1px solid #1b2028; border-radius: 6px;
|
|
1731
|
+
color: #58a6ff; padding: 4px 12px; font-size: 11px; cursor: pointer;
|
|
1732
|
+
font-family: inherit;
|
|
1733
|
+
}
|
|
1734
|
+
.export-btn:hover { border-color: #58a6ff; background: rgba(88,166,255,0.08); }
|
|
1829
1735
|
</style>
|
|
1830
1736
|
</head>
|
|
1831
1737
|
<body>
|
|
1738
|
+
|
|
1739
|
+
<!-- Left: node list panel -->
|
|
1740
|
+
<div id="node-panel">
|
|
1741
|
+
<div id="node-panel-header">Nodes (${nodes.length})</div>
|
|
1742
|
+
<input id="node-search" type="text" placeholder="Search nodes\u2026" autocomplete="off" spellcheck="false">
|
|
1743
|
+
<div id="node-list"></div>
|
|
1744
|
+
</div>
|
|
1745
|
+
|
|
1746
|
+
<!-- Center: graph -->
|
|
1832
1747
|
<div id="graph">
|
|
1833
1748
|
<div id="hud">
|
|
1834
1749
|
<strong>Cartography</strong>
|
|
1835
|
-
<span class="stats">${nodes.length} nodes \xB7 ${edges.length} edges</span><br>
|
|
1750
|
+
<span class="stats" id="hud-stats">${nodes.length} nodes \xB7 ${edges.length} edges</span><br>
|
|
1836
1751
|
<span class="zoom-level">Scroll = zoom \xB7 Drag = pan \xB7 Click = details</span>
|
|
1837
1752
|
</div>
|
|
1838
|
-
<div id="
|
|
1753
|
+
<div id="toolbar"></div>
|
|
1839
1754
|
<svg></svg>
|
|
1840
1755
|
</div>
|
|
1756
|
+
|
|
1757
|
+
<!-- Right: detail sidebar -->
|
|
1841
1758
|
<div id="sidebar">
|
|
1842
1759
|
<h2>Infrastructure Map</h2>
|
|
1843
1760
|
<p class="hint">Click a node to view details.</p>
|
|
1844
1761
|
</div>
|
|
1762
|
+
|
|
1845
1763
|
<script>
|
|
1846
1764
|
const data = ${graphData};
|
|
1847
1765
|
|
|
@@ -1854,7 +1772,6 @@ const TYPE_COLORS = {
|
|
|
1854
1772
|
config_file: '#adb5bd', saas_tool: '#c084fc', table: '#f97316', unknown: '#6c757d',
|
|
1855
1773
|
};
|
|
1856
1774
|
|
|
1857
|
-
// \u2500\u2500 Color per layer (for hull backgrounds) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1858
1775
|
const LAYER_COLORS = {
|
|
1859
1776
|
saas: '#c084fc', web: '#6bcb77', data: '#ff6b6b',
|
|
1860
1777
|
messaging: '#c77dff', infra: '#4a9eff', config: '#adb5bd', other: '#6c757d',
|
|
@@ -1864,7 +1781,7 @@ const LAYER_NAMES = {
|
|
|
1864
1781
|
messaging: 'Messaging', infra: 'Infrastructure', config: 'Config', other: 'Other',
|
|
1865
1782
|
};
|
|
1866
1783
|
|
|
1867
|
-
// \u2500\u2500 Hexagon path
|
|
1784
|
+
// \u2500\u2500 Hexagon path \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1868
1785
|
const HEX_SIZE = { saas_tool: 16, host: 18, database_server: 18, k8s_cluster: 20, default: 14 };
|
|
1869
1786
|
function hexSize(d) { return HEX_SIZE[d.type] || HEX_SIZE.default; }
|
|
1870
1787
|
function hexPath(size) {
|
|
@@ -1876,9 +1793,42 @@ function hexPath(size) {
|
|
|
1876
1793
|
return 'M' + pts.map(p => p.join(',')).join('L') + 'Z';
|
|
1877
1794
|
}
|
|
1878
1795
|
|
|
1879
|
-
// \u2500\u2500
|
|
1796
|
+
// \u2500\u2500 Left panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1797
|
+
const nodeListEl = document.getElementById('node-list');
|
|
1798
|
+
const nodeSearchEl = document.getElementById('node-search');
|
|
1799
|
+
let selectedNodeId = null;
|
|
1800
|
+
|
|
1801
|
+
function buildNodeList(filter) {
|
|
1802
|
+
const q = (filter || '').toLowerCase();
|
|
1803
|
+
nodeListEl.innerHTML = '';
|
|
1804
|
+
const sorted = [...data.nodes].sort((a, b) => a.name.localeCompare(b.name));
|
|
1805
|
+
for (const d of sorted) {
|
|
1806
|
+
if (q && !d.name.toLowerCase().includes(q) && !d.type.includes(q) && !d.id.toLowerCase().includes(q)) continue;
|
|
1807
|
+
const item = document.createElement('div');
|
|
1808
|
+
item.className = 'node-list-item' + (d.id === selectedNodeId ? ' active' : '');
|
|
1809
|
+
item.dataset.id = d.id;
|
|
1810
|
+
const color = TYPE_COLORS[d.type] || '#aaa';
|
|
1811
|
+
item.innerHTML = \`<span class="node-list-dot" style="background:\${color}"></span>
|
|
1812
|
+
<span class="node-list-name" title="\${d.id}">\${d.name}</span>
|
|
1813
|
+
<span class="node-list-type">\${d.type.replace(/_/g,' ')}</span>\`;
|
|
1814
|
+
item.onclick = () => { selectNode(d); focusNode(d); };
|
|
1815
|
+
nodeListEl.appendChild(item);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
nodeSearchEl.addEventListener('input', e => buildNodeList(e.target.value));
|
|
1820
|
+
|
|
1821
|
+
// \u2500\u2500 Sidebar detail view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1880
1822
|
const sidebar = document.getElementById('sidebar');
|
|
1881
1823
|
|
|
1824
|
+
function selectNode(d) {
|
|
1825
|
+
selectedNodeId = d.id;
|
|
1826
|
+
buildNodeList(nodeSearchEl.value);
|
|
1827
|
+
showNode(d);
|
|
1828
|
+
// highlight hex
|
|
1829
|
+
d3.selectAll('.node-hex').classed('selected', nd => nd.id === d.id);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1882
1832
|
function showNode(d) {
|
|
1883
1833
|
const c = TYPE_COLORS[d.type] || '#aaa';
|
|
1884
1834
|
const confPct = Math.round(d.confidence * 100);
|
|
@@ -1912,9 +1862,28 @@ function showNode(d) {
|
|
|
1912
1862
|
\${metaRows}
|
|
1913
1863
|
</table>
|
|
1914
1864
|
\${related.length > 0 ? '<div class="edges-list"><strong>Connections (' + related.length + '):</strong>'+edgeItems+'</div>' : ''}
|
|
1865
|
+
<div class="action-row">
|
|
1866
|
+
<button class="btn-delete" onclick="deleteNode('\${d.id}')">\u{1F5D1} Delete node</button>
|
|
1867
|
+
</div>
|
|
1915
1868
|
\`;
|
|
1916
1869
|
}
|
|
1917
1870
|
|
|
1871
|
+
// \u2500\u2500 Delete node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1872
|
+
function deleteNode(id) {
|
|
1873
|
+
const idx = data.nodes.findIndex(n => n.id === id);
|
|
1874
|
+
if (idx === -1) return;
|
|
1875
|
+
data.nodes.splice(idx, 1);
|
|
1876
|
+
data.links = data.links.filter(l =>
|
|
1877
|
+
(l.source.id || l.source) !== id && (l.target.id || l.target) !== id
|
|
1878
|
+
);
|
|
1879
|
+
selectedNodeId = null;
|
|
1880
|
+
sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Node deleted.</p>';
|
|
1881
|
+
document.getElementById('hud-stats').textContent =
|
|
1882
|
+
data.nodes.length + ' nodes \xB7 ' + data.links.length + ' edges';
|
|
1883
|
+
rebuildGraph();
|
|
1884
|
+
buildNodeList(nodeSearchEl.value);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1918
1887
|
// \u2500\u2500 SVG setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1919
1888
|
const svgEl = d3.select('svg');
|
|
1920
1889
|
const graphDiv = document.getElementById('graph');
|
|
@@ -1922,7 +1891,6 @@ const W = () => graphDiv.clientWidth;
|
|
|
1922
1891
|
const H = () => graphDiv.clientHeight;
|
|
1923
1892
|
const g = svgEl.append('g');
|
|
1924
1893
|
|
|
1925
|
-
// Arrow marker for directed edges
|
|
1926
1894
|
svgEl.append('defs').append('marker')
|
|
1927
1895
|
.attr('id', 'arrow').attr('viewBox', '0 0 10 6')
|
|
1928
1896
|
.attr('refX', 10).attr('refY', 3)
|
|
@@ -1931,7 +1899,6 @@ svgEl.append('defs').append('marker')
|
|
|
1931
1899
|
.append('path').attr('d', 'M0,0 L10,3 L0,6 Z').attr('fill', '#555');
|
|
1932
1900
|
|
|
1933
1901
|
let currentZoom = 1;
|
|
1934
|
-
|
|
1935
1902
|
const zoomBehavior = d3.zoom().scaleExtent([0.08, 6]).on('zoom', e => {
|
|
1936
1903
|
g.attr('transform', e.transform);
|
|
1937
1904
|
currentZoom = e.transform.k;
|
|
@@ -1944,7 +1911,9 @@ const layers = [...new Set(data.nodes.map(d => d.layer))];
|
|
|
1944
1911
|
const layerVisible = {};
|
|
1945
1912
|
layers.forEach(l => layerVisible[l] = true);
|
|
1946
1913
|
|
|
1947
|
-
const
|
|
1914
|
+
const toolbarEl = document.getElementById('toolbar');
|
|
1915
|
+
|
|
1916
|
+
// Filter buttons
|
|
1948
1917
|
layers.forEach(layer => {
|
|
1949
1918
|
const btn = document.createElement('button');
|
|
1950
1919
|
btn.className = 'filter-btn';
|
|
@@ -1954,10 +1923,47 @@ layers.forEach(layer => {
|
|
|
1954
1923
|
btn.classList.toggle('off', !layerVisible[layer]);
|
|
1955
1924
|
updateVisibility();
|
|
1956
1925
|
};
|
|
1957
|
-
|
|
1926
|
+
toolbarEl.appendChild(btn);
|
|
1958
1927
|
});
|
|
1959
1928
|
|
|
1960
|
-
//
|
|
1929
|
+
// JGF export button
|
|
1930
|
+
const jgfBtn = document.createElement('button');
|
|
1931
|
+
jgfBtn.className = 'export-btn';
|
|
1932
|
+
jgfBtn.textContent = '\u2193 JGF';
|
|
1933
|
+
jgfBtn.title = 'Export JSON Graph Format';
|
|
1934
|
+
jgfBtn.onclick = exportJGF;
|
|
1935
|
+
toolbarEl.appendChild(jgfBtn);
|
|
1936
|
+
|
|
1937
|
+
// \u2500\u2500 JGF export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1938
|
+
function exportJGF() {
|
|
1939
|
+
const jgf = {
|
|
1940
|
+
graph: {
|
|
1941
|
+
directed: true,
|
|
1942
|
+
type: 'cartography',
|
|
1943
|
+
label: 'Infrastructure Map',
|
|
1944
|
+
metadata: { exportedAt: new Date().toISOString() },
|
|
1945
|
+
nodes: Object.fromEntries(data.nodes.map(n => [n.id, {
|
|
1946
|
+
label: n.name,
|
|
1947
|
+
metadata: { type: n.type, layer: n.layer, confidence: n.confidence,
|
|
1948
|
+
discoveredVia: n.discoveredVia, discoveredAt: n.discoveredAt,
|
|
1949
|
+
tags: n.tags, ...n.metadata }
|
|
1950
|
+
}])),
|
|
1951
|
+
edges: data.links.map(l => ({
|
|
1952
|
+
source: l.source.id || l.source,
|
|
1953
|
+
target: l.target.id || l.target,
|
|
1954
|
+
relation: l.relationship,
|
|
1955
|
+
metadata: { confidence: l.confidence, evidence: l.evidence }
|
|
1956
|
+
})),
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
const blob = new Blob([JSON.stringify(jgf, null, 2)], { type: 'application/json' });
|
|
1960
|
+
const url = URL.createObjectURL(blob);
|
|
1961
|
+
const a = document.createElement('a');
|
|
1962
|
+
a.href = url; a.download = 'cartography-graph.jgf.json'; a.click();
|
|
1963
|
+
URL.revokeObjectURL(url);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// \u2500\u2500 Cluster force \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1961
1967
|
function clusterForce(alpha) {
|
|
1962
1968
|
const centroids = {};
|
|
1963
1969
|
const counts = {};
|
|
@@ -1967,42 +1973,23 @@ function clusterForce(alpha) {
|
|
|
1967
1973
|
centroids[d.layer].y += d.y || 0;
|
|
1968
1974
|
counts[d.layer]++;
|
|
1969
1975
|
});
|
|
1970
|
-
for (const l in centroids) {
|
|
1971
|
-
centroids[l].x /= counts[l];
|
|
1972
|
-
centroids[l].y /= counts[l];
|
|
1973
|
-
}
|
|
1976
|
+
for (const l in centroids) { centroids[l].x /= counts[l]; centroids[l].y /= counts[l]; }
|
|
1974
1977
|
const strength = alpha * 0.15;
|
|
1975
1978
|
data.nodes.forEach(d => {
|
|
1976
1979
|
const c = centroids[d.layer];
|
|
1977
|
-
if (c) {
|
|
1978
|
-
d.vx += (c.x - d.x) * strength;
|
|
1979
|
-
d.vy += (c.y - d.y) * strength;
|
|
1980
|
-
}
|
|
1980
|
+
if (c) { d.vx += (c.x - d.x) * strength; d.vy += (c.y - d.y) * strength; }
|
|
1981
1981
|
});
|
|
1982
1982
|
}
|
|
1983
1983
|
|
|
1984
|
-
// \u2500\u2500
|
|
1985
|
-
const sim = d3.forceSimulation(data.nodes)
|
|
1986
|
-
.force('link', d3.forceLink(data.links).id(d => d.id).distance(d => d.relationship === 'contains' ? 50 : 100).strength(0.4))
|
|
1987
|
-
.force('charge', d3.forceManyBody().strength(-280))
|
|
1988
|
-
.force('center', d3.forceCenter(W() / 2, H() / 2))
|
|
1989
|
-
.force('collision', d3.forceCollide().radius(d => hexSize(d) + 10))
|
|
1990
|
-
.force('cluster', clusterForce);
|
|
1991
|
-
|
|
1992
|
-
// \u2500\u2500 Draw: hull backgrounds per layer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1984
|
+
// \u2500\u2500 Hull group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1993
1985
|
const hullGroup = g.append('g').attr('class', 'hulls');
|
|
1994
1986
|
const hullPaths = {};
|
|
1995
1987
|
const hullLabels = {};
|
|
1996
|
-
|
|
1997
1988
|
layers.forEach(layer => {
|
|
1998
|
-
hullPaths[layer] = hullGroup.append('path')
|
|
1999
|
-
.attr('
|
|
2000
|
-
|
|
2001
|
-
.attr('
|
|
2002
|
-
hullLabels[layer] = hullGroup.append('text')
|
|
2003
|
-
.attr('class', 'hull-label')
|
|
2004
|
-
.attr('fill', LAYER_COLORS[layer] || '#666')
|
|
2005
|
-
.text(LAYER_NAMES[layer] || layer);
|
|
1989
|
+
hullPaths[layer] = hullGroup.append('path').attr('class', 'hull')
|
|
1990
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').attr('stroke', LAYER_COLORS[layer] || '#666');
|
|
1991
|
+
hullLabels[layer] = hullGroup.append('text').attr('class', 'hull-label')
|
|
1992
|
+
.attr('fill', LAYER_COLORS[layer] || '#666').text(LAYER_NAMES[layer] || layer);
|
|
2006
1993
|
});
|
|
2007
1994
|
|
|
2008
1995
|
function updateHulls() {
|
|
@@ -2017,7 +2004,6 @@ function updateHulls() {
|
|
|
2017
2004
|
}
|
|
2018
2005
|
const hull = d3.polygonHull(pts);
|
|
2019
2006
|
if (!hull) { hullPaths[layer].attr('d', null); return; }
|
|
2020
|
-
// Pad the hull outward for organic island feel
|
|
2021
2007
|
const cx = d3.mean(hull, p => p[0]);
|
|
2022
2008
|
const cy = d3.mean(hull, p => p[1]);
|
|
2023
2009
|
const padded = hull.map(p => {
|
|
@@ -2030,94 +2016,119 @@ function updateHulls() {
|
|
|
2030
2016
|
});
|
|
2031
2017
|
}
|
|
2032
2018
|
|
|
2033
|
-
// \u2500\u2500
|
|
2019
|
+
// \u2500\u2500 Graph rendering (rebuildable after delete) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2020
|
+
let linkSel, linkLabelSel, nodeSel, nodeLabelSel, sim;
|
|
2034
2021
|
const linkGroup = g.append('g');
|
|
2035
|
-
const
|
|
2036
|
-
.attr('class', 'link')
|
|
2037
|
-
.attr('stroke', d => d.confidence < 0.6 ? '#2a2e35' : '#3d434b')
|
|
2038
|
-
.attr('stroke-dasharray', d => d.confidence < 0.6 ? '4 3' : null)
|
|
2039
|
-
.attr('stroke-width', d => d.confidence < 0.6 ? 0.8 : 1.2)
|
|
2040
|
-
.attr('marker-end', 'url(#arrow)');
|
|
2041
|
-
|
|
2042
|
-
link.append('title').text(d => \`\${d.relationship} (\${Math.round(d.confidence*100)}%)
|
|
2043
|
-
\${d.evidence||''}\`);
|
|
2022
|
+
const nodeGroup = g.append('g');
|
|
2044
2023
|
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
.
|
|
2024
|
+
function focusNode(d) {
|
|
2025
|
+
if (!d.x || !d.y) return;
|
|
2026
|
+
const w = W(), h = H();
|
|
2027
|
+
svgEl.transition().duration(500).call(
|
|
2028
|
+
zoomBehavior.transform,
|
|
2029
|
+
d3.zoomIdentity.translate(w / 2, h / 2).scale(Math.min(3, currentZoom < 1 ? 1.5 : currentZoom)).translate(-d.x, -d.y)
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2049
2032
|
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
)
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
.
|
|
2063
|
-
.
|
|
2064
|
-
|
|
2065
|
-
const c = d3.color(TYPE_COLORS[d.type] || '#aaa');
|
|
2066
|
-
return c ? c.brighter(0.8).formatHex() : '#ccc';
|
|
2067
|
-
})
|
|
2068
|
-
.attr('fill-opacity', d => 0.6 + d.confidence * 0.4);
|
|
2069
|
-
|
|
2070
|
-
node.append('title').text(d => \`\${d.name} (\${d.type})
|
|
2071
|
-
conf: \${Math.round(d.confidence*100)}%\`);
|
|
2033
|
+
function rebuildGraph() {
|
|
2034
|
+
if (sim) sim.stop();
|
|
2035
|
+
|
|
2036
|
+
// Links
|
|
2037
|
+
linkSel = linkGroup.selectAll('line').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
|
|
2038
|
+
linkSel.exit().remove();
|
|
2039
|
+
const linkEnter = linkSel.enter().append('line').attr('class', 'link');
|
|
2040
|
+
linkSel = linkEnter.merge(linkSel)
|
|
2041
|
+
.attr('stroke', d => d.confidence < 0.6 ? '#2a2e35' : '#3d434b')
|
|
2042
|
+
.attr('stroke-dasharray', d => d.confidence < 0.6 ? '4 3' : null)
|
|
2043
|
+
.attr('stroke-width', d => d.confidence < 0.6 ? 0.8 : 1.2)
|
|
2044
|
+
.attr('marker-end', 'url(#arrow)');
|
|
2045
|
+
linkSel.select('title').remove();
|
|
2046
|
+
linkSel.append('title').text(d => \`\${d.relationship} (\${Math.round(d.confidence*100)}%)
|
|
2047
|
+
\${d.evidence||''}\`);
|
|
2072
2048
|
|
|
2073
|
-
//
|
|
2074
|
-
|
|
2075
|
-
.
|
|
2076
|
-
.attr('
|
|
2077
|
-
|
|
2078
|
-
|
|
2049
|
+
// Link labels
|
|
2050
|
+
linkLabelSel = linkGroup.selectAll('text').data(data.links, d => \`\${d.source.id||d.source}>\${d.target.id||d.target}\`);
|
|
2051
|
+
linkLabelSel.exit().remove();
|
|
2052
|
+
linkLabelSel = linkLabelSel.enter().append('text').attr('class', 'link-label').merge(linkLabelSel)
|
|
2053
|
+
.text(d => d.relationship);
|
|
2054
|
+
|
|
2055
|
+
// Nodes
|
|
2056
|
+
nodeSel = nodeGroup.selectAll('g').data(data.nodes, d => d.id);
|
|
2057
|
+
nodeSel.exit().remove();
|
|
2058
|
+
const nodeEnter = nodeSel.enter().append('g')
|
|
2059
|
+
.call(d3.drag()
|
|
2060
|
+
.on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
2061
|
+
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
2062
|
+
.on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
2063
|
+
)
|
|
2064
|
+
.on('click', (e, d) => { e.stopPropagation(); selectNode(d); });
|
|
2065
|
+
nodeEnter.append('path').attr('class', 'node-hex');
|
|
2066
|
+
nodeEnter.append('title');
|
|
2067
|
+
nodeEnter.append('text').attr('class', 'node-label').attr('text-anchor', 'middle');
|
|
2068
|
+
|
|
2069
|
+
nodeSel = nodeEnter.merge(nodeSel);
|
|
2070
|
+
nodeSel.select('.node-hex')
|
|
2071
|
+
.attr('d', d => hexPath(hexSize(d)))
|
|
2072
|
+
.attr('fill', d => TYPE_COLORS[d.type] || '#aaa')
|
|
2073
|
+
.attr('stroke', d => { const c = d3.color(TYPE_COLORS[d.type] || '#aaa'); return c ? c.brighter(0.8).formatHex() : '#ccc'; })
|
|
2074
|
+
.attr('fill-opacity', d => 0.6 + d.confidence * 0.4)
|
|
2075
|
+
.classed('selected', d => d.id === selectedNodeId);
|
|
2076
|
+
nodeSel.select('title').text(d => \`\${d.name} (\${d.type})
|
|
2077
|
+
conf: \${Math.round(d.confidence*100)}%\`);
|
|
2078
|
+
nodeLabelSel = nodeSel.select('.node-label')
|
|
2079
|
+
.attr('dy', d => hexSize(d) + 13)
|
|
2080
|
+
.text(d => d.name.length > 20 ? d.name.substring(0, 18) + '\u2026' : d.name);
|
|
2081
|
+
|
|
2082
|
+
// Simulation
|
|
2083
|
+
sim = d3.forceSimulation(data.nodes)
|
|
2084
|
+
.force('link', d3.forceLink(data.links).id(d => d.id).distance(d => d.relationship === 'contains' ? 50 : 100).strength(0.4))
|
|
2085
|
+
.force('charge', d3.forceManyBody().strength(-280))
|
|
2086
|
+
.force('center', d3.forceCenter(W() / 2, H() / 2))
|
|
2087
|
+
.force('collision', d3.forceCollide().radius(d => hexSize(d) + 10))
|
|
2088
|
+
.force('cluster', clusterForce)
|
|
2089
|
+
.on('tick', () => {
|
|
2090
|
+
updateHulls();
|
|
2091
|
+
linkSel.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
2092
|
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
2093
|
+
linkLabelSel.attr('x', d => (d.source.x + d.target.x) / 2)
|
|
2094
|
+
.attr('y', d => (d.source.y + d.target.y) / 2 - 4);
|
|
2095
|
+
nodeSel.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2079
2098
|
|
|
2080
|
-
// \u2500\u2500
|
|
2099
|
+
// \u2500\u2500 LOD & visibility \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2081
2100
|
function updateLOD(k) {
|
|
2082
|
-
|
|
2083
|
-
|
|
2101
|
+
if (nodeLabelSel) nodeLabelSel.style('opacity', k > 0.5 ? Math.min(1, (k - 0.5) * 2) : 0);
|
|
2102
|
+
if (linkLabelSel) linkLabelSel.style('opacity', k > 1.2 ? Math.min(1, (k - 1.2) * 3) : 0);
|
|
2084
2103
|
d3.selectAll('.hull-label').style('font-size', k < 0.4 ? '18px' : '13px');
|
|
2085
2104
|
}
|
|
2086
2105
|
|
|
2087
|
-
// \u2500\u2500 Visibility filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2088
2106
|
function updateVisibility() {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2107
|
+
if (!nodeSel) return;
|
|
2108
|
+
nodeSel.style('display', d => layerVisible[d.layer] ? null : 'none');
|
|
2109
|
+
linkSel.style('display', d => {
|
|
2110
|
+
const s = data.nodes.find(n => n.id === (d.source.id||d.source));
|
|
2111
|
+
const t = data.nodes.find(n => n.id === (d.target.id||d.target));
|
|
2112
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2094
2113
|
});
|
|
2095
|
-
|
|
2096
|
-
const
|
|
2097
|
-
const
|
|
2098
|
-
return (
|
|
2114
|
+
linkLabelSel.style('display', d => {
|
|
2115
|
+
const s = data.nodes.find(n => n.id === (d.source.id||d.source));
|
|
2116
|
+
const t = data.nodes.find(n => n.id === (d.target.id||d.target));
|
|
2117
|
+
return (s && layerVisible[s.layer]) && (t && layerVisible[t.layer]) ? null : 'none';
|
|
2099
2118
|
});
|
|
2100
2119
|
}
|
|
2101
2120
|
|
|
2102
|
-
// \u2500\u2500
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
2107
|
-
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
2108
|
-
linkLabel
|
|
2109
|
-
.attr('x', d => (d.source.x + d.target.x) / 2)
|
|
2110
|
-
.attr('y', d => (d.source.y + d.target.y) / 2 - 4);
|
|
2111
|
-
node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
|
|
2112
|
-
});
|
|
2121
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2122
|
+
rebuildGraph();
|
|
2123
|
+
buildNodeList();
|
|
2124
|
+
updateLOD(1);
|
|
2113
2125
|
|
|
2114
|
-
// Click empty space to deselect
|
|
2115
2126
|
svgEl.on('click', () => {
|
|
2127
|
+
selectedNodeId = null;
|
|
2128
|
+
d3.selectAll('.node-hex').classed('selected', false);
|
|
2129
|
+
buildNodeList(nodeSearchEl.value);
|
|
2116
2130
|
sidebar.innerHTML = '<h2>Infrastructure Map</h2><p class="hint">Click a node to view details.</p>';
|
|
2117
2131
|
});
|
|
2118
|
-
|
|
2119
|
-
// Initial LOD
|
|
2120
|
-
updateLOD(1);
|
|
2121
2132
|
</script>
|
|
2122
2133
|
</body>
|
|
2123
2134
|
</html>`;
|
|
@@ -2251,7 +2262,7 @@ function exportSOPDashboard(sops) {
|
|
|
2251
2262
|
<body>
|
|
2252
2263
|
<div class="header">
|
|
2253
2264
|
<h1>SOP Dashboard</h1>
|
|
2254
|
-
<div class="subtitle">
|
|
2265
|
+
<div class="subtitle">Cartography \u2014 Standard Operating Procedures</div>
|
|
2255
2266
|
<div class="stats-row">
|
|
2256
2267
|
<div class="stat-card"><div class="value" id="sop-count">0</div><div class="label">SOPs</div></div>
|
|
2257
2268
|
<div class="stat-card"><div class="value" id="step-count">0</div><div class="label">Total Steps</div></div>
|
|
@@ -2288,7 +2299,7 @@ systems.forEach(([name, count]) => {
|
|
|
2288
2299
|
|
|
2289
2300
|
const listDiv = document.getElementById('sop-list');
|
|
2290
2301
|
if (sops.length === 0) {
|
|
2291
|
-
listDiv.innerHTML = '<div class="empty">No SOPs found.
|
|
2302
|
+
listDiv.innerHTML = '<div class="empty">No SOPs found. Run a discovery session first.</div>';
|
|
2292
2303
|
}
|
|
2293
2304
|
|
|
2294
2305
|
sops.forEach((sop, i) => {
|
|
@@ -2408,20 +2419,8 @@ function checkPrerequisites() {
|
|
|
2408
2419
|
process.stderr.write("\u2713 Eingeloggt via claude login (Subscription)\n");
|
|
2409
2420
|
}
|
|
2410
2421
|
}
|
|
2411
|
-
function checkPollInterval(intervalMs) {
|
|
2412
|
-
if (intervalMs < MIN_POLL_INTERVAL_MS) {
|
|
2413
|
-
process.stderr.write(
|
|
2414
|
-
`\u26A0 Minimum Shadow-Intervall: ${MIN_POLL_INTERVAL_MS / 1e3} Sekunden (Agent SDK Overhead)
|
|
2415
|
-
`
|
|
2416
|
-
);
|
|
2417
|
-
return MIN_POLL_INTERVAL_MS;
|
|
2418
|
-
}
|
|
2419
|
-
return intervalMs;
|
|
2420
|
-
}
|
|
2421
2422
|
export {
|
|
2422
2423
|
CartographyDB,
|
|
2423
|
-
MIN_POLL_INTERVAL_MS,
|
|
2424
|
-
checkPollInterval,
|
|
2425
2424
|
checkPrerequisites,
|
|
2426
2425
|
createCartographyTools,
|
|
2427
2426
|
CartographyDB as default,
|
|
@@ -2433,11 +2432,9 @@ export {
|
|
|
2433
2432
|
exportSOPDashboard,
|
|
2434
2433
|
exportSOPMarkdown,
|
|
2435
2434
|
generateDependencyMermaid,
|
|
2436
|
-
generateSOPs,
|
|
2437
2435
|
generateTopologyMermaid,
|
|
2438
2436
|
generateWorkflowMermaid,
|
|
2439
2437
|
runDiscovery,
|
|
2440
|
-
runShadowCycle,
|
|
2441
2438
|
safetyHook,
|
|
2442
2439
|
stripSensitive
|
|
2443
2440
|
};
|