htmlgraph 0.26.23__py3-none-any.whl → 0.26.25__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/__init__.py +1 -1
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/api/main.py +56 -23
- htmlgraph/api/templates/dashboard-redesign.html +3 -3
- htmlgraph/api/templates/dashboard.html +3 -3
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/builders/track.py +26 -0
- htmlgraph/cli/base.py +31 -7
- htmlgraph/cli/work/__init__.py +74 -0
- htmlgraph/cli/work/browse.py +114 -0
- htmlgraph/cli/work/snapshot.py +558 -0
- htmlgraph/collections/base.py +34 -0
- htmlgraph/collections/todo.py +12 -0
- htmlgraph/converter.py +11 -0
- htmlgraph/db/schema.py +34 -1
- htmlgraph/hooks/orchestrator.py +88 -14
- htmlgraph/hooks/session_handler.py +3 -1
- htmlgraph/models.py +22 -2
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/plugin_manager.py +1 -2
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/refs.py +343 -0
- htmlgraph/sdk.py +162 -1
- htmlgraph/session_manager.py +154 -2
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +755 -0
- htmlgraph/track_builder.py +12 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/RECORD +36 -28
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.23.data → htmlgraph-0.26.25.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.23.dist-info → htmlgraph-0.26.25.dist-info}/entry_points.txt +0 -0
htmlgraph/api/main.py
CHANGED
|
@@ -267,7 +267,19 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
267
267
|
COUNT(*) as event_count,
|
|
268
268
|
SUM(e.cost_tokens) as total_tokens,
|
|
269
269
|
COUNT(DISTINCT e.session_id) as session_count,
|
|
270
|
-
MAX(e.timestamp) as last_active
|
|
270
|
+
MAX(e.timestamp) as last_active,
|
|
271
|
+
MAX(e.model) as model,
|
|
272
|
+
CASE
|
|
273
|
+
WHEN MAX(e.timestamp) > datetime('now', '-5 minutes') THEN 'active'
|
|
274
|
+
ELSE 'idle'
|
|
275
|
+
END as status,
|
|
276
|
+
AVG(e.execution_duration_seconds) as avg_duration,
|
|
277
|
+
SUM(CASE WHEN e.event_type = 'error' THEN 1 ELSE 0 END) as error_count,
|
|
278
|
+
ROUND(
|
|
279
|
+
100.0 * COUNT(CASE WHEN e.status = 'completed' THEN 1 END) /
|
|
280
|
+
CAST(COUNT(*) AS FLOAT),
|
|
281
|
+
1
|
|
282
|
+
) as success_rate
|
|
271
283
|
FROM agent_events e
|
|
272
284
|
GROUP BY e.agent_id
|
|
273
285
|
ORDER BY event_count DESC
|
|
@@ -297,11 +309,19 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
297
309
|
|
|
298
310
|
agents.append(
|
|
299
311
|
{
|
|
312
|
+
"id": row[0],
|
|
300
313
|
"agent_id": row[0],
|
|
314
|
+
"name": row[0],
|
|
301
315
|
"event_count": event_count,
|
|
302
316
|
"total_tokens": row[2] or 0,
|
|
303
317
|
"session_count": row[3],
|
|
318
|
+
"last_activity": row[4],
|
|
304
319
|
"last_active": row[4],
|
|
320
|
+
"model": row[5] or "unknown",
|
|
321
|
+
"status": row[6] or "idle",
|
|
322
|
+
"avg_duration": row[7],
|
|
323
|
+
"error_count": row[8] or 0,
|
|
324
|
+
"success_rate": row[9] or 0.0,
|
|
305
325
|
"workload_pct": round(workload_pct, 1),
|
|
306
326
|
}
|
|
307
327
|
)
|
|
@@ -1663,22 +1683,29 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1663
1683
|
finally:
|
|
1664
1684
|
await db.close()
|
|
1665
1685
|
|
|
1666
|
-
# ==========
|
|
1686
|
+
# ========== WORK ITEMS ENDPOINTS ==========
|
|
1667
1687
|
|
|
1668
1688
|
@app.get("/views/features", response_class=HTMLResponse)
|
|
1669
|
-
async def
|
|
1670
|
-
|
|
1689
|
+
async def features_view_redirect(
|
|
1690
|
+
request: Request, status: str = "all"
|
|
1691
|
+
) -> HTMLResponse:
|
|
1692
|
+
"""Redirect to work-items view (legacy endpoint for backward compatibility)."""
|
|
1693
|
+
return await work_items_view(request, status)
|
|
1694
|
+
|
|
1695
|
+
@app.get("/views/work-items", response_class=HTMLResponse)
|
|
1696
|
+
async def work_items_view(request: Request, status: str = "all") -> HTMLResponse:
|
|
1697
|
+
"""Get work items (features, bugs, spikes) by status as HTMX partial."""
|
|
1671
1698
|
db = await get_db()
|
|
1672
1699
|
cache = app.state.query_cache
|
|
1673
1700
|
query_start_time = time.time()
|
|
1674
1701
|
|
|
1675
1702
|
try:
|
|
1676
1703
|
# Create cache key from query parameters
|
|
1677
|
-
cache_key = f"
|
|
1704
|
+
cache_key = f"work_items_view:{status}"
|
|
1678
1705
|
|
|
1679
1706
|
# Check cache first
|
|
1680
1707
|
cached_response = cache.get(cache_key)
|
|
1681
|
-
|
|
1708
|
+
work_items_by_status: dict = {
|
|
1682
1709
|
"todo": [],
|
|
1683
1710
|
"in_progress": [],
|
|
1684
1711
|
"blocked": [],
|
|
@@ -1689,9 +1716,9 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1689
1716
|
query_time_ms = (time.time() - query_start_time) * 1000
|
|
1690
1717
|
cache.record_metric(cache_key, query_time_ms, cache_hit=True)
|
|
1691
1718
|
logger.debug(
|
|
1692
|
-
f"Cache HIT for
|
|
1719
|
+
f"Cache HIT for work_items_view (key={cache_key}, time={query_time_ms:.2f}ms)"
|
|
1693
1720
|
)
|
|
1694
|
-
|
|
1721
|
+
work_items_by_status = cached_response
|
|
1695
1722
|
else:
|
|
1696
1723
|
# OPTIMIZATION: Use composite index idx_features_status_priority
|
|
1697
1724
|
# for efficient filtering and ordering
|
|
@@ -1733,37 +1760,37 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1733
1760
|
exec_time_ms = (time.time() - exec_start) * 1000
|
|
1734
1761
|
|
|
1735
1762
|
for row in rows:
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1763
|
+
item_id = row[0]
|
|
1764
|
+
item_status = row[3]
|
|
1765
|
+
work_items_by_status.setdefault(item_status, []).append(
|
|
1739
1766
|
{
|
|
1740
|
-
"id":
|
|
1767
|
+
"id": item_id,
|
|
1741
1768
|
"type": row[1],
|
|
1742
1769
|
"title": row[2],
|
|
1743
|
-
"status":
|
|
1770
|
+
"status": item_status,
|
|
1744
1771
|
"priority": row[4],
|
|
1745
1772
|
"assigned_to": row[5],
|
|
1746
1773
|
"created_at": row[6],
|
|
1747
1774
|
"updated_at": row[7],
|
|
1748
1775
|
"description": row[8],
|
|
1749
|
-
"contributors": feature_agents.get(
|
|
1776
|
+
"contributors": feature_agents.get(item_id, []),
|
|
1750
1777
|
}
|
|
1751
1778
|
)
|
|
1752
1779
|
|
|
1753
1780
|
# Cache the results
|
|
1754
|
-
cache.set(cache_key,
|
|
1781
|
+
cache.set(cache_key, work_items_by_status)
|
|
1755
1782
|
query_time_ms = (time.time() - query_start_time) * 1000
|
|
1756
1783
|
cache.record_metric(cache_key, exec_time_ms, cache_hit=False)
|
|
1757
1784
|
logger.debug(
|
|
1758
|
-
f"Cache MISS for
|
|
1785
|
+
f"Cache MISS for work_items_view (key={cache_key}, "
|
|
1759
1786
|
f"db_time={exec_time_ms:.2f}ms, total_time={query_time_ms:.2f}ms)"
|
|
1760
1787
|
)
|
|
1761
1788
|
|
|
1762
1789
|
return templates.TemplateResponse(
|
|
1763
|
-
"partials/
|
|
1790
|
+
"partials/work-items.html",
|
|
1764
1791
|
{
|
|
1765
1792
|
"request": request,
|
|
1766
|
-
"
|
|
1793
|
+
"work_items_by_status": work_items_by_status,
|
|
1767
1794
|
},
|
|
1768
1795
|
)
|
|
1769
1796
|
finally:
|
|
@@ -1830,19 +1857,19 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1830
1857
|
else:
|
|
1831
1858
|
# OPTIMIZATION: Combine session data with event counts in single query
|
|
1832
1859
|
# This eliminates N+1 query problem (was 20+ queries, now 2)
|
|
1833
|
-
# Note: Database uses
|
|
1860
|
+
# Note: Database uses created_at and completed_at (not started_at/ended_at)
|
|
1834
1861
|
query = """
|
|
1835
1862
|
SELECT
|
|
1836
1863
|
s.session_id,
|
|
1837
1864
|
s.agent_assigned,
|
|
1838
1865
|
s.status,
|
|
1839
|
-
s.
|
|
1840
|
-
s.
|
|
1866
|
+
s.created_at,
|
|
1867
|
+
s.completed_at,
|
|
1841
1868
|
COUNT(DISTINCT e.event_id) as event_count
|
|
1842
1869
|
FROM sessions s
|
|
1843
1870
|
LEFT JOIN agent_events e ON s.session_id = e.session_id
|
|
1844
1871
|
GROUP BY s.session_id
|
|
1845
|
-
ORDER BY s.
|
|
1872
|
+
ORDER BY s.created_at DESC
|
|
1846
1873
|
LIMIT 20
|
|
1847
1874
|
"""
|
|
1848
1875
|
|
|
@@ -1860,7 +1887,13 @@ def get_app(db_path: str) -> FastAPI:
|
|
|
1860
1887
|
ended_at = datetime.fromisoformat(row[4])
|
|
1861
1888
|
duration_seconds = (ended_at - started_at).total_seconds()
|
|
1862
1889
|
else:
|
|
1863
|
-
|
|
1890
|
+
# Use UTC to handle timezone-aware datetime comparison
|
|
1891
|
+
now = (
|
|
1892
|
+
datetime.now(started_at.tzinfo)
|
|
1893
|
+
if started_at.tzinfo
|
|
1894
|
+
else datetime.now()
|
|
1895
|
+
)
|
|
1896
|
+
duration_seconds = (now - started_at).total_seconds()
|
|
1864
1897
|
|
|
1865
1898
|
sessions.append(
|
|
1866
1899
|
{
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
<span class="tab-icon">◊</span>
|
|
54
54
|
ORCHESTRATION
|
|
55
55
|
</button>
|
|
56
|
-
<button class="tab-button" data-tab="
|
|
57
|
-
hx-get="/views/
|
|
56
|
+
<button class="tab-button" data-tab="work-items"
|
|
57
|
+
hx-get="/views/work-items"
|
|
58
58
|
hx-target="#content-area"
|
|
59
59
|
hx-trigger="click">
|
|
60
60
|
<span class="tab-icon">█</span>
|
|
61
|
-
|
|
61
|
+
WORK ITEMS
|
|
62
62
|
</button>
|
|
63
63
|
<button class="tab-button" data-tab="agents"
|
|
64
64
|
hx-get="/views/agents"
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
Orchestration
|
|
53
53
|
</button>
|
|
54
54
|
<button class="tab-button"
|
|
55
|
-
hx-get="/views/
|
|
55
|
+
hx-get="/views/work-items"
|
|
56
56
|
hx-target="#content-area"
|
|
57
57
|
hx-trigger="click"
|
|
58
|
-
data-tab="
|
|
58
|
+
data-tab="work-items">
|
|
59
59
|
<span class="tab-icon">🎯</span>
|
|
60
|
-
|
|
60
|
+
Work Items
|
|
61
61
|
</button>
|
|
62
62
|
<button class="tab-button"
|
|
63
63
|
hx-get="/views/agents"
|