htmlgraph 0.26.24__py3-none-any.whl → 0.27.0__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 (155) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cross_session.py +13 -9
  7. htmlgraph/analytics/dependency.py +10 -6
  8. htmlgraph/analytics/work_type.py +15 -11
  9. htmlgraph/analytics_index.py +2 -1
  10. htmlgraph/api/main.py +114 -51
  11. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  12. htmlgraph/api/templates/dashboard.html +3 -3
  13. htmlgraph/api/templates/partials/work-items.html +613 -0
  14. htmlgraph/attribute_index.py +2 -1
  15. htmlgraph/builders/base.py +2 -1
  16. htmlgraph/builders/bug.py +2 -1
  17. htmlgraph/builders/chore.py +2 -1
  18. htmlgraph/builders/epic.py +2 -1
  19. htmlgraph/builders/feature.py +2 -1
  20. htmlgraph/builders/insight.py +2 -1
  21. htmlgraph/builders/metric.py +2 -1
  22. htmlgraph/builders/pattern.py +2 -1
  23. htmlgraph/builders/phase.py +2 -1
  24. htmlgraph/builders/spike.py +2 -1
  25. htmlgraph/builders/track.py +28 -1
  26. htmlgraph/cli/analytics.py +2 -1
  27. htmlgraph/cli/base.py +33 -8
  28. htmlgraph/cli/core.py +2 -1
  29. htmlgraph/cli/main.py +2 -1
  30. htmlgraph/cli/models.py +2 -1
  31. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  32. htmlgraph/cli/work/__init__.py +76 -1
  33. htmlgraph/cli/work/browse.py +115 -0
  34. htmlgraph/cli/work/features.py +2 -1
  35. htmlgraph/cli/work/orchestration.py +2 -1
  36. htmlgraph/cli/work/report.py +2 -1
  37. htmlgraph/cli/work/sessions.py +2 -1
  38. htmlgraph/cli/work/snapshot.py +559 -0
  39. htmlgraph/cli/work/tracks.py +2 -1
  40. htmlgraph/collections/base.py +43 -4
  41. htmlgraph/collections/bug.py +2 -1
  42. htmlgraph/collections/chore.py +2 -1
  43. htmlgraph/collections/epic.py +2 -1
  44. htmlgraph/collections/feature.py +2 -1
  45. htmlgraph/collections/insight.py +2 -1
  46. htmlgraph/collections/metric.py +2 -1
  47. htmlgraph/collections/pattern.py +2 -1
  48. htmlgraph/collections/phase.py +2 -1
  49. htmlgraph/collections/session.py +12 -7
  50. htmlgraph/collections/spike.py +6 -1
  51. htmlgraph/collections/task_delegation.py +7 -2
  52. htmlgraph/collections/todo.py +14 -1
  53. htmlgraph/collections/traces.py +15 -10
  54. htmlgraph/context_analytics.py +2 -1
  55. htmlgraph/converter.py +11 -0
  56. htmlgraph/dependency_models.py +2 -1
  57. htmlgraph/edge_index.py +2 -1
  58. htmlgraph/event_log.py +81 -66
  59. htmlgraph/event_migration.py +2 -1
  60. htmlgraph/file_watcher.py +12 -8
  61. htmlgraph/find_api.py +2 -1
  62. htmlgraph/git_events.py +6 -2
  63. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  64. htmlgraph/hooks/drift_handler.py +3 -3
  65. htmlgraph/hooks/event_tracker.py +40 -61
  66. htmlgraph/hooks/installer.py +5 -1
  67. htmlgraph/hooks/orchestrator.py +92 -14
  68. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  69. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  70. htmlgraph/hooks/posttooluse.py +4 -0
  71. htmlgraph/hooks/prompt_analyzer.py +5 -5
  72. htmlgraph/hooks/session_handler.py +5 -2
  73. htmlgraph/hooks/session_summary.py +6 -2
  74. htmlgraph/hooks/validator.py +8 -4
  75. htmlgraph/ids.py +2 -1
  76. htmlgraph/learning.py +2 -1
  77. htmlgraph/mcp_server.py +2 -1
  78. htmlgraph/models.py +18 -1
  79. htmlgraph/operations/analytics.py +2 -1
  80. htmlgraph/operations/bootstrap.py +2 -1
  81. htmlgraph/operations/events.py +2 -1
  82. htmlgraph/operations/fastapi_server.py +2 -1
  83. htmlgraph/operations/hooks.py +2 -1
  84. htmlgraph/operations/initialization.py +2 -1
  85. htmlgraph/operations/server.py +2 -1
  86. htmlgraph/orchestration/__init__.py +4 -0
  87. htmlgraph/orchestration/claude_launcher.py +23 -20
  88. htmlgraph/orchestration/command_builder.py +2 -1
  89. htmlgraph/orchestration/headless_spawner.py +6 -2
  90. htmlgraph/orchestration/model_selection.py +7 -3
  91. htmlgraph/orchestration/plugin_manager.py +25 -21
  92. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  93. htmlgraph/orchestration/spawners/claude.py +5 -2
  94. htmlgraph/orchestration/spawners/codex.py +12 -19
  95. htmlgraph/orchestration/spawners/copilot.py +13 -18
  96. htmlgraph/orchestration/spawners/gemini.py +12 -19
  97. htmlgraph/orchestration/subprocess_runner.py +6 -3
  98. htmlgraph/orchestration/task_coordination.py +16 -8
  99. htmlgraph/orchestrator.py +2 -1
  100. htmlgraph/parallel.py +2 -1
  101. htmlgraph/query_builder.py +2 -1
  102. htmlgraph/reflection.py +2 -1
  103. htmlgraph/refs.py +344 -0
  104. htmlgraph/repo_hash.py +2 -1
  105. htmlgraph/sdk/__init__.py +398 -0
  106. htmlgraph/sdk/__init__.pyi +14 -0
  107. htmlgraph/sdk/analytics/__init__.py +19 -0
  108. htmlgraph/sdk/analytics/engine.py +155 -0
  109. htmlgraph/sdk/analytics/helpers.py +178 -0
  110. htmlgraph/sdk/analytics/registry.py +109 -0
  111. htmlgraph/sdk/base.py +484 -0
  112. htmlgraph/sdk/constants.py +216 -0
  113. htmlgraph/sdk/core.pyi +308 -0
  114. htmlgraph/sdk/discovery.py +120 -0
  115. htmlgraph/sdk/help/__init__.py +12 -0
  116. htmlgraph/sdk/help/mixin.py +699 -0
  117. htmlgraph/sdk/mixins/__init__.py +15 -0
  118. htmlgraph/sdk/mixins/attribution.py +113 -0
  119. htmlgraph/sdk/mixins/mixin.py +410 -0
  120. htmlgraph/sdk/operations/__init__.py +12 -0
  121. htmlgraph/sdk/operations/mixin.py +427 -0
  122. htmlgraph/sdk/orchestration/__init__.py +17 -0
  123. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  124. htmlgraph/sdk/orchestration/spawner.py +204 -0
  125. htmlgraph/sdk/planning/__init__.py +19 -0
  126. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  127. htmlgraph/sdk/planning/mixin.py +211 -0
  128. htmlgraph/sdk/planning/parallel.py +186 -0
  129. htmlgraph/sdk/planning/queue.py +210 -0
  130. htmlgraph/sdk/planning/recommendations.py +87 -0
  131. htmlgraph/sdk/planning/smart_planning.py +319 -0
  132. htmlgraph/sdk/session/__init__.py +19 -0
  133. htmlgraph/sdk/session/continuity.py +57 -0
  134. htmlgraph/sdk/session/handoff.py +110 -0
  135. htmlgraph/sdk/session/info.py +309 -0
  136. htmlgraph/sdk/session/manager.py +103 -0
  137. htmlgraph/server.py +21 -17
  138. htmlgraph/session_manager.py +1 -7
  139. htmlgraph/session_warning.py +2 -1
  140. htmlgraph/sessions/handoff.py +10 -3
  141. htmlgraph/system_prompts.py +2 -1
  142. htmlgraph/track_builder.py +14 -1
  143. htmlgraph/transcript.py +2 -1
  144. htmlgraph/watch.py +2 -1
  145. htmlgraph/work_type_utils.py +2 -1
  146. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
  147. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
  148. htmlgraph/sdk.py +0 -3430
  149. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
  150. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
  151. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  152. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  153. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  154. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
  155. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
htmlgraph/api/main.py CHANGED
@@ -204,7 +204,37 @@ def get_app(db_path: str) -> FastAPI:
204
204
  return "0"
205
205
  return f"{value:,}"
206
206
 
207
+ def format_duration(seconds: float | int | None) -> str:
208
+ """Format duration in seconds to human-readable string."""
209
+ if seconds is None:
210
+ return "0.00s"
211
+ return f"{float(seconds):.2f}s"
212
+
213
+ def format_bytes(bytes_size: int | float | None) -> str:
214
+ """Format bytes to MB with 2 decimal places."""
215
+ if bytes_size is None:
216
+ return "0.00MB"
217
+ return f"{int(bytes_size) / (1024 * 1024):.2f}MB"
218
+
219
+ def truncate_text(text: str | None, length: int = 50) -> str:
220
+ """Truncate text to specified length with ellipsis."""
221
+ if text is None:
222
+ return ""
223
+ return text[:length] + "..." if len(text) > length else text
224
+
225
+ def format_timestamp(ts: Any) -> str:
226
+ """Format timestamp to readable string."""
227
+ if ts is None:
228
+ return ""
229
+ if hasattr(ts, "strftime"):
230
+ return str(ts.strftime("%Y-%m-%d %H:%M:%S"))
231
+ return str(ts)
232
+
207
233
  templates.env.filters["format_number"] = format_number
234
+ templates.env.filters["format_duration"] = format_duration
235
+ templates.env.filters["format_bytes"] = format_bytes
236
+ templates.env.filters["truncate"] = truncate_text
237
+ templates.env.filters["format_timestamp"] = format_timestamp
208
238
 
209
239
  # Setup static files
210
240
  static_dir = Path(__file__).parent / "static"
@@ -267,7 +297,19 @@ def get_app(db_path: str) -> FastAPI:
267
297
  COUNT(*) as event_count,
268
298
  SUM(e.cost_tokens) as total_tokens,
269
299
  COUNT(DISTINCT e.session_id) as session_count,
270
- MAX(e.timestamp) as last_active
300
+ MAX(e.timestamp) as last_active,
301
+ MAX(e.model) as model,
302
+ CASE
303
+ WHEN MAX(e.timestamp) > datetime('now', '-5 minutes') THEN 'active'
304
+ ELSE 'idle'
305
+ END as status,
306
+ AVG(e.execution_duration_seconds) as avg_duration,
307
+ SUM(CASE WHEN e.event_type = 'error' THEN 1 ELSE 0 END) as error_count,
308
+ ROUND(
309
+ 100.0 * COUNT(CASE WHEN e.status = 'completed' THEN 1 END) /
310
+ CAST(COUNT(*) AS FLOAT),
311
+ 1
312
+ ) as success_rate
271
313
  FROM agent_events e
272
314
  GROUP BY e.agent_id
273
315
  ORDER BY event_count DESC
@@ -275,8 +317,8 @@ def get_app(db_path: str) -> FastAPI:
275
317
 
276
318
  # Execute query with timing
277
319
  exec_start = time.time()
278
- cursor = await db.execute(query)
279
- rows = await cursor.fetchall()
320
+ async with db.execute(query) as cursor:
321
+ rows = await cursor.fetchall()
280
322
  exec_time_ms = (time.time() - exec_start) * 1000
281
323
 
282
324
  agents = []
@@ -297,11 +339,19 @@ def get_app(db_path: str) -> FastAPI:
297
339
 
298
340
  agents.append(
299
341
  {
342
+ "id": row[0],
300
343
  "agent_id": row[0],
344
+ "name": row[0],
301
345
  "event_count": event_count,
302
346
  "total_tokens": row[2] or 0,
303
347
  "session_count": row[3],
348
+ "last_activity": row[4],
304
349
  "last_active": row[4],
350
+ "model": row[5] or "unknown",
351
+ "status": row[6] or "idle",
352
+ "avg_duration": row[7],
353
+ "error_count": row[8] or 0,
354
+ "success_rate": row[9] or 0.0,
305
355
  "workload_pct": round(workload_pct, 1),
306
356
  }
307
357
  )
@@ -414,8 +464,8 @@ def get_app(db_path: str) -> FastAPI:
414
464
 
415
465
  # Execute query with timing
416
466
  exec_start = time.time()
417
- cursor = await db.execute(query, params)
418
- rows = await cursor.fetchall()
467
+ async with db.execute(query, params) as cursor:
468
+ rows = await cursor.fetchall()
419
469
  exec_time_ms = (time.time() - exec_start) * 1000
420
470
 
421
471
  # Build result models
@@ -465,15 +515,15 @@ def get_app(db_path: str) -> FastAPI:
465
515
  (SELECT COUNT(DISTINCT agent_id) FROM agent_events) as total_agents,
466
516
  (SELECT COUNT(*) FROM sessions) as total_sessions
467
517
  """
468
- cursor = await db.execute(stats_query)
469
- row = await cursor.fetchone()
518
+ async with db.execute(stats_query) as cursor:
519
+ row = await cursor.fetchone()
470
520
 
471
521
  # Query distinct agent IDs for the agent set
472
522
  agents_query = (
473
523
  "SELECT DISTINCT agent_id FROM agent_events WHERE agent_id IS NOT NULL"
474
524
  )
475
- agents_cursor = await db.execute(agents_query)
476
- agents_rows = await agents_cursor.fetchall()
525
+ async with db.execute(agents_query) as agents_cursor:
526
+ agents_rows = await agents_cursor.fetchall()
477
527
  agents = [row[0] for row in agents_rows]
478
528
 
479
529
  if row is None:
@@ -596,8 +646,8 @@ def get_app(db_path: str) -> FastAPI:
596
646
  parent_query += " ORDER BY timestamp DESC LIMIT ?"
597
647
  parent_params.append(limit)
598
648
 
599
- cursor = await db.execute(parent_query, parent_params)
600
- parent_rows = await cursor.fetchall()
649
+ async with db.execute(parent_query, parent_params) as cursor:
650
+ parent_rows = await cursor.fetchall()
601
651
 
602
652
  traces: list[dict[str, Any]] = []
603
653
 
@@ -635,8 +685,8 @@ def get_app(db_path: str) -> FastAPI:
635
685
  WHERE parent_event_id = ?
636
686
  ORDER BY timestamp ASC
637
687
  """
638
- child_cursor = await db.execute(child_query, (parent_event_id,))
639
- child_rows = await child_cursor.fetchall()
688
+ async with db.execute(child_query, (parent_event_id,)) as child_cursor:
689
+ child_rows = await child_cursor.fetchall()
640
690
 
641
691
  child_events = []
642
692
  for child_row in child_rows:
@@ -786,8 +836,8 @@ def get_app(db_path: str) -> FastAPI:
786
836
  params.append(limit)
787
837
 
788
838
  exec_start = time.time()
789
- cursor = await db.execute(query, params)
790
- rows = await cursor.fetchall()
839
+ async with db.execute(query, params) as cursor:
840
+ rows = await cursor.fetchall()
791
841
 
792
842
  for row in rows:
793
843
  events.append(
@@ -831,8 +881,8 @@ def get_app(db_path: str) -> FastAPI:
831
881
  spike_query += " ORDER BY created_at DESC LIMIT ?"
832
882
  spike_params.append(limit)
833
883
 
834
- spike_cursor = await db.execute(spike_query, spike_params)
835
- spike_rows = await spike_cursor.fetchall()
884
+ async with db.execute(spike_query, spike_params) as spike_cursor:
885
+ spike_rows = await spike_cursor.fetchall()
836
886
 
837
887
  for row in spike_rows:
838
888
  events.append(
@@ -882,8 +932,8 @@ def get_app(db_path: str) -> FastAPI:
882
932
  collab_query += " ORDER BY timestamp DESC LIMIT ?"
883
933
  collab_params.append(limit)
884
934
 
885
- collab_cursor = await db.execute(collab_query, collab_params)
886
- collab_rows = await collab_cursor.fetchall()
935
+ async with db.execute(collab_query, collab_params) as collab_cursor:
936
+ collab_rows = await collab_cursor.fetchall()
887
937
 
888
938
  for row in collab_rows:
889
939
  events.append(
@@ -996,8 +1046,8 @@ def get_app(db_path: str) -> FastAPI:
996
1046
  LIMIT ?
997
1047
  """
998
1048
 
999
- cursor = await db.execute(user_query_query, [limit])
1000
- user_query_rows = await cursor.fetchall()
1049
+ async with db.execute(user_query_query, [limit]) as cursor:
1050
+ user_query_rows = await cursor.fetchall()
1001
1051
 
1002
1052
  conversation_turns: list[dict[str, Any]] = []
1003
1053
 
@@ -1041,8 +1091,8 @@ def get_app(db_path: str) -> FastAPI:
1041
1091
  if depth >= max_depth:
1042
1092
  return [], 0.0, 0, 0
1043
1093
 
1044
- cursor = await db.execute(children_query, [parent_id])
1045
- rows = await cursor.fetchall()
1094
+ async with db.execute(children_query, [parent_id]) as cursor:
1095
+ rows = await cursor.fetchall()
1046
1096
 
1047
1097
  children_list: list[dict[str, Any]] = []
1048
1098
  total_dur = 0.0
@@ -1318,8 +1368,8 @@ def get_app(db_path: str) -> FastAPI:
1318
1368
  query += " ORDER BY started_at DESC LIMIT ? OFFSET ?"
1319
1369
  params.extend([limit, offset])
1320
1370
 
1321
- cursor = await db.execute(query, params)
1322
- rows = await cursor.fetchall()
1371
+ async with db.execute(query, params) as cursor:
1372
+ rows = await cursor.fetchall()
1323
1373
 
1324
1374
  # Get total count for pagination
1325
1375
  count_query = "SELECT COUNT(*) FROM sessions WHERE 1=1"
@@ -1328,8 +1378,8 @@ def get_app(db_path: str) -> FastAPI:
1328
1378
  count_query += " AND status = ?"
1329
1379
  count_params.append(status)
1330
1380
 
1331
- count_cursor = await db.execute(count_query, count_params)
1332
- count_row = await count_cursor.fetchone()
1381
+ async with db.execute(count_query, count_params) as count_cursor:
1382
+ count_row = await count_cursor.fetchone()
1333
1383
  total = int(count_row[0]) if count_row else 0
1334
1384
 
1335
1385
  # Build session objects
@@ -1396,8 +1446,8 @@ def get_app(db_path: str) -> FastAPI:
1396
1446
  LIMIT 50
1397
1447
  """
1398
1448
 
1399
- cursor = await db.execute(query)
1400
- rows = list(await cursor.fetchall())
1449
+ async with db.execute(query) as cursor:
1450
+ rows = list(await cursor.fetchall())
1401
1451
  logger.debug(f"orchestration_view: Query executed, got {len(rows)} rows")
1402
1452
 
1403
1453
  delegations = []
@@ -1663,22 +1713,29 @@ def get_app(db_path: str) -> FastAPI:
1663
1713
  finally:
1664
1714
  await db.close()
1665
1715
 
1666
- # ========== FEATURES ENDPOINTS ==========
1716
+ # ========== WORK ITEMS ENDPOINTS ==========
1667
1717
 
1668
1718
  @app.get("/views/features", response_class=HTMLResponse)
1669
- async def features_view(request: Request, status: str = "all") -> HTMLResponse:
1670
- """Get features by status as HTMX partial."""
1719
+ async def features_view_redirect(
1720
+ request: Request, status: str = "all"
1721
+ ) -> HTMLResponse:
1722
+ """Redirect to work-items view (legacy endpoint for backward compatibility)."""
1723
+ return await work_items_view(request, status)
1724
+
1725
+ @app.get("/views/work-items", response_class=HTMLResponse)
1726
+ async def work_items_view(request: Request, status: str = "all") -> HTMLResponse:
1727
+ """Get work items (features, bugs, spikes) by status as HTMX partial."""
1671
1728
  db = await get_db()
1672
1729
  cache = app.state.query_cache
1673
1730
  query_start_time = time.time()
1674
1731
 
1675
1732
  try:
1676
1733
  # Create cache key from query parameters
1677
- cache_key = f"features_view:{status}"
1734
+ cache_key = f"work_items_view:{status}"
1678
1735
 
1679
1736
  # Check cache first
1680
1737
  cached_response = cache.get(cache_key)
1681
- features_by_status: dict = {
1738
+ work_items_by_status: dict = {
1682
1739
  "todo": [],
1683
1740
  "in_progress": [],
1684
1741
  "blocked": [],
@@ -1689,9 +1746,9 @@ def get_app(db_path: str) -> FastAPI:
1689
1746
  query_time_ms = (time.time() - query_start_time) * 1000
1690
1747
  cache.record_metric(cache_key, query_time_ms, cache_hit=True)
1691
1748
  logger.debug(
1692
- f"Cache HIT for features_view (key={cache_key}, time={query_time_ms:.2f}ms)"
1749
+ f"Cache HIT for work_items_view (key={cache_key}, time={query_time_ms:.2f}ms)"
1693
1750
  )
1694
- features_by_status = cached_response
1751
+ work_items_by_status = cached_response
1695
1752
  else:
1696
1753
  # OPTIMIZATION: Use composite index idx_features_status_priority
1697
1754
  # for efficient filtering and ordering
@@ -1733,37 +1790,37 @@ def get_app(db_path: str) -> FastAPI:
1733
1790
  exec_time_ms = (time.time() - exec_start) * 1000
1734
1791
 
1735
1792
  for row in rows:
1736
- feature_id = row[0]
1737
- feature_status = row[3]
1738
- features_by_status.setdefault(feature_status, []).append(
1793
+ item_id = row[0]
1794
+ item_status = row[3]
1795
+ work_items_by_status.setdefault(item_status, []).append(
1739
1796
  {
1740
- "id": feature_id,
1797
+ "id": item_id,
1741
1798
  "type": row[1],
1742
1799
  "title": row[2],
1743
- "status": feature_status,
1800
+ "status": item_status,
1744
1801
  "priority": row[4],
1745
1802
  "assigned_to": row[5],
1746
1803
  "created_at": row[6],
1747
1804
  "updated_at": row[7],
1748
1805
  "description": row[8],
1749
- "contributors": feature_agents.get(feature_id, []),
1806
+ "contributors": feature_agents.get(item_id, []),
1750
1807
  }
1751
1808
  )
1752
1809
 
1753
1810
  # Cache the results
1754
- cache.set(cache_key, features_by_status)
1811
+ cache.set(cache_key, work_items_by_status)
1755
1812
  query_time_ms = (time.time() - query_start_time) * 1000
1756
1813
  cache.record_metric(cache_key, exec_time_ms, cache_hit=False)
1757
1814
  logger.debug(
1758
- f"Cache MISS for features_view (key={cache_key}, "
1815
+ f"Cache MISS for work_items_view (key={cache_key}, "
1759
1816
  f"db_time={exec_time_ms:.2f}ms, total_time={query_time_ms:.2f}ms)"
1760
1817
  )
1761
1818
 
1762
1819
  return templates.TemplateResponse(
1763
- "partials/features.html",
1820
+ "partials/work-items.html",
1764
1821
  {
1765
1822
  "request": request,
1766
- "features_by_status": features_by_status,
1823
+ "work_items_by_status": work_items_by_status,
1767
1824
  },
1768
1825
  )
1769
1826
  finally:
@@ -1830,19 +1887,19 @@ def get_app(db_path: str) -> FastAPI:
1830
1887
  else:
1831
1888
  # OPTIMIZATION: Combine session data with event counts in single query
1832
1889
  # This eliminates N+1 query problem (was 20+ queries, now 2)
1833
- # Note: Database uses agent_assigned but started_at/ended_at (partial migration)
1890
+ # Note: Database uses created_at and completed_at (not started_at/ended_at)
1834
1891
  query = """
1835
1892
  SELECT
1836
1893
  s.session_id,
1837
1894
  s.agent_assigned,
1838
1895
  s.status,
1839
- s.started_at,
1840
- s.ended_at,
1896
+ s.created_at,
1897
+ s.completed_at,
1841
1898
  COUNT(DISTINCT e.event_id) as event_count
1842
1899
  FROM sessions s
1843
1900
  LEFT JOIN agent_events e ON s.session_id = e.session_id
1844
1901
  GROUP BY s.session_id
1845
- ORDER BY s.started_at DESC
1902
+ ORDER BY s.created_at DESC
1846
1903
  LIMIT 20
1847
1904
  """
1848
1905
 
@@ -1860,7 +1917,13 @@ def get_app(db_path: str) -> FastAPI:
1860
1917
  ended_at = datetime.fromisoformat(row[4])
1861
1918
  duration_seconds = (ended_at - started_at).total_seconds()
1862
1919
  else:
1863
- duration_seconds = (datetime.now() - started_at).total_seconds()
1920
+ # Use UTC to handle timezone-aware datetime comparison
1921
+ now = (
1922
+ datetime.now(started_at.tzinfo)
1923
+ if started_at.tzinfo
1924
+ else datetime.now()
1925
+ )
1926
+ duration_seconds = (now - started_at).total_seconds()
1864
1927
 
1865
1928
  sessions.append(
1866
1929
  {
@@ -53,12 +53,12 @@
53
53
  <span class="tab-icon">◊</span>
54
54
  ORCHESTRATION
55
55
  </button>
56
- <button class="tab-button" data-tab="features"
57
- hx-get="/views/features"
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
- FEATURES
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/features"
55
+ hx-get="/views/work-items"
56
56
  hx-target="#content-area"
57
57
  hx-trigger="click"
58
- data-tab="features">
58
+ data-tab="work-items">
59
59
  <span class="tab-icon">🎯</span>
60
- Features
60
+ Work Items
61
61
  </button>
62
62
  <button class="tab-button"
63
63
  hx-get="/views/agents"