x-ipe 1.0.23__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 (146) hide show
  1. x_ipe/app.py +32 -1
  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 +289 -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 +10 -2
  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 +50 -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 +535 -2
  87. x_ipe/services/kb_service.py +378 -0
  88. x_ipe/services/proxy_service.py +37 -7
  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 +148 -1
  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 +23 -0
  103. x_ipe/static/css/tracing-dashboard.css +796 -0
  104. x_ipe/static/css/uiux-feedback.css +7 -1
  105. x_ipe/static/css/workplace.css +636 -0
  106. x_ipe/static/img/homepage-infinity-loop.png +0 -0
  107. x_ipe/static/js/features/confirm-dialog.js +169 -0
  108. x_ipe/static/js/features/folder-view.js +742 -0
  109. x_ipe/static/js/features/homepage-infinity.js +314 -0
  110. x_ipe/static/js/features/kb-core.js +371 -0
  111. x_ipe/static/js/features/quality-evaluation.js +387 -0
  112. x_ipe/static/js/features/sidebar.js +255 -12
  113. x_ipe/static/js/features/tracing-dashboard.js +855 -0
  114. x_ipe/static/js/features/tracing-graph.js +1031 -0
  115. x_ipe/static/js/features/tree-drag.js +227 -0
  116. x_ipe/static/js/features/tree-search.js +228 -0
  117. x_ipe/static/js/features/workplace.js +661 -33
  118. x_ipe/static/js/init.js +76 -0
  119. x_ipe/static/js/terminal-v2.js +45 -14
  120. x_ipe/static/js/terminal.js +50 -49
  121. x_ipe/static/js/uiux-feedback.js +75 -16
  122. x_ipe/templates/base.html +24 -0
  123. x_ipe/templates/index.html +10 -1
  124. x_ipe/templates/knowledge-base.html +110 -0
  125. x_ipe/templates/workplace.html +4 -0
  126. x_ipe/tracing/__init__.py +37 -0
  127. x_ipe/tracing/buffer.py +135 -0
  128. x_ipe/tracing/context.py +125 -0
  129. x_ipe/tracing/decorator.py +288 -0
  130. x_ipe/tracing/middleware.py +197 -0
  131. x_ipe/tracing/parser.py +235 -0
  132. x_ipe/tracing/redactor.py +111 -0
  133. x_ipe/tracing/writer.py +122 -0
  134. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
  135. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
  136. x_ipe/app.py.bak +0 -1333
  137. x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
  138. x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
  139. x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
  140. x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
  141. x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
  142. x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
  143. x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
  144. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
  145. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
  146. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
@@ -13,6 +13,8 @@ from collections import deque
13
13
  from datetime import datetime
14
14
  from typing import Dict, Optional, Any, Callable
15
15
 
16
+ from x_ipe.tracing import x_ipe_tracing
17
+
16
18
 
17
19
  # Constants for session management
18
20
  BUFFER_MAX_CHARS = 10240 # 10KB limit for output buffer
@@ -30,15 +32,18 @@ class OutputBuffer:
30
32
  def __init__(self, max_chars: int = BUFFER_MAX_CHARS):
31
33
  self._buffer: deque = deque(maxlen=max_chars)
32
34
 
35
+ @x_ipe_tracing()
33
36
  def append(self, data: str) -> None:
34
37
  """Append data character by character to maintain limit."""
35
38
  for char in data:
36
39
  self._buffer.append(char)
37
40
 
41
+ @x_ipe_tracing()
38
42
  def get_contents(self) -> str:
39
43
  """Get all buffered content as string."""
40
44
  return ''.join(self._buffer)
41
45
 
46
+ @x_ipe_tracing()
42
47
  def clear(self) -> None:
43
48
  """Clear the buffer."""
44
49
  self._buffer.clear()
@@ -68,6 +73,7 @@ class PersistentSession:
68
73
  self.created_at = datetime.now()
69
74
  self._lock = threading.Lock()
70
75
 
76
+ @x_ipe_tracing()
71
77
  def start_pty(self, rows: int = 24, cols: int = 80) -> None:
72
78
  """Start the underlying PTY process."""
73
79
  def buffered_emit(data: str) -> None:
@@ -80,6 +86,7 @@ class PersistentSession:
80
86
  self.pty_session = PTYSession(self.session_id, buffered_emit)
81
87
  self.pty_session.start(rows, cols)
82
88
 
89
+ @x_ipe_tracing()
83
90
  def attach(self, socket_sid: str, emit_callback: Callable[[str], None]) -> None:
84
91
  """Attach a WebSocket connection to this session."""
85
92
  with self._lock:
@@ -88,6 +95,7 @@ class PersistentSession:
88
95
  self.state = 'connected'
89
96
  self.disconnect_time = None
90
97
 
98
+ @x_ipe_tracing()
91
99
  def detach(self) -> None:
92
100
  """Detach WebSocket, keeping PTY alive for reconnection."""
93
101
  with self._lock:
@@ -96,20 +104,24 @@ class PersistentSession:
96
104
  self.state = 'disconnected'
97
105
  self.disconnect_time = datetime.now()
98
106
 
107
+ @x_ipe_tracing()
99
108
  def get_buffer(self) -> str:
100
109
  """Get buffered output for replay."""
101
110
  return self.output_buffer.get_contents()
102
111
 
112
+ @x_ipe_tracing()
103
113
  def write(self, data: str) -> None:
104
114
  """Write input to PTY."""
105
115
  if self.pty_session:
106
116
  self.pty_session.write(data)
107
117
 
118
+ @x_ipe_tracing()
108
119
  def resize(self, rows: int, cols: int) -> None:
109
120
  """Resize the PTY."""
110
121
  if self.pty_session:
111
122
  self.pty_session._set_size(rows, cols)
112
123
 
124
+ @x_ipe_tracing()
113
125
  def is_expired(self, timeout_seconds: int = SESSION_TIMEOUT) -> bool:
114
126
  """Check if session has expired (1hr after disconnect)."""
115
127
  if self.state == 'connected':
@@ -119,6 +131,7 @@ class PersistentSession:
119
131
  elapsed = datetime.now() - self.disconnect_time
120
132
  return elapsed.total_seconds() > timeout_seconds
121
133
 
134
+ @x_ipe_tracing()
122
135
  def close(self) -> None:
123
136
  """Close session and cleanup resources."""
124
137
  if self.pty_session:
@@ -139,6 +152,7 @@ class SessionManager:
139
152
  self._cleanup_timer: Optional[threading.Timer] = None
140
153
  self._running = False
141
154
 
155
+ @x_ipe_tracing()
142
156
  def create_session(self, emit_callback: Callable[[str], None],
143
157
  rows: int = 24, cols: int = 80) -> str:
144
158
  """Create new persistent session, returns session_id."""
@@ -152,16 +166,19 @@ class SessionManager:
152
166
 
153
167
  return session_id
154
168
 
169
+ @x_ipe_tracing()
155
170
  def get_session(self, session_id: str) -> Optional[PersistentSession]:
156
171
  """Get session by ID."""
157
172
  with self._lock:
158
173
  return self.sessions.get(session_id)
159
174
 
175
+ @x_ipe_tracing()
160
176
  def has_session(self, session_id: str) -> bool:
161
177
  """Check if session exists."""
162
178
  with self._lock:
163
179
  return session_id in self.sessions
164
180
 
181
+ @x_ipe_tracing()
165
182
  def remove_session(self, session_id: str) -> None:
166
183
  """Remove and close a session."""
167
184
  with self._lock:
@@ -169,6 +186,7 @@ class SessionManager:
169
186
  if session:
170
187
  session.close()
171
188
 
189
+ @x_ipe_tracing()
172
190
  def cleanup_expired(self) -> int:
173
191
  """Remove expired sessions. Returns count removed."""
174
192
  expired_ids = []
@@ -182,11 +200,13 @@ class SessionManager:
182
200
 
183
201
  return len(expired_ids)
184
202
 
203
+ @x_ipe_tracing()
185
204
  def start_cleanup_task(self) -> None:
186
205
  """Start background cleanup task (every 5 minutes)."""
187
206
  self._running = True
188
207
  self._schedule_cleanup()
189
208
 
209
+ @x_ipe_tracing()
190
210
  def stop_cleanup_task(self) -> None:
191
211
  """Stop the cleanup task."""
192
212
  self._running = False
@@ -230,6 +250,7 @@ class PTYSession:
230
250
  self.rows = 24
231
251
  self.cols = 80
232
252
 
253
+ @x_ipe_tracing()
233
254
  def start(self, rows: int = 24, cols: int = 80) -> None:
234
255
  """Spawn PTY with shell and start output reader."""
235
256
  import pty
@@ -310,6 +331,7 @@ class PTYSession:
310
331
  except Exception:
311
332
  pass
312
333
 
334
+ @x_ipe_tracing()
313
335
  def write(self, data: str) -> None:
314
336
  """Write input to PTY."""
315
337
  if self.fd is not None:
@@ -328,6 +350,7 @@ class PTYSession:
328
350
  winsize = struct.pack('HHHH', self.rows, self.cols, 0, 0)
329
351
  fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize)
330
352
 
353
+ @x_ipe_tracing()
331
354
  def close(self) -> None:
332
355
  """Terminate PTY session and cleanup."""
333
356
  import signal
@@ -351,6 +374,7 @@ class PTYSession:
351
374
  pass
352
375
  self.pid = None
353
376
 
377
+ @x_ipe_tracing()
354
378
  def isalive(self) -> bool:
355
379
  """Check if the PTY process is still running."""
356
380
  return self._running and self.fd is not None
@@ -10,6 +10,8 @@ import re
10
10
  from pathlib import Path
11
11
  from typing import Dict, List, Any, Optional
12
12
 
13
+ from x_ipe.tracing import x_ipe_tracing
14
+
13
15
 
14
16
  THEMES_DIR = 'x-ipe-docs/themes'
15
17
  THEME_PREFIX = 'theme-'
@@ -45,6 +47,7 @@ class ThemesService:
45
47
  self.project_root = Path(project_root).resolve()
46
48
  self.themes_dir = self.project_root / THEMES_DIR
47
49
 
50
+ @x_ipe_tracing()
48
51
  def list_themes(self) -> List[Dict[str, Any]]:
49
52
  """
50
53
  List all valid themes with metadata.
@@ -92,6 +95,7 @@ class ThemesService:
92
95
 
93
96
  return themes
94
97
 
98
+ @x_ipe_tracing()
95
99
  def get_theme(self, name: str) -> Optional[Dict[str, Any]]:
96
100
  """
97
101
  Get detailed information about a specific theme.
@@ -11,6 +11,8 @@ import copy
11
11
  from pathlib import Path
12
12
  from typing import Dict, Any
13
13
 
14
+ from x_ipe.tracing import x_ipe_tracing
15
+
14
16
 
15
17
  CONFIG_DIR = 'x-ipe-docs/config'
16
18
  CONFIG_FILE = 'tools.json'
@@ -56,6 +58,7 @@ class ToolsConfigService:
56
58
  self.config_path = self.config_dir / CONFIG_FILE
57
59
  self.legacy_path = self.project_root / LEGACY_PATH
58
60
 
61
+ @x_ipe_tracing()
59
62
  def load(self) -> Dict[str, Any]:
60
63
  """
61
64
  Load config, migrating from legacy if needed.
@@ -76,6 +79,7 @@ class ToolsConfigService:
76
79
 
77
80
  return self._create_default()
78
81
 
82
+ @x_ipe_tracing()
79
83
  def save(self, config: Dict[str, Any]) -> bool:
80
84
  """
81
85
  Save config to file.
@@ -0,0 +1,333 @@
1
+ """
2
+ FEATURE-023: Application Action Tracing - Core
3
+
4
+ TracingService for managing tracing configuration and lifecycle.
5
+
6
+ Provides high-level API for starting/stopping tracing, reading
7
+ configuration from tools.json, and cleaning up old log files.
8
+ """
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import Dict, List, Any, Optional
11
+ from pathlib import Path
12
+
13
+ from x_ipe.services.tools_config_service import ToolsConfigService
14
+ from x_ipe.tracing.writer import TraceLogWriter
15
+ from x_ipe.tracing.parser import TraceLogParser
16
+ from x_ipe.tracing import x_ipe_tracing
17
+
18
+
19
+ class TracingService:
20
+ """
21
+ Service for managing tracing configuration and lifecycle.
22
+
23
+ Integrates with tools.json for configuration persistence and
24
+ provides methods for controlling tracing state.
25
+
26
+ Usage:
27
+ service = TracingService("/path/to/project")
28
+
29
+ # Check status
30
+ config = service.get_config()
31
+ is_active = service.is_active()
32
+
33
+ # Control tracing
34
+ service.start(duration_minutes=15)
35
+ service.stop()
36
+
37
+ # List/cleanup logs
38
+ logs = service.list_logs()
39
+ deleted = service.cleanup_on_startup()
40
+ """
41
+
42
+ def __init__(self, project_root: str):
43
+ """
44
+ Initialize TracingService.
45
+
46
+ Args:
47
+ project_root: Path to the project root directory
48
+ """
49
+ self.project_root = Path(project_root)
50
+ self.tools_config = ToolsConfigService(str(project_root))
51
+
52
+ @x_ipe_tracing()
53
+ def get_config(self) -> Dict[str, Any]:
54
+ """
55
+ Get current tracing configuration.
56
+
57
+ Returns:
58
+ Dictionary with tracing settings:
59
+ - enabled: bool
60
+ - stop_at: str or None (ISO timestamp)
61
+ - log_path: str
62
+ - retention_hours: int
63
+ - ignored_apis: list
64
+ """
65
+ config = self.tools_config.load()
66
+ return {
67
+ "enabled": config.get("tracing_enabled", False),
68
+ "stop_at": config.get("tracing_stop_at"),
69
+ "log_path": config.get("tracing_log_path", "instance/traces/"),
70
+ "retention_hours": config.get("tracing_retention_hours", 24),
71
+ "ignored_apis": config.get("tracing_ignored_apis", [])
72
+ }
73
+
74
+ @x_ipe_tracing()
75
+ def is_active(self) -> bool:
76
+ """
77
+ Check if tracing is currently active.
78
+
79
+ Tracing is active if:
80
+ - tracing_enabled is True, OR
81
+ - tracing_stop_at is set and in the future
82
+
83
+ Returns:
84
+ True if tracing should be performed
85
+ """
86
+ config = self.get_config()
87
+
88
+ if config["enabled"]:
89
+ return True
90
+
91
+ stop_at = config["stop_at"]
92
+ if stop_at:
93
+ try:
94
+ # Parse ISO timestamp - keep it timezone-aware
95
+ stop_time = datetime.fromisoformat(
96
+ stop_at.replace("Z", "+00:00")
97
+ )
98
+ return datetime.now(timezone.utc) < stop_time
99
+ except (ValueError, AttributeError):
100
+ return False
101
+
102
+ return False
103
+
104
+ @x_ipe_tracing()
105
+ def start(self, duration_minutes: int) -> Dict[str, Any]:
106
+ """
107
+ Start tracing for specified duration.
108
+
109
+ Args:
110
+ duration_minutes: Duration in minutes (must be 3, 15, or 30)
111
+
112
+ Returns:
113
+ Dictionary with success status and stop_at timestamp
114
+
115
+ Raises:
116
+ ValueError: If duration is not 3, 15, or 30
117
+ """
118
+ if duration_minutes not in [3, 15, 30]:
119
+ raise ValueError("Duration must be 3, 15, or 30 minutes")
120
+
121
+ stop_at = datetime.now(timezone.utc) + timedelta(minutes=duration_minutes)
122
+ # Convert +00:00 to Z for consistent format
123
+ stop_at_str = stop_at.isoformat().replace("+00:00", "Z")
124
+
125
+ config = self.tools_config.load()
126
+ config["tracing_stop_at"] = stop_at_str
127
+ self.tools_config.save(config)
128
+
129
+ return {"success": True, "stop_at": stop_at_str}
130
+
131
+ @x_ipe_tracing()
132
+ def stop(self) -> Dict[str, Any]:
133
+ """
134
+ Stop tracing immediately.
135
+
136
+ Clears tracing_stop_at and sets tracing_enabled to False.
137
+
138
+ Returns:
139
+ Dictionary with success status
140
+ """
141
+ config = self.tools_config.load()
142
+ config["tracing_stop_at"] = None
143
+ config["tracing_enabled"] = False
144
+ self.tools_config.save(config)
145
+
146
+ return {"success": True}
147
+
148
+ @x_ipe_tracing()
149
+ def update_ignored_apis(self, patterns: List[str]) -> None:
150
+ """
151
+ Update the list of ignored API patterns.
152
+
153
+ Args:
154
+ patterns: List of API path patterns to ignore
155
+ """
156
+ config = self.tools_config.load()
157
+ config["tracing_ignored_apis"] = patterns
158
+ self.tools_config.save(config)
159
+
160
+ @x_ipe_tracing()
161
+ def list_logs(self) -> List[Dict[str, Any]]:
162
+ """
163
+ List all trace log files.
164
+
165
+ Returns:
166
+ List of log file metadata dictionaries:
167
+ - trace_id: str
168
+ - api: str (e.g., "GET /api/project/structure")
169
+ - filename: str
170
+ - size: int (bytes)
171
+ - timestamp: str (ISO format)
172
+ """
173
+ config = self.get_config()
174
+ log_path = self.project_root / config["log_path"]
175
+
176
+ if not log_path.exists():
177
+ return []
178
+
179
+ logs = []
180
+ for filepath in sorted(log_path.glob("*.log"), reverse=True):
181
+ try:
182
+ # Parse filename: {timestamp}-{api}-{trace_id}.log
183
+ # Example: 20260202-072505-get-api-project-structure-a649c048-3d73.log
184
+ stem = filepath.stem
185
+
186
+ # Extract trace_id (last 2 UUID segments: xxxxxxxx-xxxx)
187
+ # Split and find the UUID pattern at the end
188
+ parts = stem.split("-")
189
+ if len(parts) >= 4:
190
+ # Last 2 parts form the trace_id (e.g., "a649c048-3d73")
191
+ trace_id = f"{parts[-2]}-{parts[-1]}"
192
+ # First 2 parts are timestamp (YYYYMMDD-HHMMSS)
193
+ # Middle parts are the API name
194
+ api_parts = parts[2:-2] # Skip timestamp and trace_id
195
+ api_name = "-".join(api_parts) if api_parts else "unknown"
196
+ # Convert api_name back to path format (e.g., "get-api-project-structure" -> "GET /api/project/structure")
197
+ api = self._filename_to_api(api_name)
198
+ else:
199
+ trace_id = stem
200
+ api = "/unknown"
201
+
202
+ stat = filepath.stat()
203
+ logs.append({
204
+ "trace_id": trace_id,
205
+ "api": api,
206
+ "filename": filepath.name,
207
+ "size": stat.st_size,
208
+ "timestamp": datetime.fromtimestamp(stat.st_mtime).isoformat()
209
+ })
210
+ except OSError:
211
+ continue
212
+
213
+ return logs
214
+
215
+ def _filename_to_api(self, api_name: str) -> str:
216
+ """
217
+ Convert sanitized API filename component back to API format.
218
+
219
+ Args:
220
+ api_name: Sanitized name (e.g., "get-api-project-structure")
221
+
222
+ Returns:
223
+ API string (e.g., "GET /api/project/structure")
224
+ """
225
+ if not api_name or api_name == "unknown":
226
+ return "/unknown"
227
+
228
+ # Split by first hyphen to get method
229
+ parts = api_name.split("-", 1)
230
+ if len(parts) < 2:
231
+ return f"/{api_name}"
232
+
233
+ method = parts[0].upper()
234
+ path_part = parts[1]
235
+
236
+ # Convert hyphens back to slashes for path
237
+ path = "/" + path_part.replace("-", "/")
238
+
239
+ return f"{method} {path}"
240
+
241
+ @x_ipe_tracing()
242
+ def cleanup_on_startup(self) -> int:
243
+ """
244
+ Clean up old log files on backend startup.
245
+
246
+ Uses retention_hours from configuration to determine
247
+ which files to delete.
248
+
249
+ Returns:
250
+ Number of files deleted
251
+ """
252
+ config = self.get_config()
253
+ log_path = self.project_root / config["log_path"]
254
+
255
+ writer = TraceLogWriter(str(log_path))
256
+ deleted = writer.cleanup(config["retention_hours"])
257
+
258
+ if deleted > 0:
259
+ print(f"[TRACING] Cleaned up {deleted} old trace log(s)")
260
+
261
+ return deleted
262
+
263
+ @x_ipe_tracing()
264
+ def delete_all_logs(self) -> int:
265
+ """
266
+ Delete all trace log files.
267
+
268
+ Returns:
269
+ Number of files deleted
270
+ """
271
+ config = self.get_config()
272
+ log_path = self.project_root / config["log_path"]
273
+
274
+ if not log_path.exists():
275
+ return 0
276
+
277
+ deleted = 0
278
+ for filepath in log_path.glob("*.log"):
279
+ try:
280
+ filepath.unlink()
281
+ deleted += 1
282
+ except OSError:
283
+ continue
284
+
285
+ return deleted
286
+
287
+ @x_ipe_tracing()
288
+ def get_trace(self, trace_id: str) -> Optional[Dict[str, Any]]:
289
+ """
290
+ Get parsed trace data for visualization.
291
+
292
+ Searches for a log file matching the trace_id (exact or partial)
293
+ and parses it into visualization-ready structure.
294
+
295
+ Args:
296
+ trace_id: Full or partial trace ID to search for
297
+
298
+ Returns:
299
+ Parsed trace data or None if not found:
300
+ {
301
+ "trace_id": str,
302
+ "api": str,
303
+ "timestamp": str,
304
+ "total_time_ms": int,
305
+ "status": str,
306
+ "nodes": [...],
307
+ "edges": [...]
308
+ }
309
+ """
310
+ config = self.get_config()
311
+ log_path = self.project_root / config["log_path"]
312
+
313
+ if not log_path.exists():
314
+ return None
315
+
316
+ # Search for matching file
317
+ matching_file = None
318
+ for filepath in log_path.glob("*.log"):
319
+ if trace_id in filepath.stem:
320
+ matching_file = filepath
321
+ break
322
+
323
+ if not matching_file:
324
+ return None
325
+
326
+ # Parse the file
327
+ parser = TraceLogParser()
328
+ result = parser.parse(matching_file)
329
+
330
+ # Add filename for reference
331
+ result["filename"] = matching_file.name
332
+
333
+ return result
@@ -4,8 +4,12 @@ FEATURE-022-D: UI/UX Feedback Service
4
4
  Handles saving feedback entries to the file system.
5
5
  """
6
6
  import base64
7
+ import shutil
8
+ import re
7
9
  from pathlib import Path
8
- from datetime import datetime
10
+ from datetime import datetime, timedelta
11
+
12
+ from x_ipe.tracing import x_ipe_tracing
9
13
 
10
14
 
11
15
  class UiuxFeedbackService:
@@ -15,6 +19,149 @@ class UiuxFeedbackService:
15
19
  self.project_root = Path(project_root)
16
20
  self.feedback_dir = self.project_root / 'x-ipe-docs' / 'uiux-feedback'
17
21
 
22
+ @x_ipe_tracing()
23
+ def list_feedback(self, days: int = 2) -> list:
24
+ """
25
+ List feedback entries from the last N days.
26
+
27
+ Args:
28
+ days: Number of days to look back (default 2)
29
+
30
+ Returns:
31
+ List of feedback entries sorted by date descending
32
+ """
33
+ entries = []
34
+ cutoff = datetime.now() - timedelta(days=days)
35
+
36
+ if not self.feedback_dir.exists():
37
+ return entries
38
+
39
+ for folder in self.feedback_dir.iterdir():
40
+ if not folder.is_dir():
41
+ continue
42
+
43
+ # Check folder modification time
44
+ mtime = datetime.fromtimestamp(folder.stat().st_mtime)
45
+ if mtime < cutoff:
46
+ continue
47
+
48
+ # Parse feedback.md to extract details
49
+ feedback_md = folder / 'feedback.md'
50
+ if not feedback_md.exists():
51
+ continue
52
+
53
+ entry = self._parse_feedback_md(folder.name, feedback_md)
54
+ if entry:
55
+ entry['mtime'] = mtime
56
+ entries.append(entry)
57
+
58
+ # Sort by date descending (newest first)
59
+ entries.sort(key=lambda x: x['mtime'], reverse=True)
60
+
61
+ # Remove mtime from output (internal use only)
62
+ for entry in entries:
63
+ del entry['mtime']
64
+
65
+ return entries
66
+
67
+ def _parse_feedback_md(self, folder_name: str, feedback_md: Path) -> dict:
68
+ """
69
+ Parse feedback.md file to extract entry details.
70
+
71
+ Args:
72
+ folder_name: Name of the feedback folder
73
+ feedback_md: Path to feedback.md file
74
+
75
+ Returns:
76
+ Dict with id, name, url, description, date
77
+ """
78
+ try:
79
+ content = feedback_md.read_text(encoding='utf-8')
80
+
81
+ # Extract URL
82
+ url_match = re.search(r'\*\*URL:\*\*\s*(.+)', content)
83
+ url = url_match.group(1).strip() if url_match else ''
84
+
85
+ # Extract date
86
+ date_match = re.search(r'\*\*Date:\*\*\s*(.+)', content)
87
+ date = date_match.group(1).strip() if date_match else ''
88
+
89
+ # Extract description (between ## Feedback and ## Screenshot or end)
90
+ desc_match = re.search(r'## Feedback\s*\n\n(.+?)(?=\n## Screenshot|\Z)', content, re.DOTALL)
91
+ description = desc_match.group(1).strip() if desc_match else ''
92
+ if description == '_No description provided_':
93
+ description = ''
94
+
95
+ return {
96
+ 'id': folder_name,
97
+ 'name': folder_name,
98
+ 'url': url,
99
+ 'description': description,
100
+ 'date': date
101
+ }
102
+ except Exception:
103
+ return None
104
+
105
+ @x_ipe_tracing()
106
+ def delete_feedback(self, feedback_id: str) -> dict:
107
+ """
108
+ Delete a feedback folder by ID.
109
+
110
+ Args:
111
+ feedback_id: The feedback folder name/ID
112
+
113
+ Returns:
114
+ dict with success (or error on failure)
115
+ """
116
+ try:
117
+ folder_path = self.feedback_dir / feedback_id
118
+
119
+ if not folder_path.exists():
120
+ return {'success': False, 'error': 'Feedback not found'}
121
+
122
+ if not folder_path.is_dir():
123
+ return {'success': False, 'error': 'Invalid feedback ID'}
124
+
125
+ # Delete the folder
126
+ shutil.rmtree(folder_path)
127
+
128
+ return {'success': True}
129
+ except Exception as e:
130
+ return {'success': False, 'error': str(e)}
131
+
132
+ @x_ipe_tracing()
133
+ def cleanup_old_feedback(self, days: int = 7) -> int:
134
+ """
135
+ Delete feedback folders older than N days.
136
+
137
+ Args:
138
+ days: Retention period in days (default 7)
139
+
140
+ Returns:
141
+ Number of folders deleted
142
+ """
143
+ deleted = 0
144
+ cutoff = datetime.now() - timedelta(days=days)
145
+
146
+ if not self.feedback_dir.exists():
147
+ return deleted
148
+
149
+ for folder in list(self.feedback_dir.iterdir()):
150
+ if not folder.is_dir():
151
+ continue
152
+
153
+ # Check folder modification time
154
+ mtime = datetime.fromtimestamp(folder.stat().st_mtime)
155
+ if mtime < cutoff:
156
+ try:
157
+ shutil.rmtree(folder)
158
+ deleted += 1
159
+ except Exception:
160
+ pass
161
+
162
+ return deleted
163
+
164
+ @x_ipe_tracing()
18
165
  def save_feedback(self, data: dict) -> dict:
19
166
  """
20
167
  Save feedback entry to file system.