htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__py3-none-any.whl

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.
Files changed (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/server.py CHANGED
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  HtmlGraph REST API Server.
3
7
 
@@ -13,20 +17,20 @@ Or via CLI:
13
17
  """
14
18
 
15
19
  import json
16
- import re
20
+ import socket
21
+ import sys
17
22
  import urllib.parse
18
23
  from datetime import datetime, timezone
19
- from http.server import HTTPServer, SimpleHTTPRequestHandler
24
+ from http.server import SimpleHTTPRequestHandler
20
25
  from pathlib import Path
21
- from typing import Any
26
+ from typing import Any, Literal, cast
22
27
 
23
- from htmlgraph.graph import HtmlGraph
24
- from htmlgraph.models import Node, Edge, Step
25
- from htmlgraph.converter import node_to_dict, dict_to_node
26
28
  from htmlgraph.analytics_index import AnalyticsIndex
29
+ from htmlgraph.converter import dict_to_node, node_to_dict
27
30
  from htmlgraph.event_log import JsonlEventLog
28
- from htmlgraph.file_watcher import GraphWatcher
31
+ from htmlgraph.graph import HtmlGraph
29
32
  from htmlgraph.ids import generate_id
33
+ from htmlgraph.models import Node
30
34
 
31
35
 
32
36
  class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
@@ -39,9 +43,19 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
39
43
  analytics_db: AnalyticsIndex | None = None
40
44
 
41
45
  # Work item types (subfolders in .htmlgraph/)
42
- COLLECTIONS = ["features", "bugs", "spikes", "chores", "epics", "sessions", "agents", "tracks"]
43
-
44
- def __init__(self, *args, **kwargs):
46
+ COLLECTIONS = [
47
+ "features",
48
+ "bugs",
49
+ "spikes",
50
+ "chores",
51
+ "epics",
52
+ "sessions",
53
+ "agents",
54
+ "tracks",
55
+ "task-delegations",
56
+ ]
57
+
58
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
45
59
  # Set directory for static file serving
46
60
  self.directory = str(self.static_dir)
47
61
  super().__init__(*args, **kwargs)
@@ -54,14 +68,14 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
54
68
 
55
69
  # Tracks support both file-based (track-xxx.html) and directory-based (track-xxx/index.html)
56
70
  if collection == "tracks":
57
- from htmlgraph.planning import Track
58
71
  from htmlgraph.converter import html_to_node
72
+ from htmlgraph.planning import Track
59
73
 
60
74
  graph = HtmlGraph(
61
75
  collection_dir,
62
76
  stylesheet_path="../styles.css",
63
77
  auto_load=False, # Manual load to convert to Track objects
64
- pattern=["*.html", "*/index.html"]
78
+ pattern=["*.html", "*/index.html"],
65
79
  )
66
80
 
67
81
  # Helper to convert Node to Track with has_spec/has_plan detection
@@ -74,41 +88,62 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
74
88
  # Consolidated format: spec/plan are in the same file
75
89
  # Check for data-section attributes in the file
76
90
  content = filepath.read_text(encoding="utf-8")
77
- has_spec = 'data-section="overview"' in content or 'data-section="requirements"' in content
91
+ has_spec = (
92
+ 'data-section="overview"' in content
93
+ or 'data-section="requirements"' in content
94
+ )
78
95
  has_plan = 'data-section="plan"' in content
79
96
  else:
80
97
  # Directory format: separate spec.html and plan.html files
81
- has_spec = (track_dir / "spec.html").exists() if track_dir else False
82
- has_plan = (track_dir / "plan.html").exists() if track_dir else False
98
+ has_spec = (
99
+ (track_dir / "spec.html").exists() if track_dir else False
100
+ )
101
+ has_plan = (
102
+ (track_dir / "plan.html").exists() if track_dir else False
103
+ )
104
+
105
+ # Map Node status to Track status
106
+ track_status: Literal["planned", "active", "completed", "abandoned"]
107
+ if node.status in ["planned", "active", "completed", "abandoned"]:
108
+ track_status = cast(
109
+ Literal["planned", "active", "completed", "abandoned"],
110
+ node.status,
111
+ )
112
+ else:
113
+ track_status = "planned"
83
114
 
84
115
  return Track(
85
116
  id=node.id,
86
117
  title=node.title,
87
118
  description=node.content or "",
88
- status=node.status if node.status in ["planned", "active", "completed", "abandoned"] else "planned",
119
+ status=track_status,
89
120
  priority=node.priority,
90
121
  created=node.created,
91
122
  updated=node.updated,
92
123
  has_spec=has_spec,
93
124
  has_plan=has_plan,
94
125
  features=[],
95
- sessions=[]
126
+ sessions=[],
96
127
  )
97
128
 
98
129
  # Load and convert tracks
99
- patterns = graph.pattern if isinstance(graph.pattern, list) else [graph.pattern]
130
+ patterns = (
131
+ graph.pattern
132
+ if isinstance(graph.pattern, list)
133
+ else [graph.pattern]
134
+ )
100
135
  for pat in patterns:
101
136
  for filepath in collection_dir.glob(pat):
102
137
  if filepath.is_file():
103
138
  try:
104
139
  node = html_to_node(filepath)
105
140
  track = node_to_track(node, filepath)
106
- graph._nodes[track.id] = track
141
+ graph._nodes[track.id] = track # type: ignore[assignment]
107
142
  except Exception:
108
143
  continue
109
144
 
110
145
  # Override reload to maintain Track conversion
111
- def reload_tracks():
146
+ def reload_tracks() -> int:
112
147
  graph._nodes.clear()
113
148
  for pat in patterns:
114
149
  for filepath in collection_dir.glob(pat):
@@ -116,22 +151,20 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
116
151
  try:
117
152
  node = html_to_node(filepath)
118
153
  track = node_to_track(node, filepath)
119
- graph._nodes[track.id] = track
154
+ graph._nodes[track.id] = track # type: ignore[assignment]
120
155
  except Exception:
121
156
  continue
122
157
  return len(graph._nodes)
123
158
 
124
- graph.reload = reload_tracks
159
+ graph.reload = reload_tracks # type: ignore[method-assign]
125
160
  self.graphs[collection] = graph
126
161
  else:
127
162
  self.graphs[collection] = HtmlGraph(
128
- collection_dir,
129
- stylesheet_path="../styles.css",
130
- auto_load=True
163
+ collection_dir, stylesheet_path="../styles.css", auto_load=True
131
164
  )
132
165
  return self.graphs[collection]
133
166
 
134
- def _send_json(self, data: Any, status: int = 200):
167
+ def _send_json(self, data: Any, status: int = 200) -> None:
135
168
  """Send JSON response."""
136
169
  body = json.dumps(data, indent=2, default=str).encode("utf-8")
137
170
  self.send_response(status)
@@ -141,7 +174,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
141
174
  self.end_headers()
142
175
  self.wfile.write(body)
143
176
 
144
- def _send_error_json(self, message: str, status: int = 400):
177
+ def _send_error_json(self, message: str, status: int = 400) -> None:
145
178
  """Send JSON error response."""
146
179
  self._send_json({"error": message, "status": status}, status)
147
180
 
@@ -181,7 +214,15 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
181
214
  return "api", collection, node_id, query_params
182
215
 
183
216
  def _serve_packaged_dashboard(self) -> bool:
184
- """Serve the bundled dashboard HTML if available."""
217
+ """
218
+ DEPRECATED: Serve the bundled dashboard HTML if available.
219
+
220
+ NOTE: This server is LEGACY. The active server is FastAPI-based.
221
+ See: src/python/htmlgraph/operations/fastapi_server.py
222
+
223
+ The dashboard.html file was archived to .archived-templates/
224
+ Active templates are in src/python/htmlgraph/api/templates/
225
+ """
185
226
  dashboard_path = Path(__file__).parent / "dashboard.html"
186
227
  if not dashboard_path.exists():
187
228
  return False
@@ -193,17 +234,22 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
193
234
  self.wfile.write(body)
194
235
  return True
195
236
 
196
- def do_OPTIONS(self):
237
+ def do_OPTIONS(self) -> None:
197
238
  """Handle CORS preflight."""
198
239
  self.send_response(200)
199
240
  self.send_header("Access-Control-Allow-Origin", "*")
200
- self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
241
+ self.send_header(
242
+ "Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"
243
+ )
201
244
  self.send_header("Access-Control-Allow-Headers", "Content-Type")
202
245
  self.end_headers()
203
246
 
204
- def do_GET(self):
247
+ def do_GET(self) -> None:
205
248
  """Handle GET requests."""
206
249
  api, collection, node_id, params = self._parse_path()
250
+ logger.debug(
251
+ f"do_GET: api={api}, collection={collection}, node_id={node_id}, params={params}"
252
+ )
207
253
 
208
254
  # Not an API request - serve static files
209
255
  if api != "api":
@@ -229,6 +275,15 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
229
275
  if collection == "analytics":
230
276
  return self._handle_analytics(node_id, params)
231
277
 
278
+ # GET /api/orchestration - Get delegation chains and agent coordination
279
+ if collection == "orchestration":
280
+ logger.info(f"DEBUG: Handling orchestration request, params={params}")
281
+ return self._handle_orchestration_view(params)
282
+
283
+ # GET /api/task-delegations/stats - Get aggregated delegation statistics
284
+ if collection == "task-delegations" and params.get("stats") == "true":
285
+ return self._handle_task_delegations_stats()
286
+
232
287
  # GET /api/tracks/{track_id}/features - Get features for a track
233
288
  if collection == "tracks" and node_id and params.get("features") == "true":
234
289
  return self._handle_track_features(node_id)
@@ -237,6 +292,10 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
237
292
  if collection == "features" and node_id and params.get("context") == "true":
238
293
  return self._handle_feature_context(node_id)
239
294
 
295
+ # GET /api/sessions/{session_id}?transcript=true - Get transcript stats
296
+ if collection == "sessions" and node_id and params.get("transcript") == "true":
297
+ return self._handle_session_transcript(node_id)
298
+
240
299
  # GET /api/collections - List available collections
241
300
  if collection == "collections":
242
301
  return self._send_json({"collections": self.COLLECTIONS})
@@ -251,7 +310,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
251
310
 
252
311
  self._send_error_json(f"Unknown endpoint: {self.path}", 404)
253
312
 
254
- def do_POST(self):
313
+ def do_POST(self) -> None:
255
314
  """Handle POST requests (create)."""
256
315
  api, collection, node_id, params = self._parse_path()
257
316
 
@@ -260,7 +319,11 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
260
319
  return
261
320
 
262
321
  # POST /api/tracks/{track_id}/generate-features - Generate features from plan
263
- if collection == "tracks" and node_id and params.get("generate-features") == "true":
322
+ if (
323
+ collection == "tracks"
324
+ and node_id
325
+ and params.get("generate-features") == "true"
326
+ ):
264
327
  try:
265
328
  self._handle_generate_features(node_id)
266
329
  return
@@ -289,7 +352,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
289
352
  except Exception as e:
290
353
  self._send_error_json(str(e), 500)
291
354
 
292
- def do_PUT(self):
355
+ def do_PUT(self) -> None:
293
356
  """Handle PUT requests (full update)."""
294
357
  api, collection, node_id, params = self._parse_path()
295
358
 
@@ -309,7 +372,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
309
372
  except Exception as e:
310
373
  self._send_error_json(str(e), 500)
311
374
 
312
- def do_PATCH(self):
375
+ def do_PATCH(self) -> None:
313
376
  """Handle PATCH requests (partial update)."""
314
377
  api, collection, node_id, params = self._parse_path()
315
378
 
@@ -329,7 +392,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
329
392
  except Exception as e:
330
393
  self._send_error_json(str(e), 500)
331
394
 
332
- def do_DELETE(self):
395
+ def do_DELETE(self) -> None:
333
396
  """Handle DELETE requests."""
334
397
  api, collection, node_id, params = self._parse_path()
335
398
 
@@ -347,9 +410,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
347
410
  # API Handlers
348
411
  # =========================================================================
349
412
 
350
- def _handle_status(self):
413
+ def _handle_status(self) -> None:
351
414
  """Return overall graph status."""
352
- status = {
415
+ status: dict[str, Any] = {
353
416
  "collections": {},
354
417
  "total_nodes": 0,
355
418
  "by_status": {},
@@ -390,14 +453,16 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
390
453
  def _rebuild_analytics_db(self, db_path: Path) -> None:
391
454
  events_dir = self.graph_dir / "events"
392
455
  if not events_dir.exists() or not any(events_dir.glob("*.jsonl")):
393
- raise FileNotFoundError("No event logs found under .htmlgraph/events/*.jsonl")
456
+ raise FileNotFoundError(
457
+ "No event logs found under .htmlgraph/events/*.jsonl"
458
+ )
394
459
 
395
460
  log = JsonlEventLog(events_dir)
396
461
  index = AnalyticsIndex(db_path)
397
462
  events = (event for _, event in log.iter_events())
398
463
  index.rebuild_from_events(events)
399
464
 
400
- def _handle_analytics(self, endpoint: str | None, params: dict):
465
+ def _handle_analytics(self, endpoint: str | None, params: dict) -> None:
401
466
  """
402
467
  Analytics endpoints.
403
468
 
@@ -405,7 +470,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
405
470
  If the index doesn't exist yet, we build it on-demand from `.htmlgraph/events/*.jsonl`.
406
471
  """
407
472
  if endpoint is None:
408
- return self._send_error_json("Specify an analytics endpoint (overview, features, session)", 400)
473
+ return self._send_error_json(
474
+ "Specify an analytics endpoint (overview, features, session)", 400
475
+ )
409
476
 
410
477
  db_path = self.graph_dir / "index.sqlite"
411
478
 
@@ -424,7 +491,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
424
491
  404,
425
492
  )
426
493
  except Exception as e:
427
- return self._send_error_json(f"Failed to build analytics index: {e}", 500)
494
+ return self._send_error_json(
495
+ f"Failed to build analytics index: {e}", 500
496
+ )
428
497
 
429
498
  def should_reset_index(err: Exception) -> bool:
430
499
  msg = str(err).lower()
@@ -436,7 +505,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
436
505
  or "schema_version" in msg
437
506
  )
438
507
 
439
- def with_rebuild(fn):
508
+ def with_rebuild(fn: Any) -> Any:
440
509
  try:
441
510
  return fn()
442
511
  except Exception as e:
@@ -454,16 +523,32 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
454
523
 
455
524
  if endpoint == "overview":
456
525
  try:
457
- return self._send_json(with_rebuild(lambda: self._get_analytics().overview(since=since, until=until)))
526
+ return self._send_json(
527
+ with_rebuild(
528
+ lambda: self._get_analytics().overview(since=since, until=until)
529
+ )
530
+ )
458
531
  except Exception as e:
459
- return self._send_error_json(f"Failed analytics query (overview): {e}", 500)
532
+ return self._send_error_json(
533
+ f"Failed analytics query (overview): {e}", 500
534
+ )
460
535
 
461
536
  if endpoint == "features":
462
537
  limit = int(params.get("limit", 50))
463
538
  try:
464
- return self._send_json({"features": with_rebuild(lambda: self._get_analytics().top_features(since=since, until=until, limit=limit))})
539
+ return self._send_json(
540
+ {
541
+ "features": with_rebuild(
542
+ lambda: self._get_analytics().top_features(
543
+ since=since, until=until, limit=limit
544
+ )
545
+ )
546
+ }
547
+ )
465
548
  except Exception as e:
466
- return self._send_error_json(f"Failed analytics query (features): {e}", 500)
549
+ return self._send_error_json(
550
+ f"Failed analytics query (features): {e}", 500
551
+ )
467
552
 
468
553
  if endpoint == "session":
469
554
  session_id = params.get("id")
@@ -471,9 +556,19 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
471
556
  return self._send_error_json("Missing required param: id", 400)
472
557
  limit = int(params.get("limit", 500))
473
558
  try:
474
- return self._send_json({"events": with_rebuild(lambda: self._get_analytics().session_events(session_id=session_id, limit=limit))})
559
+ return self._send_json(
560
+ {
561
+ "events": with_rebuild(
562
+ lambda: self._get_analytics().session_events(
563
+ session_id=session_id, limit=limit
564
+ )
565
+ )
566
+ }
567
+ )
475
568
  except Exception as e:
476
- return self._send_error_json(f"Failed analytics query (session): {e}", 500)
569
+ return self._send_error_json(
570
+ f"Failed analytics query (session): {e}", 500
571
+ )
477
572
 
478
573
  if endpoint == "continuity":
479
574
  feature_id = params.get("feature_id") or params.get("feature")
@@ -481,17 +576,43 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
481
576
  return self._send_error_json("Missing required param: feature_id", 400)
482
577
  limit = int(params.get("limit", 200))
483
578
  try:
484
- return self._send_json({"sessions": with_rebuild(lambda: self._get_analytics().feature_continuity(feature_id=feature_id, since=since, until=until, limit=limit))})
579
+ return self._send_json(
580
+ {
581
+ "sessions": with_rebuild(
582
+ lambda: self._get_analytics().feature_continuity(
583
+ feature_id=feature_id,
584
+ since=since,
585
+ until=until,
586
+ limit=limit,
587
+ )
588
+ )
589
+ }
590
+ )
485
591
  except Exception as e:
486
- return self._send_error_json(f"Failed analytics query (continuity): {e}", 500)
592
+ return self._send_error_json(
593
+ f"Failed analytics query (continuity): {e}", 500
594
+ )
487
595
 
488
596
  if endpoint == "transitions":
489
597
  limit = int(params.get("limit", 50))
490
598
  feature_id = params.get("feature_id") or params.get("feature")
491
599
  try:
492
- return self._send_json({"transitions": with_rebuild(lambda: self._get_analytics().top_tool_transitions(since=since, until=until, feature_id=feature_id, limit=limit))})
600
+ return self._send_json(
601
+ {
602
+ "transitions": with_rebuild(
603
+ lambda: self._get_analytics().top_tool_transitions(
604
+ since=since,
605
+ until=until,
606
+ feature_id=feature_id,
607
+ limit=limit,
608
+ )
609
+ )
610
+ }
611
+ )
493
612
  except Exception as e:
494
- return self._send_error_json(f"Failed analytics query (transitions): {e}", 500)
613
+ return self._send_error_json(
614
+ f"Failed analytics query (transitions): {e}", 500
615
+ )
495
616
 
496
617
  if endpoint == "commits":
497
618
  feature_id = params.get("feature_id") or params.get("feature")
@@ -499,9 +620,19 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
499
620
  return self._send_error_json("Missing required param: feature_id", 400)
500
621
  limit = int(params.get("limit", 200))
501
622
  try:
502
- return self._send_json({"commits": with_rebuild(lambda: self._get_analytics().feature_commits(feature_id=feature_id, limit=limit))})
623
+ return self._send_json(
624
+ {
625
+ "commits": with_rebuild(
626
+ lambda: self._get_analytics().feature_commits(
627
+ feature_id=feature_id, limit=limit
628
+ )
629
+ )
630
+ }
631
+ )
503
632
  except Exception as e:
504
- return self._send_error_json(f"Failed analytics query (commits): {e}", 500)
633
+ return self._send_error_json(
634
+ f"Failed analytics query (commits): {e}", 500
635
+ )
505
636
 
506
637
  if endpoint == "commit-graph":
507
638
  feature_id = params.get("feature_id") or params.get("feature")
@@ -509,13 +640,23 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
509
640
  return self._send_error_json("Missing required param: feature_id", 400)
510
641
  limit = int(params.get("limit", 200))
511
642
  try:
512
- return self._send_json({"graph": with_rebuild(lambda: self._get_analytics().feature_commit_graph(feature_id=feature_id, limit=limit))})
643
+ return self._send_json(
644
+ {
645
+ "graph": with_rebuild(
646
+ lambda: self._get_analytics().feature_commit_graph(
647
+ feature_id=feature_id, limit=limit
648
+ )
649
+ )
650
+ }
651
+ )
513
652
  except Exception as e:
514
- return self._send_error_json(f"Failed analytics query (commit-graph): {e}", 500)
653
+ return self._send_error_json(
654
+ f"Failed analytics query (commit-graph): {e}", 500
655
+ )
515
656
 
516
657
  return self._send_error_json(f"Unknown analytics endpoint: {endpoint}", 404)
517
658
 
518
- def _handle_query(self, params: dict):
659
+ def _handle_query(self, params: dict) -> None:
519
660
  """Handle CSS selector query across collections."""
520
661
  selector = params.get("selector", "")
521
662
  collection = params.get("collection") # Optional filter to single collection
@@ -525,7 +666,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
525
666
  selector = self._build_selector_from_params(params)
526
667
 
527
668
  results = []
528
- collections = [collection] if collection in self.COLLECTIONS else self.COLLECTIONS
669
+ collections = (
670
+ [collection] if collection in self.COLLECTIONS else self.COLLECTIONS
671
+ )
529
672
 
530
673
  for coll in collections:
531
674
  graph = self._get_graph(coll)
@@ -545,7 +688,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
545
688
  parts.append(f"[data-{key}='{params[key]}']")
546
689
  return "".join(parts)
547
690
 
548
- def _handle_list(self, collection: str, params: dict):
691
+ def _handle_list(self, collection: str, params: dict) -> None:
549
692
  """List all nodes in a collection."""
550
693
  graph = self._get_graph(collection)
551
694
 
@@ -571,7 +714,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
571
714
 
572
715
  if sort_by == "priority":
573
716
  priority_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
574
- nodes.sort(key=lambda n: priority_order.get(n.priority, 99), reverse=not reverse)
717
+ nodes.sort(
718
+ key=lambda n: priority_order.get(n.priority, 99), reverse=not reverse
719
+ )
575
720
  elif sort_by == "created":
576
721
  nodes.sort(key=lambda n: ensure_tz_aware(n.created), reverse=reverse)
577
722
  else: # default: updated
@@ -582,17 +727,19 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
582
727
  offset = int(params.get("offset", 0))
583
728
 
584
729
  total = len(nodes)
585
- nodes = nodes[offset:offset + limit]
586
-
587
- self._send_json({
588
- "collection": collection,
589
- "total": total,
590
- "limit": limit,
591
- "offset": offset,
592
- "nodes": [node_to_dict(n) for n in nodes]
593
- })
730
+ nodes = nodes[offset : offset + limit]
731
+
732
+ self._send_json(
733
+ {
734
+ "collection": collection,
735
+ "total": total,
736
+ "limit": limit,
737
+ "offset": offset,
738
+ "nodes": [node_to_dict(n) for n in nodes],
739
+ }
740
+ )
594
741
 
595
- def _handle_get(self, collection: str, node_id: str):
742
+ def _handle_get(self, collection: str, node_id: str) -> None:
596
743
  """Get a single node."""
597
744
  graph = self._get_graph(collection)
598
745
  node = graph.get(node_id)
@@ -607,7 +754,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
607
754
 
608
755
  self._send_json(data)
609
756
 
610
- def _handle_create(self, collection: str, data: dict):
757
+ def _handle_create(self, collection: str, data: dict) -> None:
611
758
  """Create a new node."""
612
759
  # Set defaults based on collection
613
760
  type_map = {
@@ -636,7 +783,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
636
783
  # Convert steps if provided as strings
637
784
  if "steps" in data and data["steps"]:
638
785
  if isinstance(data["steps"][0], str):
639
- data["steps"] = [{"description": s, "completed": False} for s in data["steps"]]
786
+ data["steps"] = [
787
+ {"description": s, "completed": False} for s in data["steps"]
788
+ ]
640
789
 
641
790
  try:
642
791
  node = dict_to_node(data)
@@ -651,7 +800,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
651
800
  except ValueError as e:
652
801
  self._send_error_json(str(e), 400)
653
802
 
654
- def _handle_update(self, collection: str, node_id: str, data: dict, partial: bool):
803
+ def _handle_update(
804
+ self, collection: str, node_id: str, data: dict, partial: bool
805
+ ) -> None:
655
806
  """Update a node (full or partial)."""
656
807
  graph = self._get_graph(collection)
657
808
  existing = graph.get(node_id)
@@ -686,7 +837,9 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
686
837
  from htmlgraph.session_manager import SessionManager
687
838
 
688
839
  sm = SessionManager(self.graph_dir)
689
- session = sm.get_active_session_for_agent(agent) or sm.start_session(agent=agent, title="API session")
840
+ session = sm.get_active_session_for_agent(
841
+ agent
842
+ ) or sm.start_session(agent=agent, title="API session")
690
843
  step_desc = None
691
844
  try:
692
845
  step_desc = existing.steps[step_idx].description
@@ -718,19 +871,30 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
718
871
  node = dict_to_node(data)
719
872
  graph.update(node)
720
873
  new_status = node.status
721
- if agent and (collection in {"features", "bugs", "spikes", "chores", "epics"}) and (new_status != old_status):
874
+ if (
875
+ agent
876
+ and (collection in {"features", "bugs", "spikes", "chores", "epics"})
877
+ and (new_status != old_status)
878
+ ):
722
879
  try:
723
880
  from htmlgraph.session_manager import SessionManager
724
881
 
725
882
  sm = SessionManager(self.graph_dir)
726
- session = sm.get_active_session_for_agent(agent) or sm.start_session(agent=agent, title="API session")
883
+ session = sm.get_active_session_for_agent(
884
+ agent
885
+ ) or sm.start_session(agent=agent, title="API session")
727
886
  sm.track_activity(
728
887
  session_id=session.id,
729
888
  tool="WorkItemStatus",
730
889
  summary=f"Status {old_status} → {new_status}: {collection}/{node_id}",
731
890
  success=True,
732
891
  feature_id=node_id,
733
- payload={"collection": collection, "node_id": node_id, "from": old_status, "to": new_status},
892
+ payload={
893
+ "collection": collection,
894
+ "node_id": node_id,
895
+ "from": old_status,
896
+ "to": new_status,
897
+ },
734
898
  )
735
899
  except Exception:
736
900
  pass
@@ -738,11 +902,12 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
738
902
  except Exception as e:
739
903
  self._send_error_json(str(e), 400)
740
904
 
741
- def _handle_delete(self, collection: str, node_id: str):
905
+ def _handle_delete(self, collection: str, node_id: str) -> None:
742
906
  """Delete a node."""
743
907
  # Special handling for tracks (directories, not single files)
744
908
  if collection == "tracks":
745
909
  from htmlgraph.track_manager import TrackManager
910
+
746
911
  manager = TrackManager(self.graph_dir)
747
912
  try:
748
913
  manager.delete_track(node_id)
@@ -764,7 +929,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
764
929
  # Track-Feature Integration Handlers
765
930
  # =========================================================================
766
931
 
767
- def _handle_track_features(self, track_id: str):
932
+ def _handle_track_features(self, track_id: str) -> None:
768
933
  """Get all features for a track."""
769
934
  features_graph = self._get_graph("features")
770
935
 
@@ -772,16 +937,18 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
772
937
  track_features = [
773
938
  node_to_dict(node)
774
939
  for node in features_graph
775
- if hasattr(node, 'track_id') and node.track_id == track_id
940
+ if hasattr(node, "track_id") and node.track_id == track_id
776
941
  ]
777
942
 
778
- self._send_json({
779
- "track_id": track_id,
780
- "features": track_features,
781
- "count": len(track_features)
782
- })
943
+ self._send_json(
944
+ {
945
+ "track_id": track_id,
946
+ "features": track_features,
947
+ "count": len(track_features),
948
+ }
949
+ )
783
950
 
784
- def _handle_feature_context(self, feature_id: str):
951
+ def _handle_feature_context(self, feature_id: str) -> None:
785
952
  """Get track/plan/spec context for a feature."""
786
953
  features_graph = self._get_graph("features")
787
954
 
@@ -795,27 +962,40 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
795
962
  self._send_error_json(f"Feature not found: {feature_id}", 404)
796
963
  return
797
964
 
798
- context = {
965
+ context: dict[str, str | list[str] | bool | None] = {
799
966
  "feature_id": feature_id,
800
967
  "feature_title": feature.title,
801
- "track_id": feature.track_id if hasattr(feature, 'track_id') else None,
802
- "plan_task_id": feature.plan_task_id if hasattr(feature, 'plan_task_id') else None,
803
- "spec_requirements": feature.spec_requirements if hasattr(feature, 'spec_requirements') else [],
968
+ "track_id": feature.track_id if hasattr(feature, "track_id") else None,
969
+ "plan_task_id": feature.plan_task_id
970
+ if hasattr(feature, "plan_task_id")
971
+ else None,
972
+ "spec_requirements": feature.spec_requirements
973
+ if hasattr(feature, "spec_requirements")
974
+ else [],
975
+ "track_exists": False,
976
+ "has_spec": False,
977
+ "has_plan": False,
978
+ "is_consolidated": False,
804
979
  }
805
980
 
806
981
  # Load track info if linked
807
- if context["track_id"]:
982
+ track_id = context["track_id"]
983
+ if track_id and isinstance(track_id, str):
808
984
  from htmlgraph.track_manager import TrackManager
985
+
809
986
  manager = TrackManager(self.graph_dir)
810
- track_dir = manager.tracks_dir / context["track_id"]
811
- track_file = manager.tracks_dir / f"{context['track_id']}.html"
987
+ track_dir = manager.tracks_dir / track_id
988
+ track_file = manager.tracks_dir / f"{track_id}.html"
812
989
 
813
990
  # Support both consolidated (single file) and directory-based tracks
814
991
  if track_file.exists():
815
992
  # Consolidated format
816
993
  context["track_exists"] = True
817
994
  content = track_file.read_text(encoding="utf-8")
818
- context["has_spec"] = 'data-section="overview"' in content or 'data-section="requirements"' in content
995
+ context["has_spec"] = (
996
+ 'data-section="overview"' in content
997
+ or 'data-section="requirements"' in content
998
+ )
819
999
  context["has_plan"] = 'data-section="plan"' in content
820
1000
  context["is_consolidated"] = True
821
1001
  elif track_dir.exists():
@@ -831,10 +1011,33 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
831
1011
 
832
1012
  self._send_json(context)
833
1013
 
834
- def _handle_generate_features(self, track_id: str):
1014
+ def _handle_session_transcript(self, session_id: str) -> None:
1015
+ """Get transcript stats for a session."""
1016
+ try:
1017
+ from htmlgraph.session_manager import SessionManager
1018
+
1019
+ manager = SessionManager(self.graph_dir)
1020
+ stats = manager.get_transcript_stats(session_id)
1021
+
1022
+ if stats is None:
1023
+ self._send_json(
1024
+ {
1025
+ "session_id": session_id,
1026
+ "transcript_linked": False,
1027
+ "message": "No transcript linked to this session",
1028
+ }
1029
+ )
1030
+ return
1031
+
1032
+ self._send_json(
1033
+ {"session_id": session_id, "transcript_linked": True, **stats}
1034
+ )
1035
+ except Exception as e:
1036
+ self._send_error_json(f"Error getting transcript stats: {e}", 500)
1037
+
1038
+ def _handle_generate_features(self, track_id: str) -> None:
835
1039
  """Generate features from plan tasks."""
836
1040
  from htmlgraph.track_manager import TrackManager
837
- from htmlgraph.planning import Plan
838
1041
 
839
1042
  manager = TrackManager(self.graph_dir)
840
1043
 
@@ -848,23 +1051,204 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
848
1051
  # Generate features
849
1052
  try:
850
1053
  features = manager.generate_features_from_plan(
851
- track_id=track_id,
852
- plan=plan,
853
- features_dir=self.graph_dir / "features"
1054
+ track_id=track_id, plan=plan, features_dir=self.graph_dir / "features"
854
1055
  )
855
1056
 
856
1057
  # Reload features graph to include new features
857
1058
  self.graphs.pop("features", None)
858
1059
 
859
- self._send_json({
860
- "track_id": track_id,
861
- "generated": len(features),
862
- "feature_ids": [f.id for f in features]
863
- })
1060
+ self._send_json(
1061
+ {
1062
+ "track_id": track_id,
1063
+ "generated": len(features),
1064
+ "feature_ids": [f.id for f in features],
1065
+ }
1066
+ )
864
1067
  except Exception as e:
865
1068
  self._send_error_json(f"Failed to generate features: {str(e)}", 500)
866
1069
 
867
- def _handle_sync_track(self, track_id: str):
1070
+ def _handle_orchestration_view(self, params: dict) -> None:
1071
+ """
1072
+ Get delegation chains and agent coordination information.
1073
+
1074
+ Queries the SQLite database for delegation events and builds
1075
+ a view of agent coordination and handoff patterns.
1076
+
1077
+ Returns:
1078
+ {
1079
+ "delegation_count": int,
1080
+ "unique_agents": int,
1081
+ "agents": [str],
1082
+ "delegation_chains": {
1083
+ "from_agent": [
1084
+ {
1085
+ "to_agent": str,
1086
+ "event_type": str,
1087
+ "timestamp": str,
1088
+ "task": str,
1089
+ "status": str
1090
+ }
1091
+ ]
1092
+ }
1093
+ }
1094
+ """
1095
+ try:
1096
+ from htmlgraph.db.schema import HtmlGraphDB
1097
+
1098
+ # Use unified index.sqlite database
1099
+ db_path = str(self.graph_dir / "index.sqlite")
1100
+ db = HtmlGraphDB(db_path=db_path)
1101
+ db.connect()
1102
+
1103
+ # Get all delegation events
1104
+ delegations = db.get_delegations(limit=1000)
1105
+ db.close()
1106
+
1107
+ # Build delegation chains grouped by from_agent
1108
+ delegation_chains: dict[str, list[dict]] = {}
1109
+ agents = set()
1110
+ delegation_count = 0
1111
+
1112
+ for delegation in delegations:
1113
+ from_agent = delegation.get("from_agent", "unknown")
1114
+ to_agent = delegation.get("to_agent", "unknown")
1115
+ timestamp = delegation.get("timestamp", "")
1116
+ reason = delegation.get("reason", "")
1117
+ status = delegation.get("status", "pending")
1118
+
1119
+ agents.add(from_agent)
1120
+ agents.add(to_agent)
1121
+ delegation_count += 1
1122
+
1123
+ if from_agent not in delegation_chains:
1124
+ delegation_chains[from_agent] = []
1125
+
1126
+ delegation_chains[from_agent].append(
1127
+ {
1128
+ "to_agent": to_agent,
1129
+ "event_type": "delegation",
1130
+ "timestamp": timestamp,
1131
+ "task": reason or "Unnamed task",
1132
+ "status": status,
1133
+ }
1134
+ )
1135
+
1136
+ self._send_json(
1137
+ {
1138
+ "delegation_count": delegation_count,
1139
+ "unique_agents": len(agents),
1140
+ "agents": sorted(list(agents)),
1141
+ "delegation_chains": delegation_chains,
1142
+ }
1143
+ )
1144
+
1145
+ except Exception as e:
1146
+ self._send_error_json(f"Failed to get orchestration view: {str(e)}", 500)
1147
+
1148
+ def _handle_task_delegations_stats(self) -> None:
1149
+ """Get aggregated statistics about task delegations."""
1150
+ try:
1151
+ delegations_graph = self._get_graph("task-delegations")
1152
+
1153
+ # Get all delegations
1154
+ all_delegations = list(delegations_graph)
1155
+
1156
+ if not all_delegations:
1157
+ self._send_json(
1158
+ {
1159
+ "total_delegations": 0,
1160
+ "by_agent_type": {},
1161
+ "by_status": {},
1162
+ "total_tokens": 0,
1163
+ "total_cost": 0.0,
1164
+ "average_duration": 0.0,
1165
+ "agent_stats": [],
1166
+ }
1167
+ )
1168
+ return
1169
+
1170
+ # Aggregate by agent type
1171
+ agent_stats: dict = {}
1172
+ by_status: dict[str, int] = {}
1173
+ total_tokens = 0
1174
+ total_cost = 0.0
1175
+ durations = []
1176
+
1177
+ for delegation in all_delegations:
1178
+ agent_type = str(getattr(delegation, "agent_type", "unknown"))
1179
+ status = str(getattr(delegation, "status", "unknown"))
1180
+ tokens_val = getattr(delegation, "tokens_used", 0)
1181
+ tokens = int(tokens_val) if tokens_val else 0
1182
+ cost_val = getattr(delegation, "cost_usd", 0)
1183
+ cost = float(cost_val) if cost_val else 0.0
1184
+ duration_val = getattr(delegation, "duration_seconds", 0)
1185
+ duration = int(duration_val) if duration_val else 0
1186
+
1187
+ # Track by agent
1188
+ if agent_type not in agent_stats:
1189
+ agent_stats[agent_type] = {
1190
+ "agent_type": agent_type,
1191
+ "tasks_completed": 0,
1192
+ "total_duration": 0,
1193
+ "total_tokens": 0,
1194
+ "total_cost": 0.0,
1195
+ "success_count": 0,
1196
+ "failure_count": 0,
1197
+ }
1198
+
1199
+ agent_stats[agent_type]["tasks_completed"] += 1
1200
+ agent_stats[agent_type]["total_duration"] += duration
1201
+ agent_stats[agent_type]["total_tokens"] += tokens
1202
+ agent_stats[agent_type]["total_cost"] += cost
1203
+
1204
+ if status == "success":
1205
+ agent_stats[agent_type]["success_count"] += 1
1206
+ else:
1207
+ agent_stats[agent_type]["failure_count"] += 1
1208
+
1209
+ # Track by status
1210
+ by_status[status] = by_status.get(status, 0) + 1
1211
+
1212
+ # Aggregate totals
1213
+ total_tokens += tokens
1214
+ total_cost += cost
1215
+ if duration:
1216
+ durations.append(duration)
1217
+
1218
+ # Calculate success rate for each agent
1219
+ for agent_stats_item in agent_stats.values():
1220
+ total = agent_stats_item["tasks_completed"]
1221
+ if total > 0:
1222
+ agent_stats_item["success_rate"] = (
1223
+ agent_stats_item["success_count"] / total
1224
+ )
1225
+ else:
1226
+ agent_stats_item["success_rate"] = 0.0
1227
+
1228
+ average_duration = sum(durations) / len(durations) if durations else 0.0
1229
+
1230
+ self._send_json(
1231
+ {
1232
+ "total_delegations": len(all_delegations),
1233
+ "by_agent_type": {
1234
+ agent: stats["tasks_completed"]
1235
+ for agent, stats in agent_stats.items()
1236
+ },
1237
+ "by_status": by_status,
1238
+ "total_tokens": total_tokens,
1239
+ "total_cost": round(total_cost, 4),
1240
+ "average_duration": round(average_duration, 2),
1241
+ "agent_stats": sorted(
1242
+ agent_stats.values(),
1243
+ key=lambda x: x["total_cost"],
1244
+ reverse=True,
1245
+ ),
1246
+ }
1247
+ )
1248
+ except Exception as e:
1249
+ self._send_error_json(f"Failed to get delegation stats: {str(e)}", 500)
1250
+
1251
+ def _handle_sync_track(self, track_id: str) -> None:
868
1252
  """Sync task and spec completion based on features."""
869
1253
  from htmlgraph.track_manager import TrackManager
870
1254
 
@@ -881,19 +1265,111 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
881
1265
  # Reload tracks graph
882
1266
  self.graphs.pop("tracks", None)
883
1267
 
884
- self._send_json({
885
- "track_id": track_id,
886
- "plan_updated": True,
887
- "spec_updated": True,
888
- "plan_completion": plan.completion_percentage,
889
- "spec_status": spec.status
890
- })
1268
+ self._send_json(
1269
+ {
1270
+ "track_id": track_id,
1271
+ "plan_updated": True,
1272
+ "spec_updated": True,
1273
+ "plan_completion": plan.completion_percentage,
1274
+ "spec_status": spec.status,
1275
+ }
1276
+ )
891
1277
  except Exception as e:
892
1278
  self._send_error_json(f"Failed to sync track: {str(e)}", 500)
893
1279
 
894
- def log_message(self, format: str, *args):
1280
+ def log_message(self, format: str, *args: str) -> None:
895
1281
  """Custom log format."""
896
- print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}")
1282
+ logger.info(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}")
1283
+
1284
+
1285
+ def find_available_port(start_port: int = 8080, max_attempts: int = 10) -> int:
1286
+ """
1287
+ Find an available port starting from start_port.
1288
+
1289
+ Args:
1290
+ start_port: Port to start searching from
1291
+ max_attempts: Maximum number of ports to try
1292
+
1293
+ Returns:
1294
+ Available port number
1295
+
1296
+ Raises:
1297
+ OSError: If no available port found in range
1298
+ """
1299
+ for port in range(start_port, start_port + max_attempts):
1300
+ try:
1301
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1302
+ s.bind(("", port))
1303
+ return port
1304
+ except OSError:
1305
+ continue
1306
+ raise OSError(
1307
+ f"No available ports found in range {start_port}-{start_port + max_attempts}"
1308
+ )
1309
+
1310
+
1311
+ def check_port_in_use(port: int, host: str = "localhost") -> bool:
1312
+ """
1313
+ Check if a port is already in use.
1314
+
1315
+ Args:
1316
+ port: Port number to check
1317
+ host: Host to check on
1318
+
1319
+ Returns:
1320
+ True if port is in use, False otherwise
1321
+ """
1322
+ try:
1323
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
1324
+ s.bind((host, port))
1325
+ return False
1326
+ except OSError:
1327
+ return True
1328
+
1329
+
1330
+ def sync_dashboard_files(
1331
+ static_dir: Path = Path("."),
1332
+ ) -> bool:
1333
+ """
1334
+ Sync dashboard.html to index.html if they differ.
1335
+
1336
+ Args:
1337
+ static_dir: Directory containing index.html
1338
+
1339
+ Returns:
1340
+ True if sync was performed, False if already in sync
1341
+
1342
+ Raises:
1343
+ PermissionError: If unable to write to index.html
1344
+ OSError: If file operations fail
1345
+ """
1346
+ dashboard_file = Path(__file__).parent / "dashboard.html"
1347
+ index_file = static_dir / "index.html"
1348
+
1349
+ # Dashboard file must exist (packaged with htmlgraph)
1350
+ if not dashboard_file.exists():
1351
+ return False
1352
+
1353
+ # If index.html doesn't exist, or content differs, sync
1354
+ if not index_file.exists():
1355
+ # Create new index.html
1356
+ import shutil
1357
+
1358
+ shutil.copy2(dashboard_file, index_file)
1359
+ return True
1360
+
1361
+ # Check if files differ (compare content, not just timestamps)
1362
+ import filecmp
1363
+
1364
+ if not filecmp.cmp(dashboard_file, index_file, shallow=False):
1365
+ # Files differ, sync them
1366
+ import shutil
1367
+
1368
+ shutil.copy2(dashboard_file, index_file)
1369
+ return True
1370
+
1371
+ # Already in sync
1372
+ return False
897
1373
 
898
1374
 
899
1375
  def serve(
@@ -901,22 +1377,41 @@ def serve(
901
1377
  graph_dir: str | Path = ".htmlgraph",
902
1378
  static_dir: str | Path = ".",
903
1379
  host: str = "localhost",
904
- watch: bool = True
905
- ):
1380
+ watch: bool = True,
1381
+ auto_port: bool = False,
1382
+ show_progress: bool = False,
1383
+ quiet: bool = False,
1384
+ ) -> None:
906
1385
  """
907
- Start the HtmlGraph server.
1386
+ Start the HtmlGraph server (FastAPI-based with WebSocket support).
1387
+
1388
+ This function launches the FastAPI server which provides:
1389
+ - REST API for CRUD operations on graph nodes
1390
+ - WebSocket endpoint at /ws/events for real-time event streaming
1391
+ - HTMX-powered dashboard for agent observability
908
1392
 
909
1393
  Args:
910
- port: Port to listen on
1394
+ port: Port to listen on (default: 8080)
911
1395
  graph_dir: Directory containing graph data (.htmlgraph/)
912
- static_dir: Directory for static files (index.html, etc.)
913
- host: Host to bind to
914
- watch: Enable file watching for auto-reload (default: True)
1396
+ static_dir: Directory for static files (index.html, etc.) - preserved for compatibility
1397
+ host: Host to bind to (default: localhost)
1398
+ watch: Enable file watching for auto-reload (default: True) - maps to reload in FastAPI
1399
+ auto_port: Automatically find available port if specified port is in use
1400
+ show_progress: Show Rich progress during startup (not used with FastAPI)
1401
+ quiet: Suppress progress output when true
915
1402
  """
1403
+ import asyncio
1404
+
1405
+ from htmlgraph.operations.fastapi_server import (
1406
+ FastAPIServerError,
1407
+ PortInUseError,
1408
+ run_fastapi_server,
1409
+ start_fastapi_server,
1410
+ )
1411
+
916
1412
  graph_dir = Path(graph_dir)
917
- static_dir = Path(static_dir)
918
1413
 
919
- # Create graph directory structure
1414
+ # Ensure graph directory exists
920
1415
  graph_dir.mkdir(parents=True, exist_ok=True)
921
1416
  for collection in HtmlGraphAPIHandler.COLLECTIONS:
922
1417
  (graph_dir / collection).mkdir(exist_ok=True)
@@ -928,75 +1423,85 @@ def serve(
928
1423
  if styles_src.exists():
929
1424
  styles_dest.write_text(styles_src.read_text())
930
1425
 
931
- # Configure handler
932
- HtmlGraphAPIHandler.graph_dir = graph_dir
933
- HtmlGraphAPIHandler.static_dir = static_dir
934
- HtmlGraphAPIHandler.graphs = {}
935
- HtmlGraphAPIHandler.analytics_db = None
936
-
937
- server = HTTPServer((host, port), HtmlGraphAPIHandler)
938
-
939
- # Start file watcher if enabled
940
- watcher = None
941
- if watch:
942
- def get_graph(collection: str) -> HtmlGraph:
943
- """Callback to get graph instance for a collection."""
944
- handler = HtmlGraphAPIHandler
945
- if collection not in handler.graphs:
946
- collection_dir = handler.graph_dir / collection
947
- handler.graphs[collection] = HtmlGraph(
948
- collection_dir,
949
- stylesheet_path="../styles.css",
950
- auto_load=True
951
- )
952
- return handler.graphs[collection]
1426
+ # Database path - use htmlgraph.db in the graph directory
1427
+ db_path = str(graph_dir / "htmlgraph.db")
953
1428
 
954
- watcher = GraphWatcher(
955
- graph_dir=graph_dir,
956
- collections=HtmlGraphAPIHandler.COLLECTIONS,
957
- get_graph_callback=get_graph
1429
+ try:
1430
+ result = start_fastapi_server(
1431
+ port=port,
1432
+ host=host,
1433
+ db_path=db_path,
1434
+ auto_port=auto_port,
1435
+ reload=watch, # Map watch to reload for FastAPI
958
1436
  )
959
- watcher.start()
960
1437
 
961
- watch_status = "Enabled" if watch else "Disabled"
962
- print(f"""
1438
+ # Print warnings if any
1439
+ for warning in result.warnings:
1440
+ if not quiet:
1441
+ logger.info(f"⚠️ {warning}")
1442
+
1443
+ # Print server info
1444
+ if not quiet:
1445
+ actual_port = result.config_used["port"]
1446
+ print(f"""
963
1447
  ╔══════════════════════════════════════════════════════════════╗
964
- HtmlGraph Server
1448
+ HtmlGraph Server (FastAPI)
965
1449
  ╠══════════════════════════════════════════════════════════════╣
966
- ║ Dashboard: http://{host}:{port}/
967
- ║ API: http://{host}:{port}/api/
968
- Graph Dir: {graph_dir}
969
- Auto-reload: {watch_status}
1450
+ ║ Dashboard: http://{host}:{actual_port}/
1451
+ ║ API: http://{host}:{actual_port}/api/
1452
+ WebSocket: ws://{host}:{actual_port}/ws/events
1453
+ Graph Dir: {graph_dir}
1454
+ ║ Database: {db_path}
1455
+ ║ Auto-reload: {"Enabled" if watch else "Disabled"}
970
1456
  ╚══════════════════════════════════════════════════════════════╝
971
1457
 
1458
+ Features:
1459
+ • Real-time agent activity feed (HTMX + WebSocket)
1460
+ • Orchestration chains visualization
1461
+ • Feature tracker with Kanban view
1462
+ • Session metrics & performance analytics
1463
+
972
1464
  API Endpoints:
973
- GET /api/status - Overall status
974
- GET /api/collections - List collections
975
- GET /api/query?status=todo - Query across collections
976
- GET /api/analytics/overview - Analytics overview (requires index)
977
- GET /api/analytics/features - Top features (requires index)
978
- GET /api/analytics/continuity?feature_id=... - Feature continuity (requires index)
979
- GET /api/analytics/transitions - Tool transitions (requires index)
980
-
981
- GET /api/{{collection}} - List nodes
982
- POST /api/{{collection}} - Create node
983
- GET /api/{{collection}}/{{id}} - Get node
984
- PUT /api/{{collection}}/{{id}} - Replace node
985
- PATCH /api/{{collection}}/{{id}} - Update node
986
- DELETE /api/{{collection}}/{{id}} - Delete node
987
-
988
- Collections: {', '.join(HtmlGraphAPIHandler.COLLECTIONS)}
1465
+ GET /api/events - List events
1466
+ GET /api/sessions - List sessions
1467
+ GET /api/orchestration - Orchestration data
1468
+ GET /api/initial-stats - Dashboard statistics
1469
+ WS /ws/events - Real-time event stream
1470
+
1471
+ Collections: {", ".join(HtmlGraphAPIHandler.COLLECTIONS)}
989
1472
 
990
1473
  Press Ctrl+C to stop.
991
1474
  """)
992
1475
 
993
- try:
994
- server.serve_forever()
1476
+ # Run the server
1477
+ asyncio.run(run_fastapi_server(result.handle))
1478
+
1479
+ except PortInUseError:
1480
+ logger.info(f"\n❌ Port {port} is already in use\n")
1481
+ logger.info("Solutions:")
1482
+ logger.info(" 1. Use a different port:")
1483
+ logger.info(f" htmlgraph serve --port {port + 1}\n")
1484
+ logger.info(" 2. Let htmlgraph automatically find an available port:")
1485
+ logger.info(" htmlgraph serve --auto-port\n")
1486
+ logger.info(f" 3. Find and kill the process using port {port}:")
1487
+ logger.info(f" lsof -ti:{port} | xargs kill -9\n")
1488
+
1489
+ # Try to find and suggest an available port
1490
+ try:
1491
+ alt_port = find_available_port(port + 1)
1492
+ logger.info(f"💡 Found available port: {alt_port}")
1493
+ logger.info(f" Run: htmlgraph serve --port {alt_port}\n")
1494
+ except OSError:
1495
+ pass
1496
+
1497
+ sys.exit(1)
1498
+
1499
+ except FastAPIServerError as e:
1500
+ logger.info(f"\n❌ Server error: {e}\n")
1501
+ sys.exit(1)
1502
+
995
1503
  except KeyboardInterrupt:
996
- print("\nShutting down...")
997
- if watcher:
998
- watcher.stop()
999
- server.shutdown()
1504
+ logger.info("\nShutting down...")
1000
1505
 
1001
1506
 
1002
1507
  if __name__ == "__main__":