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.
- htmlgraph/__init__.py +23 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/cli.py +3 -3
- htmlgraph/analytics/cost_analyzer.py +5 -1
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/main.py +114 -51
- 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/attribute_index.py +2 -1
- htmlgraph/builders/base.py +2 -1
- htmlgraph/builders/bug.py +2 -1
- htmlgraph/builders/chore.py +2 -1
- htmlgraph/builders/epic.py +2 -1
- htmlgraph/builders/feature.py +2 -1
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +2 -1
- htmlgraph/builders/spike.py +2 -1
- htmlgraph/builders/track.py +28 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +33 -8
- htmlgraph/cli/core.py +2 -1
- htmlgraph/cli/main.py +2 -1
- htmlgraph/cli/models.py +2 -1
- htmlgraph/cli/templates/cost_dashboard.py +2 -1
- htmlgraph/cli/work/__init__.py +76 -1
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +2 -1
- htmlgraph/cli/work/orchestration.py +2 -1
- htmlgraph/cli/work/report.py +2 -1
- htmlgraph/cli/work/sessions.py +2 -1
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +43 -4
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +12 -7
- htmlgraph/collections/spike.py +6 -1
- htmlgraph/collections/task_delegation.py +7 -2
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +11 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +81 -66
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +6 -2
- htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
- htmlgraph/hooks/drift_handler.py +3 -3
- htmlgraph/hooks/event_tracker.py +40 -61
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +92 -14
- htmlgraph/hooks/orchestrator_reflector.py +4 -0
- htmlgraph/hooks/post_tool_use_failure.py +7 -3
- htmlgraph/hooks/posttooluse.py +4 -0
- htmlgraph/hooks/prompt_analyzer.py +5 -5
- htmlgraph/hooks/session_handler.py +5 -2
- htmlgraph/hooks/session_summary.py +6 -2
- htmlgraph/hooks/validator.py +8 -4
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +2 -1
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +18 -1
- htmlgraph/operations/analytics.py +2 -1
- htmlgraph/operations/bootstrap.py +2 -1
- htmlgraph/operations/events.py +2 -1
- htmlgraph/operations/fastapi_server.py +2 -1
- htmlgraph/operations/hooks.py +2 -1
- htmlgraph/operations/initialization.py +2 -1
- htmlgraph/operations/server.py +2 -1
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/claude_launcher.py +23 -20
- htmlgraph/orchestration/command_builder.py +2 -1
- htmlgraph/orchestration/headless_spawner.py +6 -2
- htmlgraph/orchestration/model_selection.py +7 -3
- htmlgraph/orchestration/plugin_manager.py +25 -21
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/claude.py +5 -2
- htmlgraph/orchestration/spawners/codex.py +12 -19
- htmlgraph/orchestration/spawners/copilot.py +13 -18
- htmlgraph/orchestration/spawners/gemini.py +12 -19
- htmlgraph/orchestration/subprocess_runner.py +6 -3
- htmlgraph/orchestration/task_coordination.py +16 -8
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/parallel.py +2 -1
- htmlgraph/query_builder.py +2 -1
- htmlgraph/reflection.py +2 -1
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +2 -1
- 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/server.py +21 -17
- htmlgraph/session_manager.py +1 -7
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +10 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +14 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
- htmlgraph/sdk.py +0 -3430
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
418
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
600
|
-
|
|
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
|
-
|
|
639
|
-
|
|
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
|
-
|
|
790
|
-
|
|
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
|
-
|
|
835
|
-
|
|
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
|
-
|
|
886
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1332
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
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
|
-
# ==========
|
|
1716
|
+
# ========== WORK ITEMS ENDPOINTS ==========
|
|
1667
1717
|
|
|
1668
1718
|
@app.get("/views/features", response_class=HTMLResponse)
|
|
1669
|
-
async def
|
|
1670
|
-
|
|
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"
|
|
1734
|
+
cache_key = f"work_items_view:{status}"
|
|
1678
1735
|
|
|
1679
1736
|
# Check cache first
|
|
1680
1737
|
cached_response = cache.get(cache_key)
|
|
1681
|
-
|
|
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
|
|
1749
|
+
f"Cache HIT for work_items_view (key={cache_key}, time={query_time_ms:.2f}ms)"
|
|
1693
1750
|
)
|
|
1694
|
-
|
|
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
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1793
|
+
item_id = row[0]
|
|
1794
|
+
item_status = row[3]
|
|
1795
|
+
work_items_by_status.setdefault(item_status, []).append(
|
|
1739
1796
|
{
|
|
1740
|
-
"id":
|
|
1797
|
+
"id": item_id,
|
|
1741
1798
|
"type": row[1],
|
|
1742
1799
|
"title": row[2],
|
|
1743
|
-
"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(
|
|
1806
|
+
"contributors": feature_agents.get(item_id, []),
|
|
1750
1807
|
}
|
|
1751
1808
|
)
|
|
1752
1809
|
|
|
1753
1810
|
# Cache the results
|
|
1754
|
-
cache.set(cache_key,
|
|
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
|
|
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/
|
|
1820
|
+
"partials/work-items.html",
|
|
1764
1821
|
{
|
|
1765
1822
|
"request": request,
|
|
1766
|
-
"
|
|
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
|
|
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.
|
|
1840
|
-
s.
|
|
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.
|
|
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
|
-
|
|
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="
|
|
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"
|