@datasynx/agentic-ai-cartography 0.1.8 → 0.2.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.
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ exportAll,
4
+ exportBackstageYAML,
5
+ exportHTML,
6
+ exportJSON,
7
+ exportSOPDashboard,
8
+ exportSOPMarkdown,
9
+ generateDependencyMermaid,
10
+ generateTopologyMermaid,
11
+ generateWorkflowMermaid
12
+ } from "./chunk-GUZXO6PM.js";
13
+ export {
14
+ exportAll,
15
+ exportBackstageYAML,
16
+ exportHTML,
17
+ exportJSON,
18
+ exportSOPDashboard,
19
+ exportSOPMarkdown,
20
+ generateDependencyMermaid,
21
+ generateTopologyMermaid,
22
+ generateWorkflowMermaid
23
+ };
24
+ //# sourceMappingURL=exporter-BDVDYA3K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/dist/index.d.ts CHANGED
@@ -233,7 +233,7 @@ type ClientMessage = {
233
233
  answer: string;
234
234
  } | {
235
235
  type: 'command';
236
- command: 'new-task' | 'end-task' | 'status' | 'stop';
236
+ command: 'new-task' | 'end-task' | 'status' | 'stop' | 'pause' | 'resume';
237
237
  } | {
238
238
  type: 'task-description';
239
239
  description: string;
@@ -252,10 +252,12 @@ interface ShadowStatus {
252
252
  nodeCount: number;
253
253
  eventCount: number;
254
254
  taskCount: number;
255
+ sopCount: number;
255
256
  pendingPrompts: number;
256
257
  autoSave: boolean;
257
258
  mode: 'foreground' | 'daemon';
258
259
  agentActive: boolean;
260
+ paused: boolean;
259
261
  cyclesRun: number;
260
262
  cyclesSkipped: number;
261
263
  }
@@ -316,6 +318,12 @@ declare class CartographyDB {
316
318
  id: string;
317
319
  workflowId: string;
318
320
  }>;
321
+ markTaskAsSOPCandidate(taskId: string): void;
322
+ getAllSOPs(): Array<SOP & {
323
+ id: string;
324
+ workflowId: string;
325
+ generatedAt: string;
326
+ }>;
319
327
  setApproval(pattern: string, action: 'save' | 'ignore' | 'auto'): void;
320
328
  getApproval(pattern: string): string | undefined;
321
329
  getStats(sessionId: string): {
@@ -365,9 +373,14 @@ declare function exportBackstageYAML(nodes: NodeRow[], edges: EdgeRow[], org?: s
365
373
  declare function exportJSON(db: CartographyDB, sessionId: string): string;
366
374
  declare function exportHTML(nodes: NodeRow[], edges: EdgeRow[]): string;
367
375
  declare function exportSOPMarkdown(sop: SOP): string;
376
+ declare function exportSOPDashboard(sops: Array<SOP & {
377
+ id: string;
378
+ workflowId: string;
379
+ generatedAt?: string;
380
+ }>): string;
368
381
  declare function exportAll(db: CartographyDB, sessionId: string, outputDir: string, formats?: string[]): void;
369
382
 
370
383
  declare function checkPrerequisites(): void;
371
384
  declare function checkPollInterval(intervalMs: number): number;
372
385
 
373
- export { type ActivityEvent, type CartographyConfig, CartographyDB, type ClientMessage, type DaemonMessage, type DiscoveryEdge, type DiscoveryEvent, type DiscoveryNode, EDGE_RELATIONSHIPS, EVENT_TYPES, type EdgeRelationship, type EdgeRow, EdgeSchema, type EventRow, EventSchema, type EventType, MIN_POLL_INTERVAL_MS, NODE_TYPES, type NodeRow, NodeSchema, type NodeType, type PendingPrompt, type SOP, SOPSchema, type SOPStep, SOPStepSchema, type SessionRow, type ShadowStatus, type TaskRow, type WorkflowRow, checkPollInterval, checkPrerequisites, createCartographyTools, CartographyDB as default, defaultConfig, exportAll, exportBackstageYAML, exportHTML, exportJSON, exportSOPMarkdown, generateDependencyMermaid, generateSOPs, generateTopologyMermaid, generateWorkflowMermaid, runDiscovery, runShadowCycle, safetyHook, stripSensitive };
386
+ export { type ActivityEvent, type CartographyConfig, CartographyDB, type ClientMessage, type DaemonMessage, type DiscoveryEdge, type DiscoveryEvent, type DiscoveryNode, EDGE_RELATIONSHIPS, EVENT_TYPES, type EdgeRelationship, type EdgeRow, EdgeSchema, type EventRow, EventSchema, type EventType, MIN_POLL_INTERVAL_MS, NODE_TYPES, type NodeRow, NodeSchema, type NodeType, type PendingPrompt, type SOP, SOPSchema, type SOPStep, SOPStepSchema, type SessionRow, type ShadowStatus, type TaskRow, type WorkflowRow, checkPollInterval, checkPrerequisites, createCartographyTools, CartographyDB as default, defaultConfig, exportAll, exportBackstageYAML, exportHTML, exportJSON, exportSOPDashboard, exportSOPMarkdown, generateDependencyMermaid, generateSOPs, generateTopologyMermaid, generateWorkflowMermaid, runDiscovery, runShadowCycle, safetyHook, stripSensitive };
package/dist/index.js CHANGED
@@ -385,6 +385,24 @@ var CartographyDB = class {
385
385
  confidence: r["confidence"]
386
386
  }));
387
387
  }
388
+ markTaskAsSOPCandidate(taskId) {
389
+ this.db.prepare("UPDATE tasks SET is_sop_candidate = 1 WHERE id = ?").run(taskId);
390
+ }
391
+ getAllSOPs() {
392
+ const rows = this.db.prepare("SELECT * FROM sops ORDER BY generated_at DESC").all();
393
+ return rows.map((r) => ({
394
+ id: r["id"],
395
+ workflowId: r["workflow_id"],
396
+ title: r["title"],
397
+ description: r["description"],
398
+ steps: JSON.parse(r["steps"]),
399
+ involvedSystems: JSON.parse(r["involved_systems"]),
400
+ estimatedDuration: r["estimated_duration"],
401
+ frequency: r["frequency"],
402
+ confidence: r["confidence"],
403
+ generatedAt: r["generated_at"]
404
+ }));
405
+ }
388
406
  // ── Approvals ───────────────────────────
389
407
  setApproval(pattern, action) {
390
408
  this.db.prepare(`
@@ -735,6 +753,124 @@ async function createCartographyTools(db, sessionId, opts = {}) {
735
753
  }]
736
754
  };
737
755
  }),
756
+ tool("scan_k8s_resources", "Kubernetes-Cluster via kubectl scannen \u2014 100% readonly (get, describe)", {
757
+ namespace: z2.string().optional().describe("Namespace filtern \u2014 leer = alle Namespaces")
758
+ }, async (args) => {
759
+ const { execSync: execSync2 } = await import("child_process");
760
+ const ns = args["namespace"];
761
+ const nsFlag = ns ? `-n ${ns}` : "--all-namespaces";
762
+ const run = (cmd) => {
763
+ try {
764
+ return execSync2(cmd, { stdio: "pipe", timeout: 15e3, shell: "/bin/sh" }).toString().trim();
765
+ } catch (e) {
766
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
767
+ }
768
+ };
769
+ const sections = [
770
+ ["CONTEXT", 'kubectl config current-context 2>/dev/null || echo "(kein Context gesetzt)"'],
771
+ ["NODES", "kubectl get nodes -o wide"],
772
+ ["NAMESPACES", "kubectl get namespaces"],
773
+ ["SERVICES", `kubectl get services ${nsFlag}`],
774
+ ["DEPLOYMENTS", `kubectl get deployments ${nsFlag}`],
775
+ ["STATEFULSETS", `kubectl get statefulsets ${nsFlag}`],
776
+ ["INGRESSES", `kubectl get ingress ${nsFlag} 2>/dev/null || echo "(keine)"`],
777
+ ["PODS_RUNNING", `kubectl get pods ${nsFlag} --field-selector=status.phase=Running 2>/dev/null | head -60`],
778
+ ["CONFIGMAPS_SYSTEM", "kubectl get configmaps -n kube-system 2>/dev/null | head -30"]
779
+ ];
780
+ const out = sections.map(([l, c]) => `=== ${l} ===
781
+ ${run(c)}`).join("\n\n");
782
+ return { content: [{ type: "text", text: out }] };
783
+ }),
784
+ tool("scan_aws_resources", "AWS-Infrastruktur via AWS CLI scannen \u2014 100% readonly (describe, list)", {
785
+ region: z2.string().optional().describe("AWS Region \u2014 default: AWS_DEFAULT_REGION oder Profil"),
786
+ profile: z2.string().optional().describe("AWS CLI Profil")
787
+ }, async (args) => {
788
+ const { execSync: execSync2 } = await import("child_process");
789
+ const region = args["region"];
790
+ const profile = args["profile"];
791
+ const env = { ...process.env };
792
+ if (region) env["AWS_DEFAULT_REGION"] = region;
793
+ const pf = profile ? `--profile ${profile}` : "";
794
+ const run = (cmd) => {
795
+ try {
796
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh", env }).toString().trim();
797
+ } catch (e) {
798
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
799
+ }
800
+ };
801
+ const sections = [
802
+ ["IDENTITY", `aws sts get-caller-identity ${pf} --output json`],
803
+ ["EC2", `aws ec2 describe-instances ${pf} --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,State.Name,PublicIpAddress,PrivateIpAddress,Tags[?Key==\`Name\`].Value|[0]]' --output table`],
804
+ ["RDS", `aws rds describe-db-instances ${pf} --query 'DBInstances[*].[DBInstanceIdentifier,Engine,DBInstanceStatus,Endpoint.Address,Endpoint.Port]' --output table`],
805
+ ["ELB_V2", `aws elbv2 describe-load-balancers ${pf} --query 'LoadBalancers[*].[LoadBalancerName,DNSName,Type,State.Code]' --output table`],
806
+ ["EKS", `aws eks list-clusters ${pf} --output json`],
807
+ ["ELASTICACHE", `aws elasticache describe-cache-clusters ${pf} --query 'CacheClusters[*].[CacheClusterId,Engine,CacheClusterStatus]' --output table 2>/dev/null || echo "(nicht verf\xFCgbar)"`],
808
+ ["S3", `aws s3 ls ${pf} 2>/dev/null || echo "(nicht verf\xFCgbar)"`],
809
+ ["VPC", `aws ec2 describe-vpcs ${pf} --query 'Vpcs[*].[VpcId,CidrBlock,IsDefault,Tags[?Key==\`Name\`].Value|[0]]' --output table`]
810
+ ];
811
+ const out = sections.map(([l, c]) => `=== ${l} ===
812
+ ${run(c)}`).join("\n\n");
813
+ return { content: [{ type: "text", text: out }] };
814
+ }),
815
+ tool("scan_gcp_resources", "Google Cloud Platform via gcloud CLI scannen \u2014 100% readonly (list, describe)", {
816
+ project: z2.string().optional().describe("GCP Project ID \u2014 default: aktuelles gcloud-Projekt")
817
+ }, async (args) => {
818
+ const { execSync: execSync2 } = await import("child_process");
819
+ const project = args["project"];
820
+ const pf = project ? `--project ${project}` : "";
821
+ const run = (cmd) => {
822
+ try {
823
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
824
+ } catch (e) {
825
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
826
+ }
827
+ };
828
+ const sections = [
829
+ ["IDENTITY", `gcloud config list account --format='value(core.account)' 2>/dev/null; gcloud config get-value project 2>/dev/null`],
830
+ ["COMPUTE_INSTANCES", `gcloud compute instances list ${pf} 2>/dev/null || echo "(error)"`],
831
+ ["SQL_INSTANCES", `gcloud sql instances list ${pf} 2>/dev/null || echo "(error)"`],
832
+ ["GKE_CLUSTERS", `gcloud container clusters list ${pf} 2>/dev/null || echo "(error)"`],
833
+ ["CLOUD_RUN", `gcloud run services list ${pf} --platform managed 2>/dev/null || echo "(error)"`],
834
+ ["CLOUD_FUNCTIONS", `gcloud functions list ${pf} 2>/dev/null || echo "(error)"`],
835
+ ["REDIS", `gcloud redis instances list ${pf} --regions=- 2>/dev/null || echo "(error)"`],
836
+ ["PUBSUB", `gcloud pubsub topics list ${pf} 2>/dev/null || echo "(error)"`],
837
+ ["SPANNER", `gcloud spanner instances list ${pf} 2>/dev/null || echo "(error)"`]
838
+ ];
839
+ const out = sections.map(([l, c]) => `=== ${l} ===
840
+ ${run(c)}`).join("\n\n");
841
+ return { content: [{ type: "text", text: out }] };
842
+ }),
843
+ tool("scan_azure_resources", "Azure-Infrastruktur via az CLI scannen \u2014 100% readonly (list, show)", {
844
+ subscription: z2.string().optional().describe("Azure Subscription ID"),
845
+ resourceGroup: z2.string().optional().describe("Resource Group filtern")
846
+ }, async (args) => {
847
+ const { execSync: execSync2 } = await import("child_process");
848
+ const sub = args["subscription"];
849
+ const rg = args["resourceGroup"];
850
+ const sf = sub ? `--subscription ${sub}` : "";
851
+ const rf = rg ? `--resource-group ${rg}` : "";
852
+ const run = (cmd) => {
853
+ try {
854
+ return execSync2(cmd, { stdio: "pipe", timeout: 2e4, shell: "/bin/sh" }).toString().trim();
855
+ } catch (e) {
856
+ return `(error: ${e instanceof Error ? e.message.split("\n")[0] : String(e)})`;
857
+ }
858
+ };
859
+ const sections = [
860
+ ["IDENTITY", `az account show --output json ${sf} 2>/dev/null || echo "(nicht eingeloggt \u2014 az login)"`],
861
+ ["VMS", `az vm list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
862
+ ["AKS", `az aks list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
863
+ ["SQL_SERVERS", `az sql server list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
864
+ ["POSTGRES", `az postgres server list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
865
+ ["REDIS", `az redis list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
866
+ ["WEBAPPS", `az webapp list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
867
+ ["CONTAINER_APPS", `az containerapp list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`],
868
+ ["FUNCTIONS", `az functionapp list ${sf} ${rf} --output table 2>/dev/null || echo "(error)"`]
869
+ ];
870
+ const out = sections.map(([l, c]) => `=== ${l} ===
871
+ ${run(c)}`).join("\n\n");
872
+ return { content: [{ type: "text", text: out }] };
873
+ }),
738
874
  tool("save_sop", "Standard Operating Procedure speichern", {
739
875
  workflowId: z2.string(),
740
876
  title: z2.string(),
@@ -806,15 +942,22 @@ SCHRITT 2 \u2014 Lokale Infrastruktur:
806
942
  ss -tlnp && ps aux \u2192 alle lauschenden Ports/Prozesse identifizieren
807
943
  Jeden Service vertiefen: DB\u2192Schemas, API\u2192Endpoints, Queue\u2192Topics
808
944
 
809
- SCHRITT 3 \u2014 Config-Files:
945
+ SCHRITT 3 \u2014 Cloud & Kubernetes (falls CLI vorhanden):
946
+ scan_k8s_resources() \u2192 Nodes, Services, Pods, Deployments, Ingresses
947
+ scan_aws_resources() \u2192 EC2, RDS, ELB, EKS, ElastiCache, S3 (falls AWS CLI + Credentials)
948
+ scan_gcp_resources() \u2192 Compute, SQL, GKE, Cloud Run, Functions (falls gcloud + Auth)
949
+ scan_azure_resources() \u2192 VMs, AKS, SQL, Redis, WebApps (falls az CLI + Login)
950
+ Fehler / "nicht verf\xFCgbar" \u2192 ignorieren, weiter mit n\xE4chstem Tool
951
+
952
+ SCHRITT 4 \u2014 Config-Files:
810
953
  .env, docker-compose.yml, application.yml, kubernetes/*.yml
811
954
  Nur Host:Port extrahieren \u2014 KEINE Credentials
812
955
 
813
- SCHRITT 4 \u2014 R\xFCckfragen bei Unklarheit:
956
+ SCHRITT 5 \u2014 R\xFCckfragen bei Unklarheit:
814
957
  ask_user() nutzen wenn: Dienst unklar ist, Kontext fehlt, oder User Input sinnvoll w\xE4re
815
958
  Beispiele: "Welche Umgebung ist das (dev/staging/prod)?", "Ist <host> ein internes Tool?"
816
959
 
817
- SCHRITT 5 \u2014 Fertig wenn alle Spuren ersch\xF6pft.
960
+ SCHRITT 6 \u2014 Fertig wenn alle Spuren ersch\xF6pft.
818
961
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
819
962
 
820
963
  PORT-MAPPING: 5432=postgres, 3306=mysql, 27017=mongodb, 6379=redis,
@@ -849,6 +992,10 @@ Nutze ask_user wenn du Kontext vom User brauchst.`;
849
992
  "mcp__cartograph__save_edge",
850
993
  "mcp__cartograph__get_catalog",
851
994
  "mcp__cartograph__scan_bookmarks",
995
+ "mcp__cartograph__scan_k8s_resources",
996
+ "mcp__cartograph__scan_aws_resources",
997
+ "mcp__cartograph__scan_gcp_resources",
998
+ "mcp__cartograph__scan_azure_resources",
852
999
  "mcp__cartograph__ask_user"
853
1000
  ],
854
1001
  hooks: {
@@ -1400,6 +1547,197 @@ function exportSOPMarkdown(sop) {
1400
1547
  }
1401
1548
  return lines.join("\n");
1402
1549
  }
1550
+ function exportSOPDashboard(sops) {
1551
+ const sopsJson = JSON.stringify(sops.map((s) => ({
1552
+ id: s.id,
1553
+ title: s.title,
1554
+ description: s.description,
1555
+ steps: s.steps,
1556
+ systems: s.involvedSystems,
1557
+ duration: s.estimatedDuration,
1558
+ frequency: s.frequency,
1559
+ confidence: s.confidence,
1560
+ generatedAt: s.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1561
+ })));
1562
+ const systemCount = {};
1563
+ for (const sop of sops) {
1564
+ for (const sys of sop.involvedSystems) {
1565
+ systemCount[sys] = (systemCount[sys] ?? 0) + 1;
1566
+ }
1567
+ }
1568
+ const systemsJson = JSON.stringify(
1569
+ Object.entries(systemCount).sort((a, b) => b[1] - a[1])
1570
+ );
1571
+ return `<!DOCTYPE html>
1572
+ <html lang="de">
1573
+ <head>
1574
+ <meta charset="UTF-8">
1575
+ <title>Cartography \u2014 SOP Dashboard</title>
1576
+ <style>
1577
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1578
+ body {
1579
+ background: #0d1117; color: #e6edf3;
1580
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
1581
+ padding: 0; line-height: 1.6;
1582
+ }
1583
+ .header {
1584
+ background: linear-gradient(135deg, #161b22 0%, #1a1f2e 100%);
1585
+ border-bottom: 1px solid #30363d; padding: 32px 40px;
1586
+ }
1587
+ .header h1 { font-size: 24px; color: #58a6ff; margin-bottom: 8px; }
1588
+ .header .subtitle { color: #8b949e; font-size: 14px; }
1589
+ .stats-row {
1590
+ display: flex; gap: 24px; margin-top: 16px; flex-wrap: wrap;
1591
+ }
1592
+ .stat-card {
1593
+ background: #21262d; border: 1px solid #30363d; border-radius: 8px;
1594
+ padding: 12px 20px; min-width: 140px;
1595
+ }
1596
+ .stat-card .value { font-size: 28px; font-weight: 700; color: #58a6ff; }
1597
+ .stat-card .label { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: 0.5px; }
1598
+ .container { max-width: 1200px; margin: 0 auto; padding: 24px 40px; }
1599
+ .section-title { font-size: 18px; color: #c9d1d9; margin: 32px 0 16px; border-bottom: 1px solid #21262d; padding-bottom: 8px; }
1600
+ /* Systems bar chart */
1601
+ .systems-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; }
1602
+ .sys-tag {
1603
+ background: #21262d; border: 1px solid #30363d; border-radius: 6px;
1604
+ padding: 6px 12px; font-size: 12px; cursor: default;
1605
+ }
1606
+ .sys-tag .count { color: #58a6ff; font-weight: 600; margin-left: 4px; }
1607
+ /* SOP cards */
1608
+ .sop-card {
1609
+ background: #161b22; border: 1px solid #30363d; border-radius: 8px;
1610
+ margin-bottom: 16px; overflow: hidden; transition: border-color 0.2s;
1611
+ }
1612
+ .sop-card:hover { border-color: #58a6ff; }
1613
+ .sop-header {
1614
+ padding: 16px 20px; cursor: pointer; display: flex;
1615
+ justify-content: space-between; align-items: center;
1616
+ }
1617
+ .sop-header h3 { font-size: 16px; color: #e6edf3; }
1618
+ .sop-meta { display: flex; gap: 16px; align-items: center; font-size: 12px; color: #8b949e; }
1619
+ .sop-meta .freq { color: #3fb950; font-weight: 600; }
1620
+ .sop-meta .dur { color: #d29922; }
1621
+ .sop-meta .conf {
1622
+ display: inline-flex; align-items: center; gap: 4px;
1623
+ }
1624
+ .conf-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
1625
+ .sop-body { display: none; padding: 0 20px 20px; border-top: 1px solid #21262d; }
1626
+ .sop-body.open { display: block; padding-top: 16px; }
1627
+ .sop-desc { color: #8b949e; font-size: 13px; margin-bottom: 12px; }
1628
+ .sop-systems { margin-bottom: 12px; }
1629
+ .sop-systems span { background: #0d419d33; color: #58a6ff; border-radius: 4px; padding: 2px 8px; font-size: 11px; margin-right: 4px; }
1630
+ .steps-list { list-style: none; counter-reset: step; }
1631
+ .steps-list li {
1632
+ counter-increment: step; position: relative;
1633
+ padding: 10px 12px 10px 44px; border-left: 2px solid #30363d;
1634
+ margin-left: 14px; font-size: 13px;
1635
+ }
1636
+ .steps-list li:last-child { border-left-color: transparent; }
1637
+ .steps-list li::before {
1638
+ content: counter(step);
1639
+ position: absolute; left: -14px; top: 8px;
1640
+ width: 26px; height: 26px; border-radius: 50%;
1641
+ background: #21262d; border: 2px solid #30363d;
1642
+ display: flex; align-items: center; justify-content: center;
1643
+ font-size: 12px; font-weight: 600; color: #58a6ff;
1644
+ }
1645
+ .step-tool { color: #d2a8ff; font-weight: 600; }
1646
+ .step-target { color: #7ee787; font-size: 12px; }
1647
+ .step-notes { color: #8b949e; font-style: italic; font-size: 12px; margin-top: 2px; }
1648
+ .step-instr { color: #c9d1d9; }
1649
+ .toggle-icon { color: #8b949e; font-size: 18px; transition: transform 0.2s; }
1650
+ .toggle-icon.open { transform: rotate(90deg); }
1651
+ .empty { color: #484f58; font-size: 14px; padding: 40px; text-align: center; }
1652
+ .gen-time { color: #484f58; font-size: 11px; margin-top: 8px; }
1653
+ </style>
1654
+ </head>
1655
+ <body>
1656
+ <div class="header">
1657
+ <h1>SOP Dashboard</h1>
1658
+ <div class="subtitle">Datasynx Cartography \u2014 Standard Operating Procedures</div>
1659
+ <div class="stats-row">
1660
+ <div class="stat-card"><div class="value" id="sop-count">0</div><div class="label">SOPs</div></div>
1661
+ <div class="stat-card"><div class="value" id="step-count">0</div><div class="label">Total Steps</div></div>
1662
+ <div class="stat-card"><div class="value" id="sys-count">0</div><div class="label">Systems</div></div>
1663
+ <div class="stat-card"><div class="value" id="avg-conf">\u2014</div><div class="label">Avg Confidence</div></div>
1664
+ </div>
1665
+ </div>
1666
+ <div class="container">
1667
+ <h2 class="section-title">Beteiligte Systeme</h2>
1668
+ <div class="systems-grid" id="systems"></div>
1669
+
1670
+ <h2 class="section-title">SOPs</h2>
1671
+ <div id="sop-list"></div>
1672
+ </div>
1673
+ <script>
1674
+ const sops = ${sopsJson};
1675
+ const systems = ${systemsJson};
1676
+
1677
+ document.getElementById('sop-count').textContent = sops.length;
1678
+ document.getElementById('step-count').textContent = sops.reduce((a, s) => a + s.steps.length, 0);
1679
+ document.getElementById('sys-count').textContent = systems.length;
1680
+ const avgConf = sops.length > 0
1681
+ ? (sops.reduce((a, s) => a + s.confidence, 0) / sops.length * 100).toFixed(0) + '%'
1682
+ : '\u2014';
1683
+ document.getElementById('avg-conf').textContent = avgConf;
1684
+
1685
+ const sysDiv = document.getElementById('systems');
1686
+ systems.forEach(([name, count]) => {
1687
+ const el = document.createElement('div');
1688
+ el.className = 'sys-tag';
1689
+ el.innerHTML = name + '<span class="count">x' + count + '</span>';
1690
+ sysDiv.appendChild(el);
1691
+ });
1692
+
1693
+ const listDiv = document.getElementById('sop-list');
1694
+ if (sops.length === 0) {
1695
+ listDiv.innerHTML = '<div class="empty">Keine SOPs vorhanden. Shadow-Daemon starten und Workflows beobachten.</div>';
1696
+ }
1697
+
1698
+ sops.forEach((sop, i) => {
1699
+ const confColor = sop.confidence >= 0.8 ? '#3fb950' : sop.confidence >= 0.5 ? '#d29922' : '#f85149';
1700
+ const card = document.createElement('div');
1701
+ card.className = 'sop-card';
1702
+ card.innerHTML = \`
1703
+ <div class="sop-header" onclick="toggle(\${i})">
1704
+ <h3>\${sop.title}</h3>
1705
+ <div class="sop-meta">
1706
+ <span class="freq">\${sop.frequency}</span>
1707
+ <span class="dur">\${sop.duration}</span>
1708
+ <span class="conf"><span class="conf-dot" style="background:\${confColor}"></span>\${Math.round(sop.confidence*100)}%</span>
1709
+ <span class="toggle-icon" id="icon-\${i}">\u25B8</span>
1710
+ </div>
1711
+ </div>
1712
+ <div class="sop-body" id="body-\${i}">
1713
+ <div class="sop-desc">\${sop.description}</div>
1714
+ <div class="sop-systems">\${sop.systems.map(s => '<span>'+s+'</span>').join('')}</div>
1715
+ <ol class="steps-list">
1716
+ \${sop.steps.map(st => \`
1717
+ <li>
1718
+ <span class="step-tool">\${st.tool}</span>
1719
+ \${st.target ? '<span class="step-target"> \u2192 '+st.target+'</span>' : ''}
1720
+ <div class="step-instr">\${st.instruction}</div>
1721
+ \${st.notes ? '<div class="step-notes">'+st.notes+'</div>' : ''}
1722
+ </li>
1723
+ \`).join('')}
1724
+ </ol>
1725
+ <div class="gen-time">Generiert: \${sop.generatedAt ? sop.generatedAt.substring(0,19).replace('T',' ') : '\u2014'}</div>
1726
+ </div>
1727
+ \`;
1728
+ listDiv.appendChild(card);
1729
+ });
1730
+
1731
+ function toggle(i) {
1732
+ const body = document.getElementById('body-'+i);
1733
+ const icon = document.getElementById('icon-'+i);
1734
+ body.classList.toggle('open');
1735
+ icon.classList.toggle('open');
1736
+ }
1737
+ </script>
1738
+ </body>
1739
+ </html>`;
1740
+ }
1403
1741
  function exportAll(db, sessionId, outputDir, formats = ["mermaid", "json", "yaml", "html", "sops"]) {
1404
1742
  mkdirSync2(outputDir, { recursive: true });
1405
1743
  mkdirSync2(join2(outputDir, "sops"), { recursive: true });
@@ -1496,6 +1834,7 @@ export {
1496
1834
  exportBackstageYAML,
1497
1835
  exportHTML,
1498
1836
  exportJSON,
1837
+ exportSOPDashboard,
1499
1838
  exportSOPMarkdown,
1500
1839
  generateDependencyMermaid,
1501
1840
  generateSOPs,