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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +192 -100
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +190 -14
- htmlgraph/analytics_index.py +135 -51
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +45 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/orchestration/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {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
|
|
20
|
+
import socket
|
|
21
|
+
import sys
|
|
17
22
|
import urllib.parse
|
|
18
23
|
from datetime import datetime, timezone
|
|
19
|
-
from http.server import
|
|
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.
|
|
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 = [
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
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 = (
|
|
82
|
-
|
|
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=
|
|
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 =
|
|
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
|
-
"""
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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"] = [
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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={
|
|
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,
|
|
940
|
+
if hasattr(node, "track_id") and node.track_id == track_id
|
|
776
941
|
]
|
|
777
942
|
|
|
778
|
-
self._send_json(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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,
|
|
802
|
-
"plan_task_id": feature.plan_task_id
|
|
803
|
-
|
|
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
|
-
|
|
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 /
|
|
811
|
-
track_file = manager.tracks_dir / f"{
|
|
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"] =
|
|
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
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
932
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
962
|
-
|
|
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
|
-
║
|
|
1448
|
+
║ HtmlGraph Server (FastAPI) ║
|
|
965
1449
|
╠══════════════════════════════════════════════════════════════╣
|
|
966
|
-
║ Dashboard:
|
|
967
|
-
║ API:
|
|
968
|
-
║
|
|
969
|
-
║
|
|
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/
|
|
974
|
-
GET /api/
|
|
975
|
-
GET /api/
|
|
976
|
-
GET /api/
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
994
|
-
|
|
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
|
-
|
|
997
|
-
if watcher:
|
|
998
|
-
watcher.stop()
|
|
999
|
-
server.shutdown()
|
|
1504
|
+
logger.info("\nShutting down...")
|
|
1000
1505
|
|
|
1001
1506
|
|
|
1002
1507
|
if __name__ == "__main__":
|