@datasynx/agentic-ai-cartography 2.7.0 → 2.9.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/api-bin.js +1 -1
- package/dist/{chunk-HLWNO3RF.js → chunk-LRUWWHMQ.js} +174 -15
- package/dist/chunk-LRUWWHMQ.js.map +1 -0
- package/dist/{chunk-TBPGFEMQ.js → chunk-W4Q3TXHR.js} +162 -2
- package/dist/chunk-W4Q3TXHR.js.map +1 -0
- package/dist/cli.js +18 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +322 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -10
- package/dist/index.d.ts +124 -10
- package/dist/index.js +319 -13
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +1 -1
- package/package.json +3 -2
- package/server.json +2 -2
- package/dist/chunk-HLWNO3RF.js.map +0 -1
- package/dist/chunk-TBPGFEMQ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -4362,6 +4362,148 @@ var SqliteStoreBackend = class {
|
|
|
4362
4362
|
}
|
|
4363
4363
|
};
|
|
4364
4364
|
|
|
4365
|
+
// src/store/graph.ts
|
|
4366
|
+
function toNum(v) {
|
|
4367
|
+
if (typeof v === "number") return v;
|
|
4368
|
+
if (v && typeof v === "object" && "toNumber" in v && typeof v.toNumber === "function") {
|
|
4369
|
+
return v.toNumber();
|
|
4370
|
+
}
|
|
4371
|
+
return Number(v ?? 0);
|
|
4372
|
+
}
|
|
4373
|
+
var GraphStoreBackend = class {
|
|
4374
|
+
constructor(driver) {
|
|
4375
|
+
this.driver = driver;
|
|
4376
|
+
}
|
|
4377
|
+
async run(cypher, params) {
|
|
4378
|
+
const session = this.driver.session();
|
|
4379
|
+
try {
|
|
4380
|
+
return await session.run(cypher, params);
|
|
4381
|
+
} finally {
|
|
4382
|
+
await session.close();
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
async upsertNode(org, node, identity, contributor) {
|
|
4386
|
+
const res = await this.run(
|
|
4387
|
+
`MERGE (n:Node {org: $org, globalId: $globalId})
|
|
4388
|
+
ON CREATE SET n._created = true
|
|
4389
|
+
ON MATCH SET n._created = false
|
|
4390
|
+
SET n.id = $id, n.contentHash = $contentHash, n.type = $type, n.name = $name,
|
|
4391
|
+
n.domain = $domain, n.owner = $owner,
|
|
4392
|
+
n.confidence = CASE WHEN n.confidence IS NULL OR $confidence > n.confidence THEN $confidence ELSE n.confidence END
|
|
4393
|
+
MERGE (c:Contributor {org: $org, globalId: $globalId, machineId: $machineId})
|
|
4394
|
+
SET c.hostname = $hostname, c.user = $user, c.at = $at,
|
|
4395
|
+
c.confidence = CASE WHEN c.confidence IS NULL OR $contribConfidence > c.confidence THEN $contribConfidence ELSE c.confidence END
|
|
4396
|
+
RETURN n._created AS created`,
|
|
4397
|
+
{
|
|
4398
|
+
org,
|
|
4399
|
+
globalId: identity.globalId,
|
|
4400
|
+
contentHash: identity.contentHash,
|
|
4401
|
+
id: node.id,
|
|
4402
|
+
type: node.type,
|
|
4403
|
+
name: node.name,
|
|
4404
|
+
domain: node.domain ?? null,
|
|
4405
|
+
owner: node.owner ?? null,
|
|
4406
|
+
confidence: node.confidence,
|
|
4407
|
+
machineId: contributor.machineId,
|
|
4408
|
+
hostname: contributor.hostname,
|
|
4409
|
+
user: contributor.user,
|
|
4410
|
+
at: contributor.at,
|
|
4411
|
+
contribConfidence: contributor.confidence
|
|
4412
|
+
}
|
|
4413
|
+
);
|
|
4414
|
+
return res.records[0]?.get("created") === true ? "created" : "merged";
|
|
4415
|
+
}
|
|
4416
|
+
async insertEdge(org, edge) {
|
|
4417
|
+
await this.run(
|
|
4418
|
+
`MATCH (s:Node {org: $org, id: $source})
|
|
4419
|
+
MATCH (t:Node {org: $org, id: $target})
|
|
4420
|
+
MERGE (s)-[r:DEPENDS {relationship: $rel}]->(t)
|
|
4421
|
+
SET r.evidence = $evidence, r.confidence = $confidence`,
|
|
4422
|
+
{ org, source: edge.sourceId, target: edge.targetId, rel: edge.relationship, evidence: edge.evidence, confidence: edge.confidence }
|
|
4423
|
+
);
|
|
4424
|
+
}
|
|
4425
|
+
async getSummary(org) {
|
|
4426
|
+
const totals = await this.run(
|
|
4427
|
+
`MATCH (n:Node {org: $org})
|
|
4428
|
+
OPTIONAL MATCH (n)-[r:DEPENDS]->(:Node {org: $org})
|
|
4429
|
+
RETURN count(DISTINCT n) AS nodes, count(r) AS edges`,
|
|
4430
|
+
{ org }
|
|
4431
|
+
);
|
|
4432
|
+
const byType = await this.run(`MATCH (n:Node {org: $org}) RETURN n.type AS k, count(*) AS c`, { org });
|
|
4433
|
+
const byDomain = await this.run(`MATCH (n:Node {org: $org}) RETURN coalesce(n.domain, '(none)') AS k, count(*) AS c`, { org });
|
|
4434
|
+
const byRel = await this.run(`MATCH (:Node {org: $org})-[r:DEPENDS]->(:Node {org: $org}) RETURN r.relationship AS k, count(*) AS c`, { org });
|
|
4435
|
+
const top = await this.run(
|
|
4436
|
+
`MATCH (n:Node {org: $org})
|
|
4437
|
+
OPTIONAL MATCH (n)-[r:DEPENDS]-(:Node {org: $org})
|
|
4438
|
+
RETURN n.id AS id, n.name AS name, n.type AS type, count(r) AS degree
|
|
4439
|
+
ORDER BY degree DESC, id ASC LIMIT 10`,
|
|
4440
|
+
{ org }
|
|
4441
|
+
);
|
|
4442
|
+
const contrib = await this.run(`MATCH (c:Contributor {org: $org}) RETURN count(DISTINCT c.machineId) AS contributors`, { org });
|
|
4443
|
+
const counts = (r) => {
|
|
4444
|
+
const out = {};
|
|
4445
|
+
for (const rec of r.records) out[String(rec.get("k"))] = toNum(rec.get("c"));
|
|
4446
|
+
return out;
|
|
4447
|
+
};
|
|
4448
|
+
return {
|
|
4449
|
+
org,
|
|
4450
|
+
totals: { nodes: toNum(totals.records[0]?.get("nodes")), edges: toNum(totals.records[0]?.get("edges")) },
|
|
4451
|
+
nodesByType: counts(byType),
|
|
4452
|
+
nodesByDomain: counts(byDomain),
|
|
4453
|
+
edgesByRelationship: counts(byRel),
|
|
4454
|
+
topConnected: top.records.map((rec) => ({
|
|
4455
|
+
id: String(rec.get("id")),
|
|
4456
|
+
name: String(rec.get("name")),
|
|
4457
|
+
type: String(rec.get("type")),
|
|
4458
|
+
degree: toNum(rec.get("degree"))
|
|
4459
|
+
})),
|
|
4460
|
+
contributors: toNum(contrib.records[0]?.get("contributors"))
|
|
4461
|
+
};
|
|
4462
|
+
}
|
|
4463
|
+
async getContributors(globalId2) {
|
|
4464
|
+
const res = await this.run(
|
|
4465
|
+
`MATCH (c:Contributor {globalId: $globalId})
|
|
4466
|
+
RETURN c.machineId AS machineId, c.hostname AS hostname, c.user AS user, c.org AS org, c.at AS at, c.confidence AS confidence`,
|
|
4467
|
+
{ globalId: globalId2 }
|
|
4468
|
+
);
|
|
4469
|
+
return res.records.map((rec) => ({
|
|
4470
|
+
machineId: String(rec.get("machineId")),
|
|
4471
|
+
hostname: String(rec.get("hostname")),
|
|
4472
|
+
user: String(rec.get("user")),
|
|
4473
|
+
organization: rec.get("org") != null ? String(rec.get("org")) : void 0,
|
|
4474
|
+
at: String(rec.get("at")),
|
|
4475
|
+
confidence: toNum(rec.get("confidence"))
|
|
4476
|
+
}));
|
|
4477
|
+
}
|
|
4478
|
+
async close() {
|
|
4479
|
+
await this.driver.close();
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
|
|
4483
|
+
// src/store/index.ts
|
|
4484
|
+
async function defaultNeo4jDriver(url, user, password) {
|
|
4485
|
+
const mod = await import("neo4j-driver");
|
|
4486
|
+
return mod.default.driver(url, mod.default.auth.basic(user, password));
|
|
4487
|
+
}
|
|
4488
|
+
async function openStoreBackend(db, opts = {}) {
|
|
4489
|
+
if (opts.backend === "graph" && opts.graphUrl) {
|
|
4490
|
+
try {
|
|
4491
|
+
const make = opts.driverFactory ?? defaultNeo4jDriver;
|
|
4492
|
+
const driver = await make(opts.graphUrl, opts.graphUser ?? "neo4j", opts.graphPassword ?? "");
|
|
4493
|
+
if (driver.verifyConnectivity) await driver.verifyConnectivity();
|
|
4494
|
+
logInfo("central store: graph backend active", { host: stripSensitive(opts.graphUrl) });
|
|
4495
|
+
return new GraphStoreBackend(driver);
|
|
4496
|
+
} catch (err) {
|
|
4497
|
+
logWarn("central store: graph backend unavailable \u2014 falling back to SQLite", {
|
|
4498
|
+
host: stripSensitive(opts.graphUrl),
|
|
4499
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
4500
|
+
});
|
|
4501
|
+
return new SqliteStoreBackend(db);
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
return new SqliteStoreBackend(db);
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4365
4507
|
// src/store/query.ts
|
|
4366
4508
|
var NotFoundError = class extends Error {
|
|
4367
4509
|
constructor(message) {
|
|
@@ -4676,9 +4818,9 @@ var IngestEnvelopeSchema = z5.object({
|
|
|
4676
4818
|
contributor: ContributorSchema.optional(),
|
|
4677
4819
|
anonymizationLevel: z5.enum(["none", "anonymized", "full"]).optional()
|
|
4678
4820
|
});
|
|
4679
|
-
function ingestEnvelope(store, envelope, opts = {}) {
|
|
4821
|
+
async function ingestEnvelope(store, envelope, opts = {}) {
|
|
4680
4822
|
const anonMode = opts.anonMode ?? "reject";
|
|
4681
|
-
const org = envelope.org ?? opts.defaultOrg
|
|
4823
|
+
const org = normalizeTenant(envelope.org ?? opts.defaultOrg);
|
|
4682
4824
|
const level = envelope.anonymizationLevel ?? "anonymized";
|
|
4683
4825
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4684
4826
|
const contributor = {
|
|
@@ -4729,7 +4871,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
|
|
|
4729
4871
|
}
|
|
4730
4872
|
const safe = check.node;
|
|
4731
4873
|
const identity = computeIdentity(org, safe);
|
|
4732
|
-
const outcome = store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
|
|
4874
|
+
const outcome = await store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
|
|
4733
4875
|
accepted += 1;
|
|
4734
4876
|
if (outcome === "merged") merged += 1;
|
|
4735
4877
|
acceptedNodeIds.add(safe.id);
|
|
@@ -4745,7 +4887,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
|
|
|
4745
4887
|
if (acceptedNodeIds.size > 0 && (!acceptedNodeIds.has(edge.sourceId) || !acceptedNodeIds.has(edge.targetId))) {
|
|
4746
4888
|
continue;
|
|
4747
4889
|
}
|
|
4748
|
-
store.insertEdge(org, edge);
|
|
4890
|
+
await store.insertEdge(org, edge);
|
|
4749
4891
|
edges += 1;
|
|
4750
4892
|
}
|
|
4751
4893
|
logInfo("ingest", { org, accepted, merged, rejected, edges, violations, level, anonMode });
|
|
@@ -4755,7 +4897,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
|
|
|
4755
4897
|
// src/central/server.ts
|
|
4756
4898
|
function createIngestHandler(store, opts = {}) {
|
|
4757
4899
|
const quota = opts.quota;
|
|
4758
|
-
return (body) => {
|
|
4900
|
+
return async (body) => {
|
|
4759
4901
|
const parsed = IngestEnvelopeSchema.safeParse(body);
|
|
4760
4902
|
if (!parsed.success) {
|
|
4761
4903
|
const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
@@ -4771,7 +4913,7 @@ function createIngestHandler(store, opts = {}) {
|
|
|
4771
4913
|
}
|
|
4772
4914
|
}
|
|
4773
4915
|
try {
|
|
4774
|
-
const result = ingestEnvelope(store, parsed.data, opts);
|
|
4916
|
+
const result = await ingestEnvelope(store, parsed.data, opts);
|
|
4775
4917
|
return { status: 200, body: result };
|
|
4776
4918
|
} catch (err) {
|
|
4777
4919
|
logWarn("ingest: failed", { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -5700,7 +5842,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
|
|
|
5700
5842
|
|
|
5701
5843
|
// src/mcp/server.ts
|
|
5702
5844
|
var SERVER_NAME = "cartography";
|
|
5703
|
-
var SERVER_VERSION = "2.
|
|
5845
|
+
var SERVER_VERSION = "2.9.0";
|
|
5704
5846
|
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
5705
5847
|
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
5706
5848
|
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
@@ -5775,9 +5917,10 @@ function createMcpServer(opts = {}) {
|
|
|
5775
5917
|
"graph-summary",
|
|
5776
5918
|
"cartography://graph/summary",
|
|
5777
5919
|
{ title: "Topology summary", description: "Low-token aggregate index of the whole landscape \u2014 read this first.", mimeType: "text/markdown" },
|
|
5778
|
-
(uri) => {
|
|
5920
|
+
async (uri) => {
|
|
5779
5921
|
if (org !== void 0) {
|
|
5780
|
-
|
|
5922
|
+
const s = opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org);
|
|
5923
|
+
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(s) }] };
|
|
5781
5924
|
}
|
|
5782
5925
|
const sid = resolveSession();
|
|
5783
5926
|
if (!sid) return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: "No discovery session found. Run discovery first." }] };
|
|
@@ -5852,8 +5995,8 @@ function createMcpServer(opts = {}) {
|
|
|
5852
5995
|
server.registerTool(
|
|
5853
5996
|
"get_summary",
|
|
5854
5997
|
{ title: "Get topology summary", description: "Low-token overview of the whole landscape (counts, types, domains, most-connected, anomalies).", inputSchema: {}, annotations: readOnly },
|
|
5855
|
-
() => {
|
|
5856
|
-
if (org !== void 0) return json(db.getOrgSummary(org));
|
|
5998
|
+
async () => {
|
|
5999
|
+
if (org !== void 0) return json(opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org));
|
|
5857
6000
|
const sid = resolveSession();
|
|
5858
6001
|
if (!sid) return json({ error: "No discovery session found." });
|
|
5859
6002
|
return json(db.getGraphSummary(sid));
|
|
@@ -6379,7 +6522,7 @@ async function runHttp(factory, opts = {}) {
|
|
|
6379
6522
|
res.writeHead(413, { "content-type": "application/json" }).end('{"error":"payload too large"}');
|
|
6380
6523
|
return;
|
|
6381
6524
|
}
|
|
6382
|
-
const out = onIngest(value);
|
|
6525
|
+
const out = await onIngest(value);
|
|
6383
6526
|
res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
|
|
6384
6527
|
return;
|
|
6385
6528
|
}
|
|
@@ -8084,6 +8227,156 @@ function handleGraphqlGet() {
|
|
|
8084
8227
|
return { status: 200, body: SDL };
|
|
8085
8228
|
}
|
|
8086
8229
|
|
|
8230
|
+
// src/web/dashboard.ts
|
|
8231
|
+
var STYLE = `
|
|
8232
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
8233
|
+
:root{--bg:#0f1419;--panel:#161b22;--line:#2d333b;--fg:#e6edf3;--dim:#8b949e;--accent:#3b82f6;--ok:#3fb950;--warn:#d29922;--crit:#f85149}
|
|
8234
|
+
body{font:13px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--fg);height:100vh;display:flex;flex-direction:column;overflow:hidden}
|
|
8235
|
+
header{display:flex;align-items:center;gap:12px;padding:8px 14px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
8236
|
+
header h1{font-size:15px;font-weight:600;letter-spacing:.3px}
|
|
8237
|
+
header .ver{color:var(--dim);font-size:11px}
|
|
8238
|
+
header .spacer{flex:1}
|
|
8239
|
+
header input{background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:5px 8px;font-size:12px;width:200px}
|
|
8240
|
+
header input:focus{outline:none;border-color:var(--accent)}
|
|
8241
|
+
header button{background:var(--accent);border:none;color:#fff;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer}
|
|
8242
|
+
main{flex:1;display:grid;grid-template-columns:300px 1fr 320px;overflow:hidden}
|
|
8243
|
+
.col{overflow:auto;padding:12px;border-right:1px solid var(--line)}
|
|
8244
|
+
.col:last-child{border-right:none;border-left:1px solid var(--line)}
|
|
8245
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:10px}
|
|
8246
|
+
.card h2{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim);margin-bottom:8px}
|
|
8247
|
+
.stat{display:flex;justify-content:space-between;padding:2px 0}
|
|
8248
|
+
.stat b{font-variant-numeric:tabular-nums}
|
|
8249
|
+
.bar{height:4px;border-radius:2px;background:var(--accent);margin-top:2px}
|
|
8250
|
+
#search{width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:6px 8px;margin-bottom:8px}
|
|
8251
|
+
.node-item{padding:6px 8px;border-radius:6px;cursor:pointer;border:1px solid transparent}
|
|
8252
|
+
.node-item:hover{background:var(--panel)}
|
|
8253
|
+
.node-item.sel{background:var(--panel);border-color:var(--accent)}
|
|
8254
|
+
.node-item .t{color:var(--dim);font-size:11px}
|
|
8255
|
+
#center{position:relative;padding:0}
|
|
8256
|
+
#graph{display:block;width:100%;height:100%;background:radial-gradient(circle at 50% 40%,#11161d,#0d1117)}
|
|
8257
|
+
#empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--dim);text-align:center;padding:20px}
|
|
8258
|
+
.kv{display:flex;justify-content:space-between;gap:8px;padding:3px 0;border-bottom:1px solid var(--line)}
|
|
8259
|
+
.kv span:first-child{color:var(--dim)}
|
|
8260
|
+
.kv span:last-child{text-align:right;word-break:break-all}
|
|
8261
|
+
.chip{display:inline-block;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:1px 8px;font-size:11px;margin:2px 2px 0 0}
|
|
8262
|
+
.sev-high,.sev-critical{color:var(--crit)} .sev-medium,.sev-warning{color:var(--warn)} .sev-low,.sev-info{color:var(--dim)}
|
|
8263
|
+
#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--crit);color:#fff;padding:8px 14px;border-radius:6px;font-size:12px;opacity:0;transition:opacity .2s;pointer-events:none}
|
|
8264
|
+
#toast.show{opacity:1}
|
|
8265
|
+
`;
|
|
8266
|
+
var SCRIPT = String.raw`
|
|
8267
|
+
const $=(s)=>document.querySelector(s), api=(p)=>{
|
|
8268
|
+
const h={accept:'application/json'};
|
|
8269
|
+
const t=sessionStorage.getItem('cartograph_token'); if(t) h.authorization='Bearer '+t;
|
|
8270
|
+
const tn=sessionStorage.getItem('cartograph_tenant'); if(tn) h['x-cartograph-tenant']=tn;
|
|
8271
|
+
return fetch(p,{headers:h}).then(async r=>{ if(!r.ok){ const e=new Error('http '+r.status); e.status=r.status; throw e; } return r.json(); });
|
|
8272
|
+
};
|
|
8273
|
+
function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2600); }
|
|
8274
|
+
let NODES=[], SELECTED=null;
|
|
8275
|
+
|
|
8276
|
+
async function boot(){
|
|
8277
|
+
try{
|
|
8278
|
+
const s=await api('/v1/summary'); renderSummary(s);
|
|
8279
|
+
const n=await api('/v1/nodes?limit=1000'); NODES=n.nodes; renderList(NODES);
|
|
8280
|
+
}catch(e){
|
|
8281
|
+
if(e.status===401){ toast('Unauthorized — enter a bearer token and Reload.'); }
|
|
8282
|
+
else if(e.status===404){ const em=$('#empty'); em.textContent='No discovery session yet. Run a scan, then Reload.'; em.style.display='flex'; }
|
|
8283
|
+
else toast('Failed to load: '+e.message);
|
|
8284
|
+
}
|
|
8285
|
+
}
|
|
8286
|
+
function renderSummary(s){
|
|
8287
|
+
const max=Math.max(1,...Object.values(s.nodesByType));
|
|
8288
|
+
const types=Object.entries(s.nodesByType).sort((a,b)=>b[1]-a[1]).slice(0,12)
|
|
8289
|
+
.map(([k,v])=>'<div class="stat"><span>'+esc(k)+'</span><b>'+v+'</b></div><div class="bar" style="width:'+(v/max*100)+'%"></div>').join('');
|
|
8290
|
+
const anom=(s.anomalies||[]).slice(0,12).map(a=>'<div class="stat"><span class="sev-'+a.severity+'">'+esc(a.kind)+'</span><span class="t">'+esc(a.nodeId)+'</span></div>').join('') || '<div class="t">none</div>';
|
|
8291
|
+
$('#summary').innerHTML=
|
|
8292
|
+
'<div class="card"><h2>Totals</h2><div class="stat"><span>Nodes</span><b>'+s.totals.nodes+'</b></div><div class="stat"><span>Edges</span><b>'+s.totals.edges+'</b></div>'+(s.contributors!=null?'<div class="stat"><span>Contributors</span><b>'+s.contributors+'</b></div>':'')+'</div>'+
|
|
8293
|
+
'<div class="card"><h2>Nodes by type</h2>'+types+'</div>'+
|
|
8294
|
+
'<div class="card"><h2>Anomalies</h2>'+anom+'</div>';
|
|
8295
|
+
}
|
|
8296
|
+
function renderList(nodes){
|
|
8297
|
+
$('#list').innerHTML=nodes.map(n=>'<div class="node-item" data-id="'+esc(n.id)+'"><div>'+esc(n.name)+'</div><div class="t">'+esc(n.type)+'</div></div>').join('')||'<div class="t">no nodes</div>';
|
|
8298
|
+
$('#list').querySelectorAll('.node-item').forEach(el=>el.onclick=()=>select(el.dataset.id));
|
|
8299
|
+
}
|
|
8300
|
+
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
8301
|
+
|
|
8302
|
+
async function select(id){
|
|
8303
|
+
SELECTED=id;
|
|
8304
|
+
$('#list').querySelectorAll('.node-item').forEach(el=>el.classList.toggle('sel',el.dataset.id===id));
|
|
8305
|
+
const node=NODES.find(n=>n.id===id);
|
|
8306
|
+
try{
|
|
8307
|
+
const dep=await api('/v1/nodes/'+encodeURIComponent(id)+'/dependencies?direction=both&maxDepth=2');
|
|
8308
|
+
renderDetail(node,dep); drawGraph(id,dep);
|
|
8309
|
+
}catch(e){ toast('drill-down failed: '+e.message); }
|
|
8310
|
+
}
|
|
8311
|
+
function renderDetail(node,dep){
|
|
8312
|
+
if(!node){ $('#detail').innerHTML='<div class="t">node not in current page</div>'; return; }
|
|
8313
|
+
const fields=[['id',node.id],['type',node.type],['name',node.name],['confidence',node.confidence],['domain',node.domain],['owner',node.owner]]
|
|
8314
|
+
.filter(([,v])=>v!=null).map(([k,v])=>'<div class="kv"><span>'+k+'</span><span>'+esc(v)+'</span></div>').join('');
|
|
8315
|
+
const tags=(node.tags||[]).map(t=>'<span class="chip">'+esc(t)+'</span>').join('');
|
|
8316
|
+
const edges=(dep.edges||[]).map(e=>'<div class="kv"><span>'+esc(e.relationship)+'</span><span>'+esc(e.sourceId===node.id?('→ '+e.targetId):('← '+e.sourceId))+'</span></div>').join('')||'<div class="t">no dependencies</div>';
|
|
8317
|
+
$('#detail').innerHTML='<div class="card"><h2>Node</h2>'+fields+(tags?'<div style="margin-top:6px">'+tags+'</div>':'')+'</div><div class="card"><h2>Dependencies ('+(dep.nodes?dep.nodes.length:0)+')</h2>'+edges+'</div>';
|
|
8318
|
+
}
|
|
8319
|
+
|
|
8320
|
+
const cv=()=>$('#graph'), ctx=()=>cv().getContext('2d');
|
|
8321
|
+
function drawGraph(rootId,dep){
|
|
8322
|
+
$('#empty').style.display='none';
|
|
8323
|
+
const c=cv(); const dpr=window.devicePixelRatio||1; const w=c.clientWidth,h=c.clientHeight;
|
|
8324
|
+
c.width=w*dpr; c.height=h*dpr; const g=ctx(); g.setTransform(dpr,0,0,dpr,0,0); g.clearRect(0,0,w,h);
|
|
8325
|
+
const nodes=dep.nodes||[]; const cx=w/2,cy=h/2;
|
|
8326
|
+
// root at center; others on a circle, radius by depth.
|
|
8327
|
+
const pos={}; pos[rootId]={x:cx,y:cy};
|
|
8328
|
+
const others=nodes.filter(n=>n.id!==rootId);
|
|
8329
|
+
others.forEach((n,i)=>{ const a=(i/Math.max(1,others.length))*Math.PI*2; const r=90+(n.depth||1)*70; pos[n.id]={x:cx+Math.cos(a)*r,y:cy+Math.sin(a)*r}; });
|
|
8330
|
+
// edges
|
|
8331
|
+
g.strokeStyle='#30363d'; g.lineWidth=1.2;
|
|
8332
|
+
(dep.edges||[]).forEach(e=>{ const a=pos[e.sourceId],b=pos[e.targetId]; if(a&&b){ g.beginPath(); g.moveTo(a.x,a.y); g.lineTo(b.x,b.y); g.stroke(); } });
|
|
8333
|
+
// nodes
|
|
8334
|
+
const byId={}; nodes.forEach(n=>byId[n.id]=n);
|
|
8335
|
+
Object.entries(pos).forEach(([id,p])=>{ const n=byId[id]; const root=id===rootId;
|
|
8336
|
+
g.beginPath(); g.arc(p.x,p.y,root?13:8,0,Math.PI*2);
|
|
8337
|
+
g.fillStyle=root?'#3b82f6':'#21262d'; g.fill(); g.lineWidth=root?2:1; g.strokeStyle=root?'#60a5fa':'#484f58'; g.stroke();
|
|
8338
|
+
g.fillStyle='#c9d1d9'; g.font=(root?'600 12px':'11px')+' ui-sans-serif'; g.textAlign='center';
|
|
8339
|
+
g.fillText((n&&n.name?n.name:id).slice(0,22),p.x,p.y-(root?20:14));
|
|
8340
|
+
});
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
document.addEventListener('DOMContentLoaded',()=>{
|
|
8344
|
+
const t=sessionStorage.getItem('cartograph_token'); if(t)$('#token').value=t;
|
|
8345
|
+
const tn=sessionStorage.getItem('cartograph_tenant'); if(tn)$('#tenant').value=tn;
|
|
8346
|
+
$('#reload').onclick=()=>{ sessionStorage.setItem('cartograph_token',$('#token').value.trim()); sessionStorage.setItem('cartograph_tenant',$('#tenant').value.trim()); boot(); };
|
|
8347
|
+
$('#search').oninput=(e)=>{ const q=e.target.value.toLowerCase(); renderList(NODES.filter(n=>n.name.toLowerCase().includes(q)||n.id.toLowerCase().includes(q)||n.type.toLowerCase().includes(q))); };
|
|
8348
|
+
boot();
|
|
8349
|
+
});
|
|
8350
|
+
`;
|
|
8351
|
+
function dashboardHtml(opts = {}) {
|
|
8352
|
+
const version = opts.version ?? "";
|
|
8353
|
+
return `<!DOCTYPE html>
|
|
8354
|
+
<html lang="en"><head>
|
|
8355
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8356
|
+
<title>Cartograph dashboard</title>
|
|
8357
|
+
<style>${STYLE}</style>
|
|
8358
|
+
</head><body>
|
|
8359
|
+
<header>
|
|
8360
|
+
<h1>Cartograph</h1><span class="ver">${version ? `v${version}` : ""}</span>
|
|
8361
|
+
<span class="spacer"></span>
|
|
8362
|
+
<input id="tenant" placeholder="tenant (optional)" autocomplete="off">
|
|
8363
|
+
<input id="token" type="password" placeholder="bearer token" autocomplete="off">
|
|
8364
|
+
<button id="reload">Reload</button>
|
|
8365
|
+
</header>
|
|
8366
|
+
<main>
|
|
8367
|
+
<div class="col"><div id="summary"></div></div>
|
|
8368
|
+
<div class="col" id="center"><canvas id="graph"></canvas><div id="empty">Select a node to explore its dependencies.</div></div>
|
|
8369
|
+
<div class="col">
|
|
8370
|
+
<input id="search" placeholder="Search nodes\u2026" autocomplete="off">
|
|
8371
|
+
<div id="list"></div>
|
|
8372
|
+
<div id="detail"></div>
|
|
8373
|
+
</div>
|
|
8374
|
+
</main>
|
|
8375
|
+
<div id="toast"></div>
|
|
8376
|
+
<script>${SCRIPT}</script>
|
|
8377
|
+
</body></html>`;
|
|
8378
|
+
}
|
|
8379
|
+
|
|
8087
8380
|
// src/api/server.ts
|
|
8088
8381
|
var DEPENDENCIES_RE = /^\/v1\/nodes\/(.+)\/dependencies$/;
|
|
8089
8382
|
var MAX_GRAPHQL_BYTES = 1024 * 1024;
|
|
@@ -8125,6 +8418,8 @@ async function runApi(opts) {
|
|
|
8125
8418
|
});
|
|
8126
8419
|
const restDeps = { backend: opts.backend, version: opts.version };
|
|
8127
8420
|
const openApiDoc = buildOpenApiDocument({ version: opts.version });
|
|
8421
|
+
const dashboardEnabled = opts.dashboard !== false;
|
|
8422
|
+
const dashboardPage = dashboardEnabled ? dashboardHtml({ version: opts.version }) : "";
|
|
8128
8423
|
const allowedOrigins = opts.allowedOrigins ?? [];
|
|
8129
8424
|
assertSafeBind({ host: host2, port: requestedPort, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...token ? { token } : {} });
|
|
8130
8425
|
let allowedHosts = opts.allowedHosts ?? [];
|
|
@@ -8167,6 +8462,11 @@ async function runApi(opts) {
|
|
|
8167
8462
|
finish(200);
|
|
8168
8463
|
return;
|
|
8169
8464
|
}
|
|
8465
|
+
if (dashboardEnabled && (path === "/" || path === "/app") && req.method === "GET") {
|
|
8466
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", ...cors }).end(dashboardPage);
|
|
8467
|
+
finish(200);
|
|
8468
|
+
return;
|
|
8469
|
+
}
|
|
8170
8470
|
if (path === "/v1/health") {
|
|
8171
8471
|
if (req.method !== "GET") {
|
|
8172
8472
|
send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
|
|
@@ -8330,6 +8630,7 @@ function parseApiArgs(argv) {
|
|
|
8330
8630
|
const a = argv[i];
|
|
8331
8631
|
if (a === "--http") continue;
|
|
8332
8632
|
else if (a === "--no-graphql") opts.graphql = false;
|
|
8633
|
+
else if (a === "--no-dashboard") opts.dashboard = false;
|
|
8333
8634
|
else if (a === "--port") opts.port = Number(argv[++i]);
|
|
8334
8635
|
else if (a === "--host") opts.host = argv[++i];
|
|
8335
8636
|
else if (a === "--allowed-hosts") opts.allowedHosts = splitList(argv[++i]);
|
|
@@ -8365,12 +8666,14 @@ async function startApi(opts = {}) {
|
|
|
8365
8666
|
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
8366
8667
|
...token ? { token } : {},
|
|
8367
8668
|
...opts.graphql === false ? { graphql: false } : {},
|
|
8669
|
+
...opts.dashboard === false ? { dashboard: false } : {},
|
|
8368
8670
|
...opts.tenant ? { tenant: { defaultTenant: normalizeTenant(opts.tenant) } } : {},
|
|
8369
8671
|
log: log2
|
|
8370
8672
|
});
|
|
8371
8673
|
const graphqlNote = opts.graphql === false ? " [REST only]" : " + /graphql";
|
|
8674
|
+
const dashNote = opts.dashboard === false ? "" : ` \xB7 dashboard http://${host2}:${port}/`;
|
|
8372
8675
|
log2(
|
|
8373
|
-
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})`
|
|
8676
|
+
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})${dashNote}`
|
|
8374
8677
|
);
|
|
8375
8678
|
return server;
|
|
8376
8679
|
}
|
|
@@ -11634,6 +11937,7 @@ export {
|
|
|
11634
11937
|
DEFAULT_SERVER_NAME,
|
|
11635
11938
|
DEFAULT_TENANT,
|
|
11636
11939
|
DriftConfigSchema,
|
|
11940
|
+
GraphStoreBackend,
|
|
11637
11941
|
INGEST_SCHEMA_VERSION,
|
|
11638
11942
|
IngestEnvelopeSchema,
|
|
11639
11943
|
InvalidTenantError,
|
|
@@ -11721,6 +12025,7 @@ export {
|
|
|
11721
12025
|
createSqliteQueryBackend,
|
|
11722
12026
|
currentOs,
|
|
11723
12027
|
cursorDeeplink,
|
|
12028
|
+
dashboardHtml,
|
|
11724
12029
|
databasesScanner,
|
|
11725
12030
|
deepMerge,
|
|
11726
12031
|
defaultAllowedHosts,
|
|
@@ -11805,6 +12110,7 @@ export {
|
|
|
11805
12110
|
nodesToAssets,
|
|
11806
12111
|
normalizeId,
|
|
11807
12112
|
normalizeTenant,
|
|
12113
|
+
openStoreBackend,
|
|
11808
12114
|
orgKeyPath,
|
|
11809
12115
|
osUser,
|
|
11810
12116
|
parseApiArgs,
|