#!/usr/bin/env python3
"""
Audit Workflow HTML Generator
==============================
Generates standalone HTML audit workflow diagrams. The template is embedded
directly in the script - no network access required at runtime.
Fetch: https://audittoolbox.com/apps/workflow-generate.py.txt
Usage:
python workflow-generate.py '{"nodes":[...],"edges":[...]}'
# Or pipe JSON:
echo '{"nodes":[...],"edges":[...]}' | python workflow-generate.py
# Or with a file:
python workflow-generate.py < data.json > output.html
Output is a standalone HTML file that renders the audit workflow diagram.
JSON Schema:
{
"nodes": [
{
"id": "string", // Unique identifier (kebab-case)
"type": "step", // Optional, defaults to "step"
"position": {"x": number, "y": number}, // Optional (auto-layout if missing)
"data": {
"label": "string", // Display title
"description": "string", // Optional description
"instructions": "string", // Optional detailed instructions
"linkedAgentUrl": "string", // Optional linked agent URL
"completed": boolean // Optional completed status
}
}
],
"edges": [
{
"id": "string", // Unique identifier
"source": "string", // Source node id
"target": "string" // Target node id
}
]
}
Also accepts AuditSwarm export format:
{
"version": "1.0",
"data": {
"workflows": [{
"name": "...",
"description": "...",
"diagramJson": {
"nodes": [...],
"edges": [...]
}
}]
}
}
"""
import sys
import json
import re
import os
def read_template():
"""Read the HTML template from standalone-canvas.html."""
template_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'standalone-canvas.html')
with open(template_path, 'r', encoding='utf-8') as f:
return f.read()
def normalize_workflow(json_data):
"""Normalize various JSON input formats into {nodes, edges} for the canvas."""
if isinstance(json_data, str):
parsed = json.loads(json_data)
else:
parsed = json_data
# Handle AuditSwarm export format: {version, data: {workflows: [{diagramJson: ...}]}}
if isinstance(parsed.get('data'), dict):
workflows = parsed['data'].get('workflows', [])
if workflows and isinstance(workflows, list):
diagram = workflows[0].get('diagramJson', {})
if diagram:
parsed = diagram
# Handle direct diagramJson wrapper
if 'diagramJson' in parsed:
parsed = parsed['diagramJson']
nodes = parsed.get('nodes', [])
edges = parsed.get('edges', [])
# Normalize nodes: ensure each has type:'step' and proper data structure
normalized_nodes = []
for node in nodes:
n = {
'id': node['id'],
'type': 'step',
'data': {
'label': node.get('data', {}).get('label', 'Untitled'),
'description': node.get('data', {}).get('description', ''),
'instructions': node.get('data', {}).get('instructions', ''),
'linkedAgentUrl': node.get('data', {}).get('linkedAgentUrl', ''),
'completed': node.get('data', {}).get('completed', False),
}
}
# Preserve position if present
pos = node.get('position')
if pos and isinstance(pos, dict) and 'x' in pos and 'y' in pos:
n['position'] = {'x': pos['x'], 'y': pos['y']}
else:
n['position'] = {'x': 0, 'y': 0}
normalized_nodes.append(n)
# Normalize edges: ensure each has type:'deletable', animated:true, style
default_style = {'stroke': '#6366f1', 'strokeWidth': 2, 'strokeDasharray': '5,5'}
normalized_edges = []
for edge in edges:
normalized_edges.append({
'id': edge['id'],
'source': edge['source'],
'target': edge['target'],
'type': 'deletable',
'animated': True,
'style': dict(default_style),
})
return {'nodes': normalized_nodes, 'edges': normalized_edges}
def generate_html(json_data):
"""Generate HTML with embedded JSON data."""
try:
data = normalize_workflow(json_data)
except (json.JSONDecodeError, KeyError, TypeError) as e:
print(f"Error: Invalid JSON - {e}", file=sys.stderr)
sys.exit(1)
json_str = json.dumps(data)
template = read_template()
# Replace only the first occurrence of the @CSDATA marker (the actual data line),
# not the regex pattern inside handleSaveHtml which also contains the marker text.
html = re.sub(
r'const COLLECTIVE_SWARM_DATA = .+; // @CSDATA',
'const COLLECTIVE_SWARM_DATA = ' + json_str + '; // @CSDATA',
template,
count=1
)
return html
if __name__ == '__main__':
# Get JSON from argument or stdin
if len(sys.argv) > 1:
json_data = sys.argv[1]
else:
json_data = sys.stdin.read().strip()
if not json_data:
print(__doc__)
sys.exit(0)
print(generate_html(json_data))