x-ipe 1.0.24__py3-none-any.whl → 1.0.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.
Files changed (139) hide show
  1. x_ipe/app.py +25 -3
  2. x_ipe/handlers/terminal_handlers.py +6 -0
  3. x_ipe/handlers/voice_handlers.py +5 -0
  4. x_ipe/resources/copilot-instructions.md +19 -6
  5. x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
  6. x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
  7. x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
  8. x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
  9. x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
  10. x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
  11. x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
  12. x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
  13. x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
  14. x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
  15. x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
  16. x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
  17. x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
  18. x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
  19. x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
  20. x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
  21. x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
  22. x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
  23. x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
  24. x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
  25. x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
  26. x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
  27. x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
  28. x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
  29. x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
  30. x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
  31. x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
  32. x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
  33. x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
  34. x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
  35. x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
  36. x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
  37. x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
  38. x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
  39. x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
  40. x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
  41. x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
  42. x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
  43. x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
  44. x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
  45. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
  46. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
  47. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
  48. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
  49. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
  50. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
  51. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
  52. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
  53. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
  54. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
  55. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
  56. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
  57. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
  58. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
  59. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
  60. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
  61. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
  62. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
  63. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
  64. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
  65. x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
  66. x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
  67. x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
  68. x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
  69. x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
  70. x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
  71. x_ipe/routes/__init__.py +2 -0
  72. x_ipe/routes/ideas_routes.py +17 -0
  73. x_ipe/routes/kb_routes.py +80 -0
  74. x_ipe/routes/main_routes.py +18 -0
  75. x_ipe/routes/project_routes.py +7 -0
  76. x_ipe/routes/proxy_routes.py +2 -0
  77. x_ipe/routes/quality_evaluation_routes.py +193 -0
  78. x_ipe/routes/settings_routes.py +6 -0
  79. x_ipe/routes/tools_routes.py +6 -0
  80. x_ipe/routes/tracing_routes.py +232 -0
  81. x_ipe/routes/uiux_feedback_routes.py +30 -0
  82. x_ipe/services/__init__.py +5 -0
  83. x_ipe/services/config_service.py +6 -0
  84. x_ipe/services/file_service.py +20 -0
  85. x_ipe/services/homepage_service.py +160 -0
  86. x_ipe/services/ideas_service.py +19 -0
  87. x_ipe/services/kb_service.py +378 -0
  88. x_ipe/services/proxy_service.py +4 -0
  89. x_ipe/services/settings_service.py +13 -0
  90. x_ipe/services/skills_service.py +4 -0
  91. x_ipe/services/terminal_service.py +24 -0
  92. x_ipe/services/themes_service.py +4 -0
  93. x_ipe/services/tools_config_service.py +4 -0
  94. x_ipe/services/tracing_service.py +333 -0
  95. x_ipe/services/uiux_feedback_service.py +32 -0
  96. x_ipe/services/voice_input_service_v2.py +11 -0
  97. x_ipe/static/css/base.css +7 -0
  98. x_ipe/static/css/homepage-infinity.css +330 -0
  99. x_ipe/static/css/kb-core.css +301 -0
  100. x_ipe/static/css/quality-evaluation.css +345 -0
  101. x_ipe/static/css/sidebar.css +14 -4
  102. x_ipe/static/css/terminal.css +1 -0
  103. x_ipe/static/css/tracing-dashboard.css +796 -0
  104. x_ipe/static/css/workplace.css +20 -0
  105. x_ipe/static/img/homepage-infinity-loop.png +0 -0
  106. x_ipe/static/js/features/homepage-infinity.js +314 -0
  107. x_ipe/static/js/features/kb-core.js +371 -0
  108. x_ipe/static/js/features/quality-evaluation.js +387 -0
  109. x_ipe/static/js/features/sidebar.js +255 -12
  110. x_ipe/static/js/features/tracing-dashboard.js +855 -0
  111. x_ipe/static/js/features/tracing-graph.js +1031 -0
  112. x_ipe/static/js/features/tree-search.js +6 -2
  113. x_ipe/static/js/features/workplace.js +200 -6
  114. x_ipe/static/js/init.js +76 -0
  115. x_ipe/static/js/uiux-feedback.js +18 -2
  116. x_ipe/templates/base.html +19 -0
  117. x_ipe/templates/index.html +7 -1
  118. x_ipe/templates/knowledge-base.html +110 -0
  119. x_ipe/templates/workplace.html +4 -0
  120. x_ipe/tracing/__init__.py +37 -0
  121. x_ipe/tracing/buffer.py +135 -0
  122. x_ipe/tracing/context.py +125 -0
  123. x_ipe/tracing/decorator.py +288 -0
  124. x_ipe/tracing/middleware.py +197 -0
  125. x_ipe/tracing/parser.py +235 -0
  126. x_ipe/tracing/redactor.py +111 -0
  127. x_ipe/tracing/writer.py +122 -0
  128. {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
  129. {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/RECORD +132 -62
  130. x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
  131. x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
  132. x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
  133. x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
  134. x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
  135. x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
  136. x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
  137. {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
  138. {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
  139. {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,855 @@
1
+ /**
2
+ * FEATURE-023-B: Tracing Dashboard UI
3
+ * Matches mockup tracing-dashboard-v4.html
4
+ */
5
+
6
+ // =============================================================================
7
+ // TracingDashboard - Main Dashboard Component
8
+ // =============================================================================
9
+
10
+ class TracingDashboard {
11
+ constructor(container) {
12
+ this.container = container;
13
+ this.timerInterval = null;
14
+ this.pollInterval = null;
15
+ this.selectedTraceId = null;
16
+ this.stopAt = null;
17
+ this.config = {};
18
+ this.currentFilter = 'all';
19
+ this.searchQuery = '';
20
+ this.traces = [];
21
+ this.graphView = null; // FEATURE-023-C: DAG visualization
22
+ }
23
+
24
+ // -------------------------------------------------------------------------
25
+ // Lifecycle
26
+ // -------------------------------------------------------------------------
27
+
28
+ async init() {
29
+ this.render();
30
+ this.bindEvents();
31
+ await this.fetchStatus();
32
+ await this.refreshTraceList();
33
+ this.startPolling();
34
+ this.initGraphView(); // FEATURE-023-C: Initialize graph view
35
+ }
36
+
37
+ // FEATURE-023-C: Initialize graph view
38
+ initGraphView() {
39
+ const graphContainer = this.container.querySelector('.trace-graph-container');
40
+ if (graphContainer && typeof TracingGraphView !== 'undefined') {
41
+ this.graphView = new TracingGraphView(graphContainer);
42
+ this.graphView.showEmpty();
43
+ }
44
+ }
45
+
46
+ destroy() {
47
+ this.stopCountdown();
48
+ this.stopPolling();
49
+ if (this.graphView) {
50
+ this.graphView.destroy();
51
+ this.graphView = null;
52
+ }
53
+ }
54
+
55
+ // -------------------------------------------------------------------------
56
+ // API Methods
57
+ // -------------------------------------------------------------------------
58
+
59
+ async fetchStatus() {
60
+ try {
61
+ const response = await fetch('/api/tracing/status');
62
+ if (!response.ok) throw new Error('Failed to fetch status');
63
+
64
+ this.config = await response.json();
65
+ this.updateUIFromStatus();
66
+ } catch (error) {
67
+ console.error('Error fetching tracing status:', error);
68
+ this.showToast('Failed to load tracing status', 'error');
69
+ }
70
+ }
71
+
72
+ async startTracing(durationMinutes) {
73
+ try {
74
+ const response = await fetch('/api/tracing/start', {
75
+ method: 'POST',
76
+ headers: { 'Content-Type': 'application/json' },
77
+ body: JSON.stringify({ duration_minutes: durationMinutes })
78
+ });
79
+
80
+ if (!response.ok) throw new Error('Failed to start tracing');
81
+
82
+ const data = await response.json();
83
+ this.stopAt = new Date(data.stop_at);
84
+ this.startCountdown();
85
+ this.updateDurationButtons(durationMinutes);
86
+ this.showToast(`Tracing started for ${durationMinutes} minutes`, 'success');
87
+ } catch (error) {
88
+ console.error('Error starting tracing:', error);
89
+ this.showToast('Failed to start tracing', 'error');
90
+ }
91
+ }
92
+
93
+ async stopTracing() {
94
+ try {
95
+ const response = await fetch('/api/tracing/stop', {
96
+ method: 'POST'
97
+ });
98
+
99
+ if (!response.ok) throw new Error('Failed to stop tracing');
100
+
101
+ this.stopAt = null;
102
+ this.stopCountdown();
103
+ this.updateDurationButtons(null);
104
+ this.updateTimerDisplay();
105
+ this.showToast('Tracing stopped', 'success');
106
+ } catch (error) {
107
+ console.error('Error stopping tracing:', error);
108
+ this.showToast('Failed to stop tracing', 'error');
109
+ }
110
+ }
111
+
112
+ async refreshTraceList() {
113
+ try {
114
+ const response = await fetch('/api/tracing/logs');
115
+ if (!response.ok) throw new Error('Failed to fetch trace logs');
116
+
117
+ this.traces = await response.json();
118
+ this.renderTraceList();
119
+ } catch (error) {
120
+ console.error('Error fetching trace logs:', error);
121
+ }
122
+ }
123
+
124
+ // -------------------------------------------------------------------------
125
+ // Rendering
126
+ // -------------------------------------------------------------------------
127
+
128
+ render() {
129
+ this.container.innerHTML = `
130
+ <div class="tracing-dashboard">
131
+ <!-- Header -->
132
+ <div class="tracing-header">
133
+ <div class="tracing-header-left">
134
+ <h2>
135
+ <i class="bi bi-graph-up"></i>
136
+ Application Tracing
137
+ </h2>
138
+ </div>
139
+ <div class="tracing-header-controls">
140
+ <div class="tracing-control">
141
+ <!-- Countdown Timer (left side like mockup) -->
142
+ <div class="countdown-container inactive">
143
+ <svg class="countdown-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
144
+ <circle cx="12" cy="12" r="10"/>
145
+ <path d="M12 6v6l4 2"/>
146
+ </svg>
147
+ <span class="countdown-text">--:--</span>
148
+ <span class="countdown-label">Inactive</span>
149
+ </div>
150
+
151
+ <button class="stop-btn hidden">Stop</button>
152
+
153
+ <span class="tracing-label">Start Tracing</span>
154
+ <div class="duration-toggle">
155
+ <button class="duration-option" data-minutes="3">3 min</button>
156
+ <button class="duration-option" data-minutes="15">15 min</button>
157
+ <button class="duration-option" data-minutes="30">30 min</button>
158
+ </div>
159
+ </div>
160
+
161
+ <button class="icon-btn btn-config" title="Configuration">
162
+ <i class="bi bi-gear"></i>
163
+ </button>
164
+ <button class="icon-btn btn-ignored" title="Ignored APIs">
165
+ <i class="bi bi-slash-circle"></i>
166
+ </button>
167
+ <button class="icon-btn btn-clear" title="Clear All Traces">
168
+ <i class="bi bi-trash"></i>
169
+ </button>
170
+ </div>
171
+ </div>
172
+
173
+ <!-- Main Content -->
174
+ <div class="tracing-content">
175
+ <!-- Sidebar -->
176
+ <div class="trace-list-sidebar">
177
+ <div class="trace-list-header">
178
+ <div class="trace-list-title">Trace Logs</div>
179
+ <div class="trace-search-box">
180
+ <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
181
+ <circle cx="11" cy="11" r="8"/>
182
+ <path d="m21 21-4.35-4.35"/>
183
+ </svg>
184
+ <input type="text" placeholder="Search traces..." id="trace-search">
185
+ </div>
186
+ </div>
187
+ <div class="filter-row">
188
+ <button class="filter-chip active" data-filter="all">All</button>
189
+ <button class="filter-chip" data-filter="success">
190
+ <span class="dot success"></span>Success
191
+ </button>
192
+ <button class="filter-chip" data-filter="error">
193
+ <span class="dot error"></span>Errors
194
+ </button>
195
+ </div>
196
+ <div class="trace-list-items">
197
+ <div class="trace-list-empty">
198
+ <div class="trace-list-empty-icon">📭</div>
199
+ <div>No traces captured</div>
200
+ <div>Start tracing to begin</div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <!-- Detail Panel / Graph Container (FEATURE-023-C) -->
206
+ <div class="trace-detail-panel">
207
+ <div class="trace-graph-container">
208
+ <!-- Graph will be initialized here by TracingGraphView -->
209
+ </div>
210
+ <div class="trace-graph-zoom-controls">
211
+ <button class="trace-graph-zoom-btn" data-action="zoom-in" title="Zoom In">
212
+ <i class="bi bi-zoom-in"></i>
213
+ </button>
214
+ <button class="trace-graph-zoom-btn" data-action="zoom-out" title="Zoom Out">
215
+ <i class="bi bi-zoom-out"></i>
216
+ </button>
217
+ <button class="trace-graph-zoom-btn" data-action="zoom-reset" title="Fit to View">
218
+ <i class="bi bi-arrows-fullscreen"></i>
219
+ </button>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ `;
225
+ }
226
+
227
+ renderTraceList() {
228
+ const listContainer = this.container.querySelector('.trace-list-items');
229
+
230
+ // Filter traces
231
+ let filteredTraces = this.traces;
232
+ if (this.currentFilter === 'success') {
233
+ filteredTraces = this.traces.filter(t => !t.has_error);
234
+ } else if (this.currentFilter === 'error') {
235
+ filteredTraces = this.traces.filter(t => t.has_error);
236
+ }
237
+
238
+ // Search filter
239
+ if (this.searchQuery) {
240
+ const query = this.searchQuery.toLowerCase();
241
+ filteredTraces = filteredTraces.filter(t =>
242
+ (t.api || t.path || '').toLowerCase().includes(query) ||
243
+ (t.trace_id || '').toLowerCase().includes(query)
244
+ );
245
+ }
246
+
247
+ if (!filteredTraces || filteredTraces.length === 0) {
248
+ listContainer.innerHTML = `
249
+ <div class="trace-list-empty">
250
+ <div class="trace-list-empty-icon">📭</div>
251
+ <div>${this.searchQuery || this.currentFilter !== 'all' ? 'No matching traces' : 'No traces captured'}</div>
252
+ <div>${!this.searchQuery && this.currentFilter === 'all' ? 'Start tracing to begin' : 'Try adjusting your filters'}</div>
253
+ </div>
254
+ `;
255
+ return;
256
+ }
257
+
258
+ // Sort by timestamp descending (newest first)
259
+ filteredTraces.sort((a, b) => {
260
+ const timeA = new Date(a.timestamp || a.created || 0);
261
+ const timeB = new Date(b.timestamp || b.created || 0);
262
+ return timeB - timeA;
263
+ });
264
+
265
+ listContainer.innerHTML = filteredTraces.map(trace => {
266
+ const traceId = trace.trace_id || trace.id || 'unknown';
267
+ const api = trace.api || trace.path || trace.name || '/unknown';
268
+ const timestamp = trace.timestamp || trace.created || '';
269
+ const hasError = trace.has_error || trace.status === 'error';
270
+ const statusClass = hasError ? 'error' : 'success';
271
+ const isSelected = traceId === this.selectedTraceId;
272
+ const method = trace.method || 'GET';
273
+ const duration = trace.duration_ms || trace.duration || 0;
274
+ const nestedCount = trace.nested_count || trace.spans || 0;
275
+
276
+ // Format timestamp
277
+ let formattedTime = '';
278
+ if (timestamp) {
279
+ const date = new Date(timestamp);
280
+ formattedTime = date.toLocaleTimeString();
281
+ }
282
+
283
+ // Truncate trace ID
284
+ const shortId = traceId.substring(0, 8);
285
+
286
+ // Extract just the path from API (e.g., "GET /api/projects" -> "/api/projects")
287
+ const apiPath = api.includes(' ') ? api.split(' ').slice(1).join(' ') : api;
288
+
289
+ return `
290
+ <div class="trace-item ${statusClass} ${isSelected ? 'selected' : ''}"
291
+ data-trace-id="${traceId}" data-api-path="${this.escapeHtml(apiPath)}">
292
+ <div class="trace-id-row">
293
+ <span class="trace-id">${shortId}...</span>
294
+ <button class="trace-block-btn" title="Add to ignored APIs" data-api="${this.escapeHtml(apiPath)}">
295
+ <i class="bi bi-slash-circle"></i>
296
+ </button>
297
+ <span class="trace-status-dot ${statusClass}"></span>
298
+ </div>
299
+ <div class="trace-entry-api">
300
+ <span class="method-badge ${method.toLowerCase()}">${method}</span>
301
+ <span class="trace-path">${this.escapeHtml(api)}</span>
302
+ ${nestedCount > 0 ? `<span class="trace-nested">+${nestedCount}</span>` : ''}
303
+ </div>
304
+ <div class="trace-meta">
305
+ <span>
306
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
307
+ <circle cx="12" cy="12" r="10"/>
308
+ <polyline points="12 6 12 12 16 14"/>
309
+ </svg>
310
+ ${formattedTime}
311
+ </span>
312
+ ${duration > 0 ? `<span>${duration}ms</span>` : ''}
313
+ </div>
314
+ </div>
315
+ `;
316
+ }).join('');
317
+
318
+ // Rebind click events
319
+ listContainer.querySelectorAll('.trace-item').forEach(item => {
320
+ item.addEventListener('click', (e) => {
321
+ // Don't select trace if clicking the block button
322
+ if (e.target.closest('.trace-block-btn')) return;
323
+ this.selectTrace(item.dataset.traceId);
324
+ });
325
+ });
326
+
327
+ // Bind block button events
328
+ listContainer.querySelectorAll('.trace-block-btn').forEach(btn => {
329
+ btn.addEventListener('click', (e) => {
330
+ e.stopPropagation();
331
+ const apiPath = btn.dataset.api;
332
+ this.addToIgnoredApis(apiPath);
333
+ });
334
+ });
335
+ }
336
+
337
+ // -------------------------------------------------------------------------
338
+ // Timer Logic
339
+ // -------------------------------------------------------------------------
340
+
341
+ startCountdown() {
342
+ this.stopCountdown();
343
+ this.updateTimerDisplay();
344
+
345
+ this.timerInterval = setInterval(() => {
346
+ this.updateTimerDisplay();
347
+
348
+ // Auto-stop when timer reaches 0
349
+ if (this.stopAt && new Date() >= this.stopAt) {
350
+ this.stopCountdown();
351
+ this.stopAt = null;
352
+ this.updateDurationButtons(null);
353
+ this.updateTimerDisplay();
354
+ this.showToast('Tracing completed', 'success');
355
+ }
356
+ }, 1000);
357
+
358
+ // Show stop button
359
+ const stopBtn = this.container.querySelector('.stop-btn');
360
+ if (stopBtn) stopBtn.classList.remove('hidden');
361
+ }
362
+
363
+ stopCountdown() {
364
+ if (this.timerInterval) {
365
+ clearInterval(this.timerInterval);
366
+ this.timerInterval = null;
367
+ }
368
+
369
+ // Hide stop button
370
+ const stopBtn = this.container.querySelector('.stop-btn');
371
+ if (stopBtn) stopBtn.classList.add('hidden');
372
+ }
373
+
374
+ updateTimerDisplay() {
375
+ const container = this.container.querySelector('.countdown-container');
376
+ const timerText = this.container.querySelector('.countdown-text');
377
+ const timerLabel = this.container.querySelector('.countdown-label');
378
+ if (!container || !timerText) return;
379
+
380
+ if (!this.stopAt) {
381
+ timerText.textContent = '--:--';
382
+ if (timerLabel) timerLabel.textContent = 'Inactive';
383
+ container.classList.remove('warning');
384
+ container.classList.add('inactive');
385
+ return;
386
+ }
387
+
388
+ const now = new Date();
389
+ const remaining = Math.max(0, Math.floor((this.stopAt - now) / 1000));
390
+
391
+ const minutes = Math.floor(remaining / 60);
392
+ const seconds = remaining % 60;
393
+ timerText.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
394
+ if (timerLabel) timerLabel.textContent = 'remaining';
395
+
396
+ container.classList.remove('inactive');
397
+
398
+ // Warning state when < 1 minute
399
+ if (remaining < 60 && remaining > 0) {
400
+ container.classList.add('warning');
401
+ } else {
402
+ container.classList.remove('warning');
403
+ }
404
+ }
405
+
406
+ updateUIFromStatus() {
407
+ const stopAtStr = this.config.stop_at;
408
+ if (stopAtStr && this.config.active) {
409
+ this.stopAt = new Date(stopAtStr);
410
+ if (this.stopAt > new Date()) {
411
+ this.startCountdown();
412
+ // Estimate which duration was selected
413
+ const remaining = (this.stopAt - new Date()) / 1000 / 60;
414
+ if (remaining <= 3) this.updateDurationButtons(3);
415
+ else if (remaining <= 15) this.updateDurationButtons(15);
416
+ else this.updateDurationButtons(30);
417
+ } else {
418
+ this.stopAt = null;
419
+ }
420
+ }
421
+ this.updateTimerDisplay();
422
+ }
423
+
424
+ updateDurationButtons(activeMinutes) {
425
+ this.container.querySelectorAll('.duration-option').forEach(btn => {
426
+ const minutes = parseInt(btn.dataset.minutes);
427
+ btn.classList.toggle('active', minutes === activeMinutes);
428
+ });
429
+ }
430
+
431
+ // -------------------------------------------------------------------------
432
+ // Polling
433
+ // -------------------------------------------------------------------------
434
+
435
+ startPolling() {
436
+ this.pollInterval = setInterval(() => {
437
+ if (this.stopAt) {
438
+ this.refreshTraceList();
439
+ }
440
+ }, 5000);
441
+ }
442
+
443
+ stopPolling() {
444
+ if (this.pollInterval) {
445
+ clearInterval(this.pollInterval);
446
+ this.pollInterval = null;
447
+ }
448
+ }
449
+
450
+ // -------------------------------------------------------------------------
451
+ // Event Handling
452
+ // -------------------------------------------------------------------------
453
+
454
+ bindEvents() {
455
+ // Duration buttons
456
+ this.container.querySelectorAll('.duration-option').forEach(btn => {
457
+ btn.addEventListener('click', () => {
458
+ const minutes = parseInt(btn.dataset.minutes);
459
+ this.startTracing(minutes);
460
+ });
461
+ });
462
+
463
+ // Stop button
464
+ const stopBtn = this.container.querySelector('.stop-btn');
465
+ if (stopBtn) {
466
+ stopBtn.addEventListener('click', () => this.stopTracing());
467
+ }
468
+
469
+ // Config button
470
+ const configBtn = this.container.querySelector('.btn-config');
471
+ if (configBtn) {
472
+ configBtn.addEventListener('click', () => this.openConfigModal());
473
+ }
474
+
475
+ // Ignored APIs button
476
+ const ignoredBtn = this.container.querySelector('.btn-ignored');
477
+ if (ignoredBtn) {
478
+ ignoredBtn.addEventListener('click', () => this.openIgnoredModal());
479
+ }
480
+
481
+ // Clear all traces button
482
+ const clearBtn = this.container.querySelector('.btn-clear');
483
+ if (clearBtn) {
484
+ clearBtn.addEventListener('click', () => this.clearAllTraces());
485
+ }
486
+
487
+ // Filter chips
488
+ this.container.querySelectorAll('.filter-chip').forEach(chip => {
489
+ chip.addEventListener('click', () => {
490
+ this.container.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
491
+ chip.classList.add('active');
492
+ this.currentFilter = chip.dataset.filter;
493
+ this.renderTraceList();
494
+ });
495
+ });
496
+
497
+ // Search
498
+ const searchInput = this.container.querySelector('#trace-search');
499
+ if (searchInput) {
500
+ searchInput.addEventListener('input', (e) => {
501
+ this.searchQuery = e.target.value;
502
+ this.renderTraceList();
503
+ });
504
+ }
505
+
506
+ // FEATURE-023-C: Zoom controls
507
+ this.container.querySelectorAll('.trace-graph-zoom-btn').forEach(btn => {
508
+ btn.addEventListener('click', () => {
509
+ if (!this.graphView) return;
510
+ const action = btn.dataset.action;
511
+ if (action === 'zoom-in') this.graphView.zoomIn();
512
+ else if (action === 'zoom-out') this.graphView.zoomOut();
513
+ else if (action === 'zoom-reset') this.graphView.resetZoom();
514
+ });
515
+ });
516
+ }
517
+
518
+ selectTrace(traceId) {
519
+ this.selectedTraceId = traceId;
520
+
521
+ // Update selection UI
522
+ this.container.querySelectorAll('.trace-item').forEach(item => {
523
+ item.classList.toggle('selected', item.dataset.traceId === traceId);
524
+ });
525
+
526
+ // FEATURE-023-C: Load trace into graph view
527
+ if (this.graphView) {
528
+ this.graphView.loadTrace(traceId);
529
+ } else {
530
+ // Fallback if graph view not initialized
531
+ const graphContainer = this.container.querySelector('.trace-graph-container');
532
+ if (graphContainer) {
533
+ graphContainer.innerHTML = `
534
+ <div class="trace-graph-loading">
535
+ <div class="spinner-border spinner-border-sm" role="status"></div>
536
+ <span>Loading trace: ${traceId.substring(0, 12)}...</span>
537
+ </div>
538
+ `;
539
+ }
540
+ }
541
+ }
542
+
543
+ // -------------------------------------------------------------------------
544
+ // Modals
545
+ // -------------------------------------------------------------------------
546
+
547
+ openConfigModal() {
548
+ const modal = new TracingConfigModal(async (config) => {
549
+ await this.updateConfig(config);
550
+ });
551
+ modal.open(this.config);
552
+ }
553
+
554
+ async updateConfig(config) {
555
+ try {
556
+ const response = await fetch('/api/tracing/config', {
557
+ method: 'POST',
558
+ headers: { 'Content-Type': 'application/json' },
559
+ body: JSON.stringify(config)
560
+ });
561
+
562
+ if (response.ok) {
563
+ this.config = { ...this.config, ...config };
564
+ this.showToast('Configuration saved', 'success');
565
+ } else {
566
+ this.showToast('Failed to save configuration', 'error');
567
+ }
568
+ } catch (error) {
569
+ console.error('Error updating config:', error);
570
+ this.showToast('Failed to save configuration', 'error');
571
+ }
572
+ }
573
+
574
+ openIgnoredModal() {
575
+ const modal = new TracingIgnoredModal(async (patterns) => {
576
+ await this.updateIgnoredApis(patterns);
577
+ });
578
+ modal.open(this.config.ignored_apis || []);
579
+ }
580
+
581
+ async updateIgnoredApis(patterns) {
582
+ try {
583
+ const response = await fetch('/api/tracing/ignored', {
584
+ method: 'POST',
585
+ headers: { 'Content-Type': 'application/json' },
586
+ body: JSON.stringify({ patterns })
587
+ });
588
+
589
+ if (response.ok) {
590
+ this.config.ignored_apis = patterns;
591
+ this.showToast('Ignored APIs updated', 'success');
592
+ } else {
593
+ this.showToast('Failed to update ignored APIs', 'error');
594
+ }
595
+ } catch (error) {
596
+ console.error('Error updating ignored APIs:', error);
597
+ this.showToast('Failed to update ignored APIs', 'error');
598
+ }
599
+ }
600
+
601
+ async addToIgnoredApis(apiPath) {
602
+ if (!apiPath) return;
603
+
604
+ // Get current ignored APIs
605
+ const currentIgnored = this.config.ignored_apis || [];
606
+
607
+ // Check if already in list
608
+ if (currentIgnored.includes(apiPath)) {
609
+ this.showToast('API already in ignored list', 'info');
610
+ return;
611
+ }
612
+
613
+ // Add to list and save
614
+ const newIgnored = [...currentIgnored, apiPath];
615
+ await this.updateIgnoredApis(newIgnored);
616
+ }
617
+
618
+ async clearAllTraces() {
619
+ if (!confirm('Are you sure you want to delete all trace logs? This cannot be undone.')) {
620
+ return;
621
+ }
622
+
623
+ try {
624
+ const response = await fetch('/api/tracing/logs', {
625
+ method: 'DELETE'
626
+ });
627
+
628
+ if (response.ok) {
629
+ this.traces = [];
630
+ this.renderTraceList();
631
+ this.graph.showEmpty();
632
+ this.showToast('All traces cleared', 'success');
633
+ } else {
634
+ this.showToast('Failed to clear traces', 'error');
635
+ }
636
+ } catch (error) {
637
+ console.error('Error clearing traces:', error);
638
+ this.showToast('Failed to clear traces', 'error');
639
+ }
640
+ }
641
+
642
+ // -------------------------------------------------------------------------
643
+ // Utilities
644
+ // -------------------------------------------------------------------------
645
+
646
+ showToast(message, type = 'success') {
647
+ const toast = document.createElement('div');
648
+ toast.className = `tracing-toast ${type}`;
649
+ toast.textContent = message;
650
+ document.body.appendChild(toast);
651
+
652
+ setTimeout(() => {
653
+ toast.remove();
654
+ }, 3000);
655
+ }
656
+
657
+ escapeHtml(text) {
658
+ const div = document.createElement('div');
659
+ div.textContent = text;
660
+ return div.innerHTML;
661
+ }
662
+ }
663
+
664
+
665
+ // =============================================================================
666
+ // TracingConfigModal - Configuration Modal
667
+ // =============================================================================
668
+
669
+ class TracingConfigModal {
670
+ constructor(onSave) {
671
+ this.onSave = onSave;
672
+ this.overlay = null;
673
+ }
674
+
675
+ open(currentConfig) {
676
+ this.overlay = document.createElement('div');
677
+ this.overlay.className = 'tracing-modal-overlay';
678
+ this.overlay.innerHTML = `
679
+ <div class="tracing-modal">
680
+ <div class="tracing-modal-header">
681
+ <h3>Tracing Configuration</h3>
682
+ <button class="tracing-modal-close">&times;</button>
683
+ </div>
684
+ <div class="tracing-modal-body">
685
+ <div class="tracing-form-group">
686
+ <label>Retention Hours</label>
687
+ <input type="number" id="retention-hours" min="1" max="168"
688
+ value="${currentConfig.retention_hours || 24}">
689
+ </div>
690
+ <div class="tracing-form-group">
691
+ <label>Log Path</label>
692
+ <input type="text" id="log-path"
693
+ value="${currentConfig.log_path || 'instance/traces/'}">
694
+ </div>
695
+ </div>
696
+ <div class="tracing-modal-footer">
697
+ <button class="btn-secondary btn-cancel">Cancel</button>
698
+ <button class="btn-primary btn-save">Save</button>
699
+ </div>
700
+ </div>
701
+ `;
702
+
703
+ document.body.appendChild(this.overlay);
704
+ this.bindEvents();
705
+ }
706
+
707
+ bindEvents() {
708
+ this.overlay.querySelector('.tracing-modal-close').addEventListener('click', () => this.close());
709
+ this.overlay.querySelector('.btn-cancel').addEventListener('click', () => this.close());
710
+ this.overlay.querySelector('.btn-save').addEventListener('click', () => this.handleSave());
711
+ this.overlay.addEventListener('click', (e) => {
712
+ if (e.target === this.overlay) this.close();
713
+ });
714
+ }
715
+
716
+ handleSave() {
717
+ const retentionHours = parseInt(this.overlay.querySelector('#retention-hours').value);
718
+ const logPath = this.overlay.querySelector('#log-path').value;
719
+
720
+ this.onSave({ retention_hours: retentionHours, log_path: logPath });
721
+ this.close();
722
+ }
723
+
724
+ close() {
725
+ if (this.overlay) {
726
+ this.overlay.remove();
727
+ this.overlay = null;
728
+ }
729
+ }
730
+ }
731
+
732
+
733
+ // =============================================================================
734
+ // TracingIgnoredModal - Ignored APIs Modal
735
+ // =============================================================================
736
+
737
+ class TracingIgnoredModal {
738
+ constructor(onSave) {
739
+ this.onSave = onSave;
740
+ this.overlay = null;
741
+ this.patterns = [];
742
+ }
743
+
744
+ open(patterns) {
745
+ this.patterns = [...patterns];
746
+
747
+ this.overlay = document.createElement('div');
748
+ this.overlay.className = 'tracing-modal-overlay';
749
+ this.render();
750
+ document.body.appendChild(this.overlay);
751
+ this.bindEvents();
752
+ }
753
+
754
+ render() {
755
+ this.overlay.innerHTML = `
756
+ <div class="tracing-modal">
757
+ <div class="tracing-modal-header">
758
+ <h3>Ignored APIs</h3>
759
+ <button class="tracing-modal-close">&times;</button>
760
+ </div>
761
+ <div class="tracing-modal-body">
762
+ <p style="font-size: 13px; color: #64748b; margin-bottom: 16px;">
763
+ API patterns that will be excluded from tracing.
764
+ </p>
765
+ <div class="ignored-apis-list">
766
+ ${this.patterns.map((pattern, i) => `
767
+ <div class="ignored-api-item">
768
+ <span>${this.escapeHtml(pattern)}</span>
769
+ <button data-index="${i}" title="Remove">×</button>
770
+ </div>
771
+ `).join('')}
772
+ </div>
773
+ <div class="add-ignored-api">
774
+ <input type="text" id="new-pattern" placeholder="/api/health/*">
775
+ <button class="btn-secondary btn-add">Add</button>
776
+ </div>
777
+ </div>
778
+ <div class="tracing-modal-footer">
779
+ <button class="btn-secondary btn-cancel">Cancel</button>
780
+ <button class="btn-primary btn-save">Save</button>
781
+ </div>
782
+ </div>
783
+ `;
784
+ }
785
+
786
+ bindEvents() {
787
+ this.overlay.querySelector('.tracing-modal-close').addEventListener('click', () => this.close());
788
+ this.overlay.querySelector('.btn-cancel').addEventListener('click', () => this.close());
789
+ this.overlay.querySelector('.btn-save').addEventListener('click', () => this.handleSave());
790
+ this.overlay.querySelector('.btn-add').addEventListener('click', () => this.addPattern());
791
+
792
+ // Remove buttons
793
+ this.overlay.querySelectorAll('.ignored-api-item button').forEach(btn => {
794
+ btn.addEventListener('click', () => {
795
+ const index = parseInt(btn.dataset.index);
796
+ this.removePattern(index);
797
+ });
798
+ });
799
+
800
+ // Enter key to add
801
+ this.overlay.querySelector('#new-pattern').addEventListener('keypress', (e) => {
802
+ if (e.key === 'Enter') this.addPattern();
803
+ });
804
+
805
+ // Click outside to close
806
+ this.overlay.addEventListener('click', (e) => {
807
+ if (e.target === this.overlay) this.close();
808
+ });
809
+ }
810
+
811
+ addPattern() {
812
+ const input = this.overlay.querySelector('#new-pattern');
813
+ const pattern = input.value.trim();
814
+ if (pattern && !this.patterns.includes(pattern)) {
815
+ this.patterns.push(pattern);
816
+ this.render();
817
+ this.bindEvents();
818
+ }
819
+ }
820
+
821
+ removePattern(index) {
822
+ this.patterns.splice(index, 1);
823
+ this.render();
824
+ this.bindEvents();
825
+ }
826
+
827
+ handleSave() {
828
+ this.onSave(this.patterns);
829
+ this.close();
830
+ }
831
+
832
+ close() {
833
+ if (this.overlay) {
834
+ this.overlay.remove();
835
+ this.overlay = null;
836
+ }
837
+ }
838
+
839
+ escapeHtml(text) {
840
+ const div = document.createElement('div');
841
+ div.textContent = text;
842
+ return div.innerHTML;
843
+ }
844
+ }
845
+
846
+
847
+ // =============================================================================
848
+ // Export for module usage
849
+ // =============================================================================
850
+
851
+ if (typeof window !== 'undefined') {
852
+ window.TracingDashboard = TracingDashboard;
853
+ window.TracingConfigModal = TracingConfigModal;
854
+ window.TracingIgnoredModal = TracingIgnoredModal;
855
+ }