@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/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 ?? "local";
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.7.0";
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
- return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(db.getOrgSummary(org)) }] };
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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,