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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,794 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ title }} - HtmlGraph</title>
7
+ <script src="/static/htmx.min.js"></script>
8
+ <link rel="stylesheet" href="/static/style.css">
9
+ </head>
10
+ <body class="dark-theme">
11
+ <div class="dashboard-container">
12
+ <!-- Header -->
13
+ <header class="dashboard-header">
14
+ <div class="header-content">
15
+ <h1 class="logo">
16
+ <span class="logo-icon">⚡</span>
17
+ HtmlGraph Dashboard
18
+ </h1>
19
+ <div class="header-stats">
20
+ <div class="stat-badge">
21
+ <span class="stat-label">Events</span>
22
+ <span class="stat-value" id="event-count">0</span>
23
+ </div>
24
+ <div class="stat-badge">
25
+ <span class="stat-label">Agents</span>
26
+ <span class="stat-value" id="agent-count">0</span>
27
+ </div>
28
+ <div class="stat-badge">
29
+ <span class="stat-label">Sessions</span>
30
+ <span class="stat-value" id="session-count">0</span>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </header>
35
+
36
+ <!-- Navigation Tabs -->
37
+ <nav class="tabs-navigation">
38
+ <button class="tab-button active"
39
+ hx-get="/views/activity-feed"
40
+ hx-target="#content-area"
41
+ hx-trigger="click"
42
+ data-tab="activity">
43
+ <span class="tab-icon">📋</span>
44
+ Activity Feed
45
+ </button>
46
+ <button class="tab-button"
47
+ hx-get="/views/orchestration"
48
+ hx-target="#content-area"
49
+ hx-trigger="click"
50
+ data-tab="orchestration">
51
+ <span class="tab-icon">🔗</span>
52
+ Orchestration
53
+ </button>
54
+ <button class="tab-button"
55
+ hx-get="/views/work-items"
56
+ hx-target="#content-area"
57
+ hx-trigger="click"
58
+ data-tab="work-items">
59
+ <span class="tab-icon">🎯</span>
60
+ Work Items
61
+ </button>
62
+ <button class="tab-button"
63
+ hx-get="/views/agents"
64
+ hx-target="#content-area"
65
+ hx-trigger="click"
66
+ data-tab="agents">
67
+ <span class="tab-icon">🤖</span>
68
+ Agents
69
+ </button>
70
+ <button class="tab-button"
71
+ hx-get="/views/metrics"
72
+ hx-target="#content-area"
73
+ hx-trigger="click"
74
+ data-tab="metrics">
75
+ <span class="tab-icon">📊</span>
76
+ Metrics
77
+ </button>
78
+ <button class="tab-button"
79
+ hx-get="/views/spawners"
80
+ hx-target="#content-area"
81
+ hx-trigger="click"
82
+ data-tab="spawners">
83
+ <span class="tab-icon">🚀</span>
84
+ Spawners
85
+ </button>
86
+ </nav>
87
+
88
+ <!-- Content Area -->
89
+ <main class="content-area" id="content-area">
90
+ <div class="loading-indicator">
91
+ <div class="spinner"></div>
92
+ <p>Loading dashboard...</p>
93
+ </div>
94
+ </main>
95
+ </div>
96
+
97
+ <!-- WebSocket for live updates -->
98
+ <script>
99
+ let eventCount = 0;
100
+ let agentSet = new Set();
101
+ let sessionCount = 0;
102
+ let processedEventIds = new Set();
103
+
104
+ // Load initial stats from server
105
+ async function loadInitialStats() {
106
+ try {
107
+ const response = await fetch('/api/initial-stats');
108
+ const data = await response.json();
109
+
110
+ // Update header stats
111
+ eventCount = data.total_events || 0;
112
+ sessionCount = data.total_sessions || 0;
113
+
114
+ // Update agent set from database
115
+ if (data.agents) {
116
+ data.agents.forEach(agent => agentSet.add(agent));
117
+ }
118
+
119
+ // Update UI
120
+ document.getElementById('event-count').textContent = eventCount;
121
+ document.getElementById('agent-count').textContent = agentSet.size;
122
+ document.getElementById('session-count').textContent = sessionCount;
123
+
124
+ console.log('Initial stats loaded:', data);
125
+ } catch (error) {
126
+ console.error('Failed to load initial stats:', error);
127
+ }
128
+ }
129
+
130
+ // Initialize dashboard on load
131
+ document.addEventListener('DOMContentLoaded', function() {
132
+ // Load initial stats
133
+ loadInitialStats();
134
+
135
+ // Load initial activity feed
136
+ htmx.ajax('GET', '/views/activity-feed', {target: '#content-area'});
137
+
138
+ // Connect WebSocket for real-time updates
139
+ connectWebSocket();
140
+ });
141
+
142
+ // Convert timestamps after HTMX loads Activity Feed
143
+ document.body.addEventListener('htmx:afterSettle', function(evt) {
144
+ if (evt.detail.target.id === 'content-area') {
145
+ // Activity Feed has been loaded, convert timestamps to local timezone
146
+ if (typeof convertTimestampsToLocal === 'function') {
147
+ convertTimestampsToLocal();
148
+ }
149
+ }
150
+ });
151
+
152
+ function connectWebSocket() {
153
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
154
+ const ws = new WebSocket(wsProtocol + '//' + window.location.host + '/ws/events');
155
+
156
+ ws.onopen = function(event) {
157
+ console.log('WebSocket connected for real-time events');
158
+ updateWebSocketStatus(true);
159
+ };
160
+
161
+ ws.onmessage = function(event) {
162
+ try {
163
+ const data = JSON.parse(event.data);
164
+ console.log('[WebSocket] Received message type:', data.type);
165
+
166
+ if (data.type === 'event') {
167
+ // Prevent duplicate event insertions
168
+ if (processedEventIds.has(data.event_id)) {
169
+ return;
170
+ }
171
+ processedEventIds.add(data.event_id);
172
+
173
+ // Update stats
174
+ eventCount++;
175
+ if (data.agent_id) {
176
+ agentSet.add(data.agent_id);
177
+ }
178
+
179
+ // Update header
180
+ document.getElementById('event-count').textContent = eventCount;
181
+ document.getElementById('agent-count').textContent = agentSet.size;
182
+
183
+ // Add animation class to event count badge
184
+ const badge = document.getElementById('event-count').parentElement;
185
+ badge.classList.add('pulse');
186
+ setTimeout(() => badge.classList.remove('pulse'), 500);
187
+
188
+ // Insert new event into Activity Feed if visible
189
+ insertNewEventIntoActivityFeed(data);
190
+ }
191
+ // Handle live spawner events for real-time streaming
192
+ else if (data.type === 'spawner_event') {
193
+ console.log('[WebSocket] spawner_event received:', data.event_type, data.spawner_type, 'handler exists:', typeof window.handleSpawnerEvent === 'function');
194
+ // Delegate to activity-feed.html handler if available
195
+ if (typeof window.handleSpawnerEvent === 'function') {
196
+ window.handleSpawnerEvent(data);
197
+ } else {
198
+ console.warn('[WebSocket] handleSpawnerEvent not available, spawner event dropped:', data.event_type, data.spawner_type);
199
+ }
200
+ }
201
+ } catch (e) {
202
+ console.error('WebSocket message error:', e);
203
+ }
204
+ };
205
+
206
+ ws.onerror = function(event) {
207
+ console.error('WebSocket error:', event);
208
+ updateWebSocketStatus(false);
209
+ };
210
+
211
+ ws.onclose = function(event) {
212
+ console.log('WebSocket disconnected, reconnecting in 3s...');
213
+ updateWebSocketStatus(false);
214
+ setTimeout(connectWebSocket, 3000);
215
+ };
216
+ }
217
+
218
+ function updateWebSocketStatus(isConnected) {
219
+ // Update live update indicator in activity feed
220
+ const indicator = document.querySelector('.auto-refresh-indicator');
221
+ if (indicator) {
222
+ const dot = indicator.querySelector('.refresh-dot');
223
+ if (isConnected) {
224
+ dot.style.backgroundColor = '#10b981';
225
+ indicator.style.opacity = '1';
226
+ } else {
227
+ dot.style.backgroundColor = '#ef4444';
228
+ indicator.style.opacity = '0.6';
229
+ }
230
+ }
231
+ }
232
+
233
+ function insertNewEventIntoActivityFeed(eventData) {
234
+ // Check if activity feed is currently displayed
235
+ const activityFeed = document.querySelector('.activity-feed-view');
236
+ if (!activityFeed) {
237
+ // Activity feed not visible, skip insertion
238
+ return;
239
+ }
240
+
241
+ const activityList = activityFeed.querySelector('.activity-list');
242
+ if (!activityList) {
243
+ return;
244
+ }
245
+
246
+ // Check if there's an empty state message
247
+ const emptyState = activityList.querySelector('.empty-state');
248
+ if (emptyState) {
249
+ // Replace empty state with table
250
+ emptyState.remove();
251
+ // Create table if it doesn't exist
252
+ if (!activityList.querySelector('.activity-table')) {
253
+ const table = `
254
+ <table class="activity-table">
255
+ <thead>
256
+ <tr>
257
+ <th class="col-timestamp">Timestamp</th>
258
+ <th class="col-agent">Agent</th>
259
+ <th class="col-tool">Tool</th>
260
+ <th class="col-input">Input</th>
261
+ <th class="col-output">Output</th>
262
+ <th class="col-status">Status</th>
263
+ <th class="col-id">ID</th>
264
+ </tr>
265
+ </thead>
266
+ <tbody></tbody>
267
+ </table>
268
+ `;
269
+ activityList.insertAdjacentHTML('afterbegin', table);
270
+ }
271
+ }
272
+
273
+ // Get or create tbody
274
+ const table = activityList.querySelector('.activity-table');
275
+ if (!table) {
276
+ return;
277
+ }
278
+
279
+ const tbody = table.querySelector('tbody');
280
+ if (!tbody) {
281
+ return;
282
+ }
283
+
284
+ // Create new table row HTML
285
+ const eventRow = createActivityRowHTML(eventData);
286
+
287
+ // Handle hierarchical placement
288
+ if (eventData.parent_event_id) {
289
+ // Try to find parent row
290
+ const parentRow = tbody.querySelector(`tr[data-event-id="${eventData.parent_event_id}"]`);
291
+ if (parentRow) {
292
+ // Insert after parent row
293
+ parentRow.insertAdjacentHTML('afterend', eventRow);
294
+ highlightRow(tbody.querySelector(`tr[data-event-id="${eventData.event_id}"]`));
295
+ return;
296
+ }
297
+ }
298
+
299
+ // Default: Insert at the top of tbody
300
+ const firstRow = tbody.querySelector('tr');
301
+ if (firstRow) {
302
+ firstRow.insertAdjacentHTML('beforebegin', eventRow);
303
+ } else {
304
+ tbody.insertAdjacentHTML('afterbegin', eventRow);
305
+ }
306
+
307
+ // Highlight the new row
308
+ highlightRow(tbody.querySelector('tr:first-child'));
309
+
310
+ // Convert new event timestamp to local timezone
311
+ if (typeof convertTimestampsToLocal === 'function') {
312
+ convertTimestampsToLocal();
313
+ }
314
+
315
+ // Keep only last 100 events in feed to prevent memory issues
316
+ const allRows = tbody.querySelectorAll('tr');
317
+ if (allRows.length > 100) {
318
+ // Remove oldest items from bottom
319
+ const itemsToRemove = allRows.length - 100;
320
+ for (let i = 0; i < itemsToRemove; i++) {
321
+ allRows[allRows.length - 1 - i].remove();
322
+ }
323
+ }
324
+ }
325
+
326
+ function highlightRow(row) {
327
+ if (row) {
328
+ row.classList.add('new-event-highlight');
329
+ // Remove highlight after animation
330
+ setTimeout(() => {
331
+ row.classList.remove('new-event-highlight');
332
+ }, 2000);
333
+ }
334
+ }
335
+
336
+ function createActivityRowHTML(eventData) {
337
+ // Determine event type emoji
338
+ let eventEmoji = '📋';
339
+ if (eventData.event_type === 'delegation') {
340
+ eventEmoji = '🔗';
341
+ } else if (eventData.event_type === 'tool_call') {
342
+ eventEmoji = '🔨';
343
+ } else if (eventData.event_type === 'completion') {
344
+ eventEmoji = '🎉';
345
+ } else if (eventData.event_type === 'tool_result') {
346
+ eventEmoji = '✅';
347
+ } else if (eventData.event_type === 'error') {
348
+ eventEmoji = '❌';
349
+ }
350
+
351
+ const inputSummary = eventData.input_summary ? eventData.input_summary.substring(0, 100) : '';
352
+ const inputTruncated = eventData.input_summary && eventData.input_summary.length > 100 ? '…' : '';
353
+ const outputSummary = eventData.output_summary ? eventData.output_summary.substring(0, 100) : '';
354
+ const outputTruncated = eventData.output_summary && eventData.output_summary.length > 100 ? '…' : '';
355
+
356
+ const isChild = !!eventData.parent_event_id;
357
+ const rowClass = isChild ? 'child-row' : 'parent-row';
358
+ const indentStyle = isChild ? 'padding-left: 2rem;' : '';
359
+ const borderStyle = isChild ? 'border-left: 4px solid var(--text-muted);' : 'border-left: 4px solid var(--accent);';
360
+
361
+ // Create table row HTML
362
+ const html = `
363
+ <tr class="activity-row ${rowClass} event-${eventData.status || 'pending'}"
364
+ data-event-id="${escapeHtml(eventData.event_id)}"
365
+ style="${borderStyle}">
366
+ <td class="col-timestamp" style="${indentStyle}">
367
+ <span class="event-type-badge" title="${escapeHtml(eventData.event_type)}">
368
+ ${eventEmoji}
369
+ </span>
370
+ <span class="timestamp-text" data-utc-time="${escapeHtml(eventData.timestamp)}">${escapeHtml(eventData.timestamp)}</span>
371
+ </td>
372
+ <td class="col-agent">
373
+ <span class="agent-badge agent-${escapeHtml(eventData.agent_id.toLowerCase())}">${escapeHtml(eventData.agent_id)}</span>
374
+ ${isChild ? `<span class="child-indicator" title="Child of ${escapeHtml(eventData.parent_event_id.substring(0, 8))}">↳</span>` : ''}
375
+ </td>
376
+ <td class="col-tool">
377
+ ${eventData.tool_name ? `<code class="tool-name">${escapeHtml(eventData.tool_name)}</code>` : '<span class="text-muted">—</span>'}
378
+ </td>
379
+ <td class="col-input">
380
+ ${inputSummary ? `<span class="truncate" title="${escapeHtml(eventData.input_summary)}">${escapeHtml(inputSummary)}${inputTruncated}</span>` : '<span class="text-muted">—</span>'}
381
+ </td>
382
+ <td class="col-output">
383
+ ${outputSummary ? `<span class="truncate" title="${escapeHtml(eventData.output_summary)}">${escapeHtml(outputSummary)}${outputTruncated}</span>` : '<span class="text-muted">—</span>'}
384
+ </td>
385
+ <td class="col-status">
386
+ <span class="status-badge status-${eventData.status || 'pending'}">${escapeHtml(eventData.status || 'pending')}</span>
387
+ </td>
388
+ <td class="col-id">
389
+ <code class="event-id-code" title="${escapeHtml(eventData.event_id)}">${escapeHtml(eventData.event_id.substring(0, 8))}</code>
390
+ </td>
391
+ </tr>
392
+ `;
393
+ return html;
394
+ }
395
+
396
+ function escapeHtml(text) {
397
+ if (!text) return '';
398
+ const div = document.createElement('div');
399
+ div.textContent = text;
400
+ return div.innerHTML;
401
+ }
402
+
403
+ // Convert UTC timestamps to local timezone
404
+ function convertTimestampsToLocal() {
405
+ const timestampElements = document.querySelectorAll('[data-utc-time]');
406
+ console.log('convertTimestampsToLocal() called, found', timestampElements.length, 'timestamps to convert');
407
+ timestampElements.forEach(element => {
408
+ const utcTime = element.getAttribute('data-utc-time');
409
+ if (utcTime) {
410
+ try {
411
+ // Parse ISO 8601 UTC time - convert naive datetime to UTC format
412
+ // Input: "2026-01-06 18:01:19" → "2026-01-06T18:01:19Z"
413
+ const date = new Date(utcTime.replace(' ', 'T') + 'Z');
414
+ // Convert to local timezone using Intl API for best compatibility
415
+ const localTime = new Intl.DateTimeFormat('en-US', {
416
+ year: 'numeric',
417
+ month: '2-digit',
418
+ day: '2-digit',
419
+ hour: '2-digit',
420
+ minute: '2-digit',
421
+ second: '2-digit',
422
+ hour12: false,
423
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
424
+ }).format(date);
425
+ // Replace the displayed timestamp with local time
426
+ element.textContent = localTime;
427
+ // Add title attribute to show full ISO format on hover
428
+ element.setAttribute('title', `UTC: ${utcTime} | Local: ${localTime}`);
429
+ } catch (err) {
430
+ console.warn('Failed to convert timestamp:', utcTime, err);
431
+ }
432
+ }
433
+ });
434
+ }
435
+
436
+ // Handle tab switching
437
+ document.querySelectorAll('.tab-button').forEach(button => {
438
+ button.addEventListener('click', function() {
439
+ // Update active state
440
+ document.querySelectorAll('.tab-button').forEach(b => {
441
+ b.classList.remove('active');
442
+ });
443
+ this.classList.add('active');
444
+ });
445
+ });
446
+
447
+ // ============================================
448
+ // Jaeger-Style Trace Interactivity Functions
449
+ // (Must be global for HTMX-loaded partials)
450
+ // ============================================
451
+
452
+ // Toggle expand/collapse with animation
453
+ function toggleTrace(id, event) {
454
+ if (event) event.stopPropagation();
455
+
456
+ const parentRow = document.querySelector(`[data-id="${id}"]`);
457
+ const children = document.querySelectorAll(`[data-parent="${id}"]`);
458
+ const toggle = document.querySelector(`[data-id="${id}"] .expand-toggle`);
459
+
460
+ children.forEach(child => {
461
+ child.classList.toggle('collapsed');
462
+ });
463
+
464
+ if (toggle) {
465
+ toggle.classList.toggle('expanded');
466
+ }
467
+
468
+ // Toggle expanded class on parent row for visual styling
469
+ if (parentRow) {
470
+ parentRow.classList.toggle('expanded');
471
+ }
472
+
473
+ // Update breadcrumbs if drilling into a trace
474
+ updateBreadcrumbs(id);
475
+ }
476
+
477
+ // Highlight ancestor path on hover (Jaeger pattern)
478
+ function highlightAncestors(row) {
479
+ clearAncestorHighlight();
480
+
481
+ let parentId = row.dataset.parent;
482
+ while (parentId) {
483
+ const parent = document.querySelector(`[data-id="${parentId}"]`);
484
+ if (parent) {
485
+ parent.classList.add('ancestor-highlight');
486
+ parentId = parent.dataset.parent;
487
+ } else {
488
+ break;
489
+ }
490
+ }
491
+ }
492
+
493
+ // Clear all ancestor highlights
494
+ function clearAncestorHighlight() {
495
+ document.querySelectorAll('.ancestor-highlight').forEach(el => {
496
+ el.classList.remove('ancestor-highlight');
497
+ });
498
+ }
499
+
500
+ // Expand all traces
501
+ function expandAllTraces() {
502
+ document.querySelectorAll('.child-row').forEach(child => {
503
+ child.classList.remove('collapsed');
504
+ });
505
+ document.querySelectorAll('.expand-toggle').forEach(toggle => {
506
+ toggle.classList.add('expanded');
507
+ });
508
+ document.querySelectorAll('.parent-row.has-children').forEach(parent => {
509
+ parent.classList.add('expanded');
510
+ });
511
+ // Show breadcrumbs when all expanded
512
+ const breadcrumbs = document.getElementById('trace-breadcrumbs');
513
+ if (breadcrumbs) breadcrumbs.style.display = 'flex';
514
+ }
515
+
516
+ // Collapse all traces
517
+ function collapseAllTraces() {
518
+ document.querySelectorAll('.child-row').forEach(child => {
519
+ child.classList.add('collapsed');
520
+ });
521
+ document.querySelectorAll('.expand-toggle').forEach(toggle => {
522
+ toggle.classList.remove('expanded');
523
+ });
524
+ document.querySelectorAll('.parent-row.has-children').forEach(parent => {
525
+ parent.classList.remove('expanded');
526
+ });
527
+ // Hide breadcrumbs when all collapsed
528
+ const breadcrumbs = document.getElementById('trace-breadcrumbs');
529
+ if (breadcrumbs) breadcrumbs.style.display = 'none';
530
+ }
531
+
532
+ // Breadcrumb management
533
+ let breadcrumbStack = ['root'];
534
+
535
+ function updateBreadcrumbs(id) {
536
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
537
+ if (!breadcrumbsContainer) return;
538
+
539
+ const row = document.querySelector(`[data-id="${id}"]`);
540
+ if (!row) return;
541
+
542
+ // Only show breadcrumbs when we have nested navigation
543
+ const depth = parseInt(row.dataset.depth || '0');
544
+ if (depth > 0 || breadcrumbStack.length > 1) {
545
+ breadcrumbsContainer.style.display = 'flex';
546
+ }
547
+
548
+ // Get tool name or operation for breadcrumb label
549
+ const toolName = row.querySelector('.tool-name');
550
+ const label = toolName ? toolName.textContent : `Trace ${id.substring(0, 8)}`;
551
+
552
+ // Add to breadcrumb stack if not already present
553
+ if (!breadcrumbStack.includes(id)) {
554
+ breadcrumbStack.push(id);
555
+
556
+ const separator = document.createElement('span');
557
+ separator.className = 'separator';
558
+ separator.textContent = '>';
559
+
560
+ const crumb = document.createElement('span');
561
+ crumb.className = 'breadcrumb active';
562
+ crumb.dataset.id = id;
563
+ crumb.textContent = label;
564
+ crumb.onclick = () => navigateToBreadcrumb(id);
565
+
566
+ // Remove active class from previous breadcrumbs
567
+ breadcrumbsContainer.querySelectorAll('.breadcrumb').forEach(b => {
568
+ b.classList.remove('active');
569
+ });
570
+
571
+ breadcrumbsContainer.appendChild(separator);
572
+ breadcrumbsContainer.appendChild(crumb);
573
+ }
574
+ }
575
+
576
+ function navigateToBreadcrumb(id) {
577
+ // Find position in stack and remove everything after
578
+ const index = breadcrumbStack.indexOf(id);
579
+ if (index === -1) return;
580
+
581
+ // Remove breadcrumbs after this one
582
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
583
+ if (!breadcrumbsContainer) return;
584
+
585
+ const allCrumbs = breadcrumbsContainer.querySelectorAll('.breadcrumb, .separator');
586
+
587
+ let removing = false;
588
+ allCrumbs.forEach(el => {
589
+ if (removing) {
590
+ el.remove();
591
+ }
592
+ if (el.dataset && el.dataset.id === id) {
593
+ el.classList.add('active');
594
+ removing = true;
595
+ }
596
+ });
597
+
598
+ // Update stack
599
+ breadcrumbStack = breadcrumbStack.slice(0, index + 1);
600
+
601
+ // Hide breadcrumbs if back to root
602
+ if (breadcrumbStack.length <= 1) {
603
+ breadcrumbsContainer.style.display = 'none';
604
+ }
605
+ }
606
+
607
+ function resetBreadcrumbs() {
608
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
609
+ if (!breadcrumbsContainer) return;
610
+
611
+ breadcrumbsContainer.innerHTML = '<span class="breadcrumb" data-id="root" onclick="resetBreadcrumbs()">Session</span>';
612
+ breadcrumbsContainer.style.display = 'none';
613
+ breadcrumbStack = ['root'];
614
+
615
+ // Collapse all traces
616
+ collapseAllTraces();
617
+ }
618
+
619
+ // Initialize timestamps in activity feed after HTMX swap
620
+ function initializeActivityFeedTimestamps() {
621
+ document.querySelectorAll('.timestamp-text[data-utc-time]').forEach(el => {
622
+ const utcTime = el.dataset.utcTime;
623
+ if (utcTime) {
624
+ try {
625
+ const date = new Date(utcTime);
626
+ el.textContent = date.toLocaleTimeString();
627
+ el.title = date.toLocaleString();
628
+ } catch (e) {
629
+ // Keep original text if parsing fails
630
+ }
631
+ }
632
+ });
633
+ }
634
+
635
+ // Re-initialize after HTMX swaps in activity feed
636
+ document.body.addEventListener('htmx:afterSwap', function(evt) {
637
+ if (evt.detail.target && evt.detail.target.id === 'content-area') {
638
+ initializeActivityFeedTimestamps();
639
+ }
640
+ });
641
+
642
+ // ============================================
643
+ // Spawner Activity Filtering
644
+ // ============================================
645
+
646
+ function filterByAgentType(filterType) {
647
+ const turns = document.querySelectorAll('.conversation-turn');
648
+ turns.forEach(turn => {
649
+ const spawnerType = turn.dataset.spawnerType || 'direct';
650
+
651
+ let shouldShow = false;
652
+ if (filterType === 'all') {
653
+ shouldShow = true;
654
+ } else if (filterType === 'direct') {
655
+ shouldShow = spawnerType === 'direct';
656
+ } else if (filterType === 'spawner') {
657
+ shouldShow = spawnerType !== 'direct';
658
+ } else {
659
+ // Specific spawner type (gemini, codex, copilot)
660
+ shouldShow = spawnerType === filterType;
661
+ }
662
+
663
+ if (shouldShow) {
664
+ turn.classList.remove('hidden');
665
+ } else {
666
+ turn.classList.add('hidden');
667
+ }
668
+ });
669
+ }
670
+ </script>
671
+
672
+ <!-- Styles for real-time event highlighting and spawner badges -->
673
+ <style>
674
+ .new-event-highlight {
675
+ animation: highlightPulse 2s ease-out;
676
+ }
677
+
678
+ /* ============================================
679
+ Spawner Badge Styling
680
+ ============================================ */
681
+
682
+ .delegation-arrow {
683
+ color: var(--text-secondary);
684
+ margin: 0 0.5rem;
685
+ font-size: 0.9em;
686
+ font-weight: normal;
687
+ }
688
+
689
+ .spawner-badge {
690
+ display: inline-flex;
691
+ align-items: center;
692
+ gap: 0.25rem;
693
+ padding: 0.25rem 0.6rem;
694
+ border-radius: 4px;
695
+ font-size: 0.85rem;
696
+ font-weight: 500;
697
+ border: 1px solid;
698
+ background: white;
699
+ }
700
+
701
+ .spawner-badge.spawner-gemini {
702
+ background: #e8f5e9;
703
+ color: #2e7d32;
704
+ border-color: #4caf50;
705
+ }
706
+
707
+ .spawner-badge.spawner-codex {
708
+ background: #e3f2fd;
709
+ color: #1565c0;
710
+ border-color: #2196f3;
711
+ }
712
+
713
+ .spawner-badge.spawner-copilot {
714
+ background: #f3e5f5;
715
+ color: #6a1b9a;
716
+ border-color: #9c27b0;
717
+ }
718
+
719
+ .cost-badge {
720
+ font-size: 0.75rem;
721
+ opacity: 0.8;
722
+ margin-left: 0.25rem;
723
+ }
724
+
725
+ /* Activity feed spawner filter */
726
+ .spawner-filter {
727
+ padding: 0.75rem 1rem;
728
+ margin: 0.5rem 0.5rem 1rem 0.5rem;
729
+ border: 1px solid var(--border-subtle);
730
+ border-radius: 4px;
731
+ background: rgba(163, 230, 53, 0.02);
732
+ }
733
+
734
+ .spawner-filter label {
735
+ margin-right: 0.5rem;
736
+ font-weight: 500;
737
+ font-size: 0.85rem;
738
+ }
739
+
740
+ .spawner-filter select {
741
+ padding: 0.4rem 0.6rem;
742
+ border: 1px solid var(--border-subtle);
743
+ border-radius: 4px;
744
+ background: var(--bg-base);
745
+ color: var(--text-primary);
746
+ cursor: pointer;
747
+ font-size: 0.85rem;
748
+ }
749
+
750
+ .spawner-filter select:hover {
751
+ border-color: var(--accent, #c8ff00);
752
+ }
753
+
754
+ /* Hide turns based on filter */
755
+ .conversation-turn.hidden {
756
+ display: none;
757
+ }
758
+
759
+ @keyframes highlightPulse {
760
+ 0% {
761
+ background-color: rgba(16, 185, 129, 0.2);
762
+ border-left: 4px solid #10b981;
763
+ }
764
+ 100% {
765
+ background-color: transparent;
766
+ border-left: 4px solid transparent;
767
+ }
768
+ }
769
+
770
+ .auto-refresh-indicator {
771
+ transition: opacity 0.3s ease;
772
+ }
773
+
774
+ .refresh-dot {
775
+ display: inline-block;
776
+ width: 8px;
777
+ height: 8px;
778
+ border-radius: 50%;
779
+ margin-right: 8px;
780
+ background-color: #10b981;
781
+ animation: pulse-dot 2s infinite;
782
+ }
783
+
784
+ @keyframes pulse-dot {
785
+ 0%, 100% {
786
+ opacity: 1;
787
+ }
788
+ 50% {
789
+ opacity: 0.5;
790
+ }
791
+ }
792
+ </style>
793
+ </body>
794
+ </html>