@ai-qa/workflow 2.0.16 β†’ 2.0.18

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.
@@ -11,8 +11,8 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <div class="project-detail" style="margin-bottom:2rem;">
15
- <p class="project-path" style="font-family:var(--font-mono);color:var(--text-disabled);font-size:0.88rem;"><%= project.path %></p>
14
+ <div style="margin-bottom:2rem;">
15
+ <p style="font-family:var(--font-mono);color:var(--text-disabled);font-size:0.88rem;"><%= project.path %></p>
16
16
  </div>
17
17
 
18
18
  <div class="row" style="margin-bottom:2rem;">
@@ -20,7 +20,6 @@
20
20
  <div class="card card-lavender">
21
21
  <h3>πŸ“ User Stories</h3>
22
22
  <div class="big-number"><%= list.stories.length %></div>
23
- <a href="/stories?project=<%= project.id %>" class="btn btn-sm" style="margin-top:1rem;">View Stories</a>
24
23
  </div>
25
24
  </div>
26
25
  <div class="col">
@@ -37,81 +36,109 @@
37
36
  </div>
38
37
  </div>
39
38
 
39
+ <!-- ─── Liste des Stories avec Pipeline Status ─── -->
40
40
  <div class="card">
41
- <h3>⚑ Pipeline Execution</h3>
42
- <form id="pipeline-form" data-project="<%= project.id %>" class="form">
43
- <div class="form-group">
44
- <label>Select User Story to Run</label>
45
- <select name="storyName" class="form-input">
46
- <option value="">β€” Run status check / full suite β€”</option>
47
- <% list.stories.forEach(s => { %>
48
- <option value="<%= s %>"><%= s %></option>
49
- <% }) %>
50
- </select>
51
- </div>
52
- <button type="submit" class="btn btn-primary">Run Pipeline</button>
53
- </form>
54
- <div id="pipeline-output" class="output-box" style="display:none;"></div>
41
+ <h3>πŸ“‹ Stories Pipeline</h3>
42
+ <% if (storiesWithStatus && storiesWithStatus.length > 0) { %>
43
+ <table class="info-table">
44
+ <thead>
45
+ <tr>
46
+ <th>User Story</th>
47
+ <th>Plan</th>
48
+ <th>Test</th>
49
+ <th>Run</th>
50
+ <th>Actions</th>
51
+ </tr>
52
+ </thead>
53
+ <tbody>
54
+ <% storiesWithStatus.forEach(s => { %>
55
+ <tr>
56
+ <td><a href="/stories/<%= s.name %>?project=<%= project.id %>" style="font-weight:600;"><%= s.baseName %></a></td>
57
+ <td><% if (s.hasPlan) { %><span class="badge badge-plan">βœ…</span><% } else { %><span class="badge badge-draft">β€”</span><% } %></td>
58
+ <td><% if (s.hasSpec) { %><span class="badge badge-spec">βœ…</span><% } else { %><span class="badge badge-draft">β€”</span><% } %></td>
59
+ <td><% if (s.hasRun) { %><span class="badge badge-pass">βœ…</span><% } else { %><span class="badge badge-draft">β€”</span><% } %></td>
60
+ <td>
61
+ <div style="display:flex;gap:0.4rem;flex-wrap:wrap;">
62
+ <button class="btn btn-sm" onclick="runPipelineAction('<%= project.id %>', '<%= s.name %>', 'plan')" <%= s.hasPlan ? 'disabled style="opacity:0.5;"' : '' %>>πŸ“‹ Plan</button>
63
+ <button class="btn btn-sm" onclick="runPipelineAction('<%= project.id %>', '<%= s.name %>', 'generate')" <%= !s.hasPlan || s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>πŸ§ͺ Generate</button>
64
+ <button class="btn btn-sm btn-primary" onclick="runPipelineAction('<%= project.id %>', '<%= s.name %>', 'execute')" <%= !s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>β–Ά Execute</button>
65
+ <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="btn btn-sm">View β†’</a>
66
+ </div>
67
+ </td>
68
+ </tr>
69
+ <% }) %>
70
+ </tbody>
71
+ </table>
72
+ <% } else { %>
73
+ <p style="color:var(--text-muted);">Aucune user story trouvΓ©e. Ajoutez des fichiers <code>.md</code> dans <code>user-story/</code>.</p>
74
+ <% } %>
75
+ <div id="pipeline-stream-output" class="output-box" style="display:none;margin-top:1rem;"></div>
76
+ <div id="pipeline-stream-pulse" class="terminal-pulse" style="display:none;margin-top:0.75rem;">
77
+ <span class="pulse-dot"></span><span class="pulse-dot"></span><span class="pulse-dot"></span>
78
+ <span class="pulse-text">Running...</span>
79
+ </div>
55
80
  </div>
56
81
 
82
+ <!-- ─── Quick Actions ─── -->
57
83
  <div class="card">
58
- <h3>βš™οΈ Quick Actions</h3>
59
- <p style="color:var(--text-muted);font-size:0.9rem;margin-bottom:1rem;">Manage, execute, or build standard reports for this project instantly.</p>
60
- <div class="action-buttons">
61
- <button class="btn btn-sm" onclick="runAction('<%= project.id %>', 'status')">Status</button>
62
- <button class="btn btn-sm" onclick="runAction('<%= project.id %>', 'execute')">Execute All Tests</button>
63
- <button class="btn btn-sm" onclick="runAction('<%= project.id %>', 'report')">Generate Report</button>
84
+ <h3>⚑ Orchestrator Actions</h3>
85
+ <div class="action-buttons" style="margin-bottom:1rem;">
86
+ <button class="btn btn-sm" onclick="runQuickAction('<%= project.id %>', 'status')">πŸ“Š Status</button>
87
+ <button class="btn btn-sm btn-primary" onclick="runQuickAction('<%= project.id %>', 'report')">πŸ“ Generate Report</button>
88
+ <% if (hasAllure) { %>
89
+ <a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank">πŸ“ˆ Allure Report</a>
90
+ <% } %>
91
+ <a href="/export/report/latest?project=<%= project.id %>" class="btn btn-sm" target="_blank">πŸ“₯ Download Latest Report</a>
64
92
  </div>
65
93
  <div id="quick-output" class="output-box" style="display:none;"></div>
66
94
  </div>
67
95
 
68
96
  <script>
69
- document.getElementById('pipeline-form').addEventListener('submit', async (e) => {
70
- e.preventDefault();
71
- const form = e.target;
72
- const projectId = form.dataset.project;
73
- const formData = new FormData(form);
74
- const output = document.getElementById('pipeline-output');
75
-
97
+ async function runPipelineAction(projectId, storyName, action) {
98
+ const output = document.getElementById('pipeline-stream-output');
99
+ const pulse = document.getElementById('pipeline-stream-pulse');
76
100
  output.style.display = 'block';
77
- output.textContent = 'Running pipeline...';
101
+ output.textContent = `πŸš€ Running ${action} for ${storyName}...\n`;
78
102
  output.className = 'output-box output-running';
103
+ pulse.style.display = 'flex';
79
104
 
80
105
  try {
81
- const res = await fetch(`/projects/${projectId}/run-pipeline`, {
82
- method: 'POST',
83
- headers: { 'Content-Type': 'application/json' },
84
- body: JSON.stringify({ storyName: formData.get('storyName') })
85
- });
86
- if (!res.ok) throw new Error(`HTTP Error ${res.status}`);
87
- const data = await res.json();
88
- output.textContent = data.output || data.error || 'Done';
89
- output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
106
+ const res = await fetch(`/stories/${encodeURIComponent(storyName)}/${action}?project=${projectId}`, { method: 'POST' });
107
+ if (!res.body) { output.textContent += '\nError: No response stream'; pulse.style.display = 'none'; return; }
108
+ const reader = res.body.getReader();
109
+ const decoder = new TextDecoder('utf-8');
110
+ while (true) {
111
+ const { done, value } = await reader.read();
112
+ if (done) break;
113
+ output.textContent += decoder.decode(value, { stream: true });
114
+ output.scrollTop = output.scrollHeight;
115
+ }
116
+ pulse.style.display = 'none';
117
+ output.className = output.textContent.toLowerCase().includes('finished: success') ? 'output-box output-success' : 'output-box output-error';
118
+ if (output.textContent.toLowerCase().includes('finished: success')) {
119
+ setTimeout(() => location.reload(), 2000);
120
+ }
90
121
  } catch (err) {
91
- output.textContent = 'Network or server error: ' + err.message;
122
+ output.textContent += `\nError: ${err.message}`;
92
123
  output.className = 'output-box output-error';
124
+ pulse.style.display = 'none';
93
125
  }
94
- });
126
+ }
95
127
 
96
- async function runAction(projectId, action) {
128
+ async function runQuickAction(projectId, action) {
97
129
  const output = document.getElementById('quick-output');
98
130
  output.style.display = 'block';
99
131
  output.textContent = `Running ${action}...`;
100
132
  output.className = 'output-box output-running';
101
-
102
133
  try {
103
- const res = await fetch(`/projects/${projectId}/${action}`, {
104
- method: 'POST',
105
- headers: { 'Content-Type': 'application/json' },
106
- body: '{}'
107
- });
134
+ const res = await fetch(`/projects/${projectId}/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
108
135
  if (!res.ok) throw new Error(`HTTP Error ${res.status}`);
109
136
  const data = await res.json();
110
137
  output.textContent = data.output || data.error || 'Done';
111
138
  output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
112
139
  } catch (err) {
113
- output.textContent = 'Network or server error: ' + err.message;
140
+ output.textContent = 'Error: ' + err.message;
114
141
  output.className = 'output-box output-error';
115
142
  }
116
143
  }
117
- </script>
144
+ </script>
@@ -56,14 +56,19 @@
56
56
  <div class="card">
57
57
  <h3>πŸ“‚ Report Actions</h3>
58
58
  <div class="action-buttons-vertical">
59
+ <button class="btn btn-sm btn-primary" onclick="rerunRun('<%= project.id %>', '<%= run.id %>')" style="margin-bottom:0.5rem;">πŸ”„ Rerun This Test</button>
59
60
  <% if (run.report) { %>
60
- <a href="/export/report/<%= run.id %>?project=<%= project.id %>" class="btn btn-primary" target="_blank">Export HTML Report</a>
61
- <a href="/export/pdf/<%= run.id %>?project=<%= project.id %>" class="btn">Download Raw (.txt)</a>
61
+ <a href="/export/report/<%= run.id %>?project=<%= project.id %>" class="btn btn-sm" target="_blank">πŸ“₯ Export HTML Report</a>
62
+ <a href="/export/pdf/<%= run.id %>?project=<%= project.id %>" class="btn btn-sm">πŸ“₯ Download Raw (.txt)</a>
62
63
  <% } else { %>
63
64
  <p style="color:var(--text-disabled);font-size:0.85rem;margin-bottom:0.5rem;">No summary report generated yet.</p>
64
65
  <% } %>
66
+ <button class="btn btn-sm" onclick="generateReport('<%= project.id %>', '<%= run.id %>')" style="margin-top:0.25rem;">πŸ“ Generate Report</button>
65
67
  <% if (run.hasAllure) { %>
66
- <a href="/allure-report?project=<%= project.id %>" class="btn" target="_blank" style="background-color:var(--brand-ochre);color:var(--text);border-color:var(--border-strong);margin-top:0.5rem;">Open Allure Report β†—</a>
68
+ <a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank" style="background-color:var(--brand-ochre);color:var(--text);border-color:var(--border-strong);margin-top:0.5rem;">πŸ“ˆ Open Allure Report β†—</a>
69
+ <button class="btn btn-sm" onclick="regenerateAllure('<%= project.id %>')" style="margin-top:0.25rem;">πŸ”„ Regenerate Allure</button>
70
+ <% } else { %>
71
+ <button class="btn btn-sm" onclick="regenerateAllure('<%= project.id %>')" style="margin-top:0.25rem;">πŸ“ˆ Generate Allure Report</button>
67
72
  <% } %>
68
73
  </div>
69
74
  </div>
@@ -139,6 +144,61 @@ document.addEventListener('DOMContentLoaded', () => {
139
144
  reportRendered.innerHTML = marked.parse(reportRaw.textContent || '');
140
145
  }
141
146
  });
147
+
148
+ async function rerunRun(projectId, runId) {
149
+ if (!confirm('Rerun this test?')) return;
150
+ const output = document.querySelector('.output-box') || document.createElement('div');
151
+ output.textContent = 'πŸ”„ Rerunning...';
152
+ output.className = 'output-box output-running';
153
+ output.style.display = 'block';
154
+ try {
155
+ const res = await fetch(`/runs/${runId}/rerun?project=${projectId}`, { method: 'POST' });
156
+ const data = await res.json();
157
+ output.textContent = data.output || data.error || 'Done';
158
+ output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
159
+ if (data.success && data.runId) {
160
+ output.textContent += `\n\nNew run: ${data.runId}`;
161
+ setTimeout(() => { window.location.href = `/runs/${data.runId}?project=${projectId}`; }, 2000);
162
+ }
163
+ } catch (err) {
164
+ output.textContent = 'Error: ' + err.message;
165
+ output.className = 'output-box output-error';
166
+ }
167
+ }
168
+
169
+ async function generateReport(projectId, runId) {
170
+ const output = document.querySelector('.output-box') || document.createElement('div');
171
+ output.textContent = 'πŸ“ Generating report...';
172
+ output.className = 'output-box output-running';
173
+ output.style.display = 'block';
174
+ try {
175
+ const res = await fetch(`/projects/${projectId}/report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runId }) });
176
+ const data = await res.json();
177
+ output.textContent = data.output || data.error || 'Done';
178
+ output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
179
+ if (data.success) setTimeout(() => location.reload(), 1500);
180
+ } catch (err) {
181
+ output.textContent = 'Error: ' + err.message;
182
+ output.className = 'output-box output-error';
183
+ }
184
+ }
185
+
186
+ async function regenerateAllure(projectId) {
187
+ const output = document.querySelector('.output-box') || document.createElement('div');
188
+ output.textContent = 'πŸ“ˆ Regenerating Allure report...';
189
+ output.className = 'output-box output-running';
190
+ output.style.display = 'block';
191
+ try {
192
+ const res = await fetch(`/runs/allure-regenerate?project=${projectId}`, { method: 'POST' });
193
+ const data = await res.json();
194
+ output.textContent = data.output || data.error || 'Done';
195
+ output.className = `output-box ${data.success ? 'output-success' : 'output-error'}`;
196
+ if (data.success) setTimeout(() => location.reload(), 1500);
197
+ } catch (err) {
198
+ output.textContent = 'Error: ' + err.message;
199
+ output.className = 'output-box output-error';
200
+ }
201
+ }
142
202
  </script>
143
203
 
144
204
  <% } %>
@@ -18,11 +18,11 @@
18
18
  <% stories.forEach((s, idx) => { %>
19
19
  <div class="story-card <%= cardColors[idx % cardColors.length] %>" style="margin-bottom:1.25rem;">
20
20
  <div class="story-header">
21
- <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="story-title" style="text-decoration:none;font-weight:600;"><%= s.name %></a>
21
+ <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="story-title" style="text-decoration:none;font-weight:600;"><%= s.name.replace('.md', '') %></a>
22
22
  <div class="story-badges">
23
- <% if (s.hasPlan) { %><span class="badge badge-plan">Planned</span><% } %>
24
- <% if (s.hasSpec) { %><span class="badge badge-spec">Tested</span><% } %>
25
- <% if (!s.hasPlan) { %><span class="badge badge-draft">Draft</span><% } %>
23
+ <% if (s.hasPlan && s.hasSpec) { %><span class="badge badge-spec">βœ… Planned & Tested</span>
24
+ <% } else if (s.hasPlan) { %><span class="badge badge-plan">πŸ“‹ Planned</span>
25
+ <% } else { %><span class="badge badge-draft">πŸ“ Draft</span><% } %>
26
26
  </div>
27
27
  </div>
28
28
  <div class="story-preview" style="margin:1rem 0;">
@@ -33,10 +33,14 @@
33
33
  <% }) %>
34
34
  <% } %>
35
35
  </div>
36
- <div class="story-actions" style="border-top:1px solid var(--border);padding-top:1rem;display:flex;align-items:center;">
37
- <button class="btn btn-sm btn-primary" onclick="runPipeline('<%= project.id %>', '<%= s.name %>')">β–Ά Execute Tests</button>
38
- <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="btn btn-sm" style="margin-left:0.5rem;">View Details</a>
39
- <span class="story-action-output"></span>
36
+ <div class="story-actions" style="border-top:1px solid var(--border);padding-top:1rem;">
37
+ <div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
38
+ <button class="btn btn-sm" onclick="callPipelineAction('<%= project.id %>', '<%= s.name %>', 'plan')" <%= s.hasPlan ? 'disabled style="opacity:0.5;"' : '' %>>πŸ“‹ Plan</button>
39
+ <button class="btn btn-sm" onclick="callPipelineAction('<%= project.id %>', '<%= s.name %>', 'generate')" <%= !s.hasPlan || s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>πŸ§ͺ Generate</button>
40
+ <button class="btn btn-sm btn-primary" onclick="callPipelineAction('<%= project.id %>', '<%= s.name %>', 'execute')" <%= !s.hasSpec ? 'disabled style="opacity:0.5;"' : '' %>>β–Ά Execute</button>
41
+ <a href="/stories/<%= s.name %>?project=<%= project.id %>" class="btn btn-sm">View Details β†’</a>
42
+ </div>
43
+ <div class="story-action-output" style="margin-top:0.5rem;"></div>
40
44
  </div>
41
45
  </div>
42
46
  <% }) %>
@@ -44,8 +48,28 @@
44
48
  <% } %>
45
49
 
46
50
  <script>
47
- async function runPipeline(projectId, storyName) {
48
- // Rediriger vers la page de dΓ©tail avec l'instruction de lancement automatique
49
- window.location.href = `/stories/${encodeURIComponent(storyName)}?project=${projectId}&autoRun=true`;
51
+ async function callPipelineAction(projectId, storyName, action) {
52
+ const card = event.target.closest('.story-card');
53
+ const outputDiv = card.querySelector('.story-action-output');
54
+ outputDiv.innerHTML = `<div class="output-box output-running" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;max-height:200px;">πŸš€ Running ${action}...</div>`;
55
+
56
+ try {
57
+ const res = await fetch(`/stories/${encodeURIComponent(storyName)}/${action}?project=${projectId}`, { method: 'POST' });
58
+ if (!res.body) { outputDiv.innerHTML = '<div class="output-box output-error" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;">Error: No stream</div>'; return; }
59
+ const reader = res.body.getReader();
60
+ const decoder = new TextDecoder('utf-8');
61
+ let text = '';
62
+ while (true) {
63
+ const { done, value } = await reader.read();
64
+ if (done) break;
65
+ text += decoder.decode(value, { stream: true });
66
+ outputDiv.innerHTML = `<div class="output-box output-running" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;max-height:200px;overflow-y:auto;">${text.replace(/\n/g, '<br>')}</div>`;
67
+ }
68
+ const isSuccess = text.toLowerCase().includes('finished: success');
69
+ outputDiv.innerHTML = `<div class="output-box ${isSuccess ? 'output-success' : 'output-error'}" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;max-height:200px;overflow-y:auto;">${text.replace(/\n/g, '<br>')}</div>`;
70
+ if (isSuccess) setTimeout(() => location.reload(), 2000);
71
+ } catch (err) {
72
+ outputDiv.innerHTML = `<div class="output-box output-error" style="margin-top:0.5rem;padding:0.75rem;font-size:0.8rem;">Error: ${err.message}</div>`;
73
+ }
50
74
  }
51
- </script>
75
+ </script>
@@ -2,138 +2,177 @@
2
2
  <% title = story.name %>
3
3
 
4
4
  <div class="page-header">
5
- <h1><%= story.name %></h1>
5
+ <h1><%= story.name.replace('.md', '') %></h1>
6
6
  <span class="badge badge-project"><%= project.name %></span>
7
7
  </div>
8
8
 
9
- <div class="row">
10
- <div class="col col-40">
11
- <div class="card card-cream">
12
- <h3>πŸ“ User Story</h3>
13
- <div id="story-content-md" style="display:none;"><%= story.content %></div>
14
- <div class="markdown-content" id="story-content-rendered"></div>
15
- </div>
9
+ <!-- ─── Phase Workflow Buttons ─── -->
10
+ <div class="card" style="margin-bottom:1.5rem;">
11
+ <h3>⚑ Pipeline Workflow</h3>
12
+ <p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:1rem;">Execute each phase in order. Verify the output before proceeding to the next step.</p>
13
+ <div style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;">
14
+ <button class="btn btn-sm" id="btn-plan" onclick="triggerPhase('plan')" <%= story.plan ? 'disabled style="opacity:0.5;"' : '' %>>
15
+ <%= story.plan ? 'βœ… Planned' : 'πŸ“‹ 1. Plan' %>
16
+ </button>
17
+ <span style="color:var(--text-disabled);">β†’</span>
18
+ <button class="btn btn-sm" id="btn-generate" onclick="triggerPhase('generate')" <%= !story.plan || story.spec ? 'disabled style="opacity:0.5;"' : '' %>>
19
+ <%= story.spec ? 'βœ… Generated' : 'πŸ§ͺ 2. Generate' %>
20
+ </button>
21
+ <span style="color:var(--text-disabled);">β†’</span>
22
+ <button class="btn btn-sm btn-primary" id="btn-execute" onclick="triggerPhase('execute')" <%= !story.spec ? 'disabled style="opacity:0.5;"' : '' %>>
23
+ β–Ά 3. Execute
24
+ </button>
25
+ <span style="color:var(--text-disabled);">β†’</span>
26
+ <button class="btn btn-sm" id="btn-report" onclick="triggerPhase('report')" <%= !story.hasRun ? 'disabled style="opacity:0.5;"' : '' %>>
27
+ πŸ“Š 4. Report
28
+ </button>
29
+ <% if (story.hasAllure) { %>
30
+ <a href="/allure-report?project=<%= project.id %>" class="btn btn-sm" target="_blank" style="background-color:var(--brand-ochre);">πŸ“ˆ Allure</a>
31
+ <% } %>
32
+ </div>
33
+ <div id="pulse-indicator" class="terminal-pulse" style="display:none;margin-top:1rem;">
34
+ <span class="pulse-dot"></span><span class="pulse-dot"></span><span class="pulse-dot"></span>
35
+ <span class="pulse-text">AI Agent running...</span>
36
+ </div>
37
+ <div id="phase-output" class="output-box" style="display:none;margin-top:1rem;"></div>
38
+ </div>
16
39
 
17
- <div class="card">
18
- <h3>⚑ Pipeline Controls</h3>
19
- <p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:1rem;">Trigger step-by-step or launch the fully automated agentic sequence.</p>
20
- <div class="action-buttons-vertical">
21
- <button class="btn btn-sm btn-primary" onclick="triggerExecute('<%= story.name %>')">β–Ά Execute Tests</button>
22
- <a href="/runs?project=<%= project.id %>" class="btn btn-sm" style="margin-top:0.5rem;justify-content:center;">View Test Results β†’</a>
23
- </div>
24
-
25
- <!-- Streaming console output box -->
26
- <div id="pipeline-status" class="output-box" style="display:none;"></div>
27
-
28
- <!-- Neon loader pulse indicator -->
29
- <div id="pipeline-pulse" class="terminal-pulse" style="display:none;margin-top:1rem;">
30
- <span class="pulse-dot"></span>
31
- <span class="pulse-dot"></span>
32
- <span class="pulse-dot"></span>
33
- <span class="pulse-text">AI Agent is running...</span>
34
- </div>
35
- </div>
40
+ <!-- ─── Tabs: Story | Test Plan | Test Code | Results ─── -->
41
+ <div class="card">
42
+ <div style="display:flex;gap:1rem;border-bottom:2px solid var(--border);margin-bottom:1.5rem;padding-bottom:0.5rem;">
43
+ <button class="btn btn-sm tab-btn active-tab" data-tab="story-tab" style="border-bottom:2px solid var(--primary);border-radius:0;">πŸ“ Story</button>
44
+ <button class="btn btn-sm tab-btn" data-tab="plan-tab" style="border-radius:0;">πŸ“‹ Test Plan</button>
45
+ <button class="btn btn-sm tab-btn" data-tab="spec-tab" style="border-radius:0;">πŸ§ͺ Test Code</button>
46
+ <button class="btn btn-sm tab-btn" data-tab="results-tab" style="border-radius:0;">⚑ Results</button>
47
+ </div>
48
+
49
+ <!-- Tab: Story Content -->
50
+ <div id="story-tab" class="tab-content">
51
+ <div id="story-content-md" style="display:none;"><%= story.content %></div>
52
+ <div class="markdown-content" id="story-content-rendered"></div>
36
53
  </div>
37
54
 
38
- <div class="col col-60">
55
+ <!-- Tab: Test Plan -->
56
+ <div id="plan-tab" class="tab-content" style="display:none;">
39
57
  <% if (story.plan) { %>
40
- <div class="card">
41
- <h3>πŸ“‹ Test Plan <a href="/stories/<%= story.name %>?project=<%= project.id %>&tab=plan" class="btn btn-sm" style="margin-left:auto;">Refresh</a></h3>
42
58
  <div id="story-plan-md" style="display:none;"><%= story.plan %></div>
43
59
  <div class="markdown-content" id="story-plan-rendered"></div>
44
- </div>
60
+ <% } else { %>
61
+ <div class="empty-state" style="padding:2rem;">
62
+ <p>No test plan yet. Click <strong>"πŸ“‹ 1. Plan"</strong> above to generate one.</p>
63
+ </div>
45
64
  <% } %>
65
+ </div>
46
66
 
67
+ <!-- Tab: Test Code -->
68
+ <div id="spec-tab" class="tab-content" style="display:none;">
47
69
  <% if (story.spec) { %>
48
- <div class="card">
49
- <h3>πŸ§ͺ Test Spec: <%= story.specName %></h3>
50
- <pre class="code-block"><%= story.spec %></pre>
51
- </div>
70
+ <pre class="code-block" style="max-height:500px;"><%= story.spec %></pre>
71
+ <div style="margin-top:1rem;">
72
+ <a href="/data?project=<%= project.id %>" class="btn btn-sm">View All Test Files</a>
73
+ </div>
74
+ <% } else { %>
75
+ <div class="empty-state" style="padding:2rem;">
76
+ <p>No test code yet. Complete the <strong>Plan</strong> phase first, then click <strong>"πŸ§ͺ 2. Generate"</strong>.</p>
77
+ </div>
78
+ <% } %>
79
+ </div>
80
+
81
+ <!-- Tab: Results -->
82
+ <div id="results-tab" class="tab-content" style="display:none;">
83
+ <% if (story.results && story.results.length > 0) { %>
84
+ <table class="info-table">
85
+ <thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Passed</th><th>Failed</th><th>Actions</th></tr></thead>
86
+ <tbody>
87
+ <% story.results.slice(0, 5).forEach(r => { %>
88
+ <tr>
89
+ <td><%= r.id.substring(0, 20) %></td>
90
+ <td><%= r.success ? 'βœ… Passed' : (r.success === null ? '⏳ Running' : '❌ Failed') %></td>
91
+ <td><%= r.duration ? (r.duration / 1000).toFixed(1) + 's' : 'N/A' %></td>
92
+ <td><%= r.passedCount %></td>
93
+ <td><%= r.failedCount %></td>
94
+ <td><a href="/runs/<%= r.id %>?project=<%= project.id %>" class="btn btn-sm">View</a></td>
95
+ </tr>
96
+ <% }) %>
97
+ </tbody>
98
+ </table>
99
+ <div style="margin-top:1rem;">
100
+ <a href="/runs?project=<%= project.id %>" class="btn btn-sm">All Runs β†’</a>
101
+ <a href="/export/report/latest?project=<%= project.id %>" class="btn btn-sm" target="_blank">πŸ“₯ Download Report</a>
102
+ </div>
103
+ <% } else { %>
104
+ <div class="empty-state" style="padding:2rem;">
105
+ <p>No execution results yet. Click <strong>"β–Ά 3. Execute"</strong> above to run the tests.</p>
106
+ </div>
52
107
  <% } %>
53
108
  </div>
54
109
  </div>
55
110
 
56
111
  <script>
57
- // Parse Markdown on Client-Side for extremely high-fidelity tables, checkmarks, list layout
112
+ // ─── Tab switching ───
58
113
  document.addEventListener('DOMContentLoaded', () => {
114
+ // Render markdown
59
115
  const contentRaw = document.getElementById('story-content-md');
60
116
  const contentRendered = document.getElementById('story-content-rendered');
61
- if (contentRaw && contentRendered) {
117
+ if (contentRaw && contentRendered && typeof marked !== 'undefined') {
62
118
  contentRendered.innerHTML = marked.parse(contentRaw.textContent || '');
63
119
  }
64
-
65
120
  const planRaw = document.getElementById('story-plan-md');
66
121
  const planRendered = document.getElementById('story-plan-rendered');
67
- if (planRaw && planRendered) {
122
+ if (planRaw && planRendered && typeof marked !== 'undefined') {
68
123
  planRendered.innerHTML = marked.parse(planRaw.textContent || '');
69
124
  }
125
+
126
+ // Tab click handlers
127
+ document.querySelectorAll('.tab-btn').forEach(btn => {
128
+ btn.addEventListener('click', () => {
129
+ document.querySelectorAll('.tab-btn').forEach(b => { b.style.borderBottom = '2px solid transparent'; b.style.color = 'var(--text-muted)'; });
130
+ btn.style.borderBottom = '2px solid var(--primary)';
131
+ btn.style.color = 'var(--text)';
132
+ document.querySelectorAll('.tab-content').forEach(tc => tc.style.display = 'none');
133
+ document.getElementById(btn.dataset.tab).style.display = 'block';
134
+ });
135
+ });
70
136
  });
71
137
 
72
- async function callPipeline(action, storyName) {
73
- const status = document.getElementById('pipeline-status');
74
- const pulse = document.getElementById('pipeline-pulse');
75
-
76
- // Reset and show containers
77
- status.style.display = 'block';
78
- status.textContent = `πŸš€ Initializing QA agent action [${action}]...\n\n`;
79
- status.className = 'output-box output-running';
138
+ // ─── Phase execution ───
139
+ async function triggerPhase(action) {
140
+ const output = document.getElementById('phase-output');
141
+ const pulse = document.getElementById('pulse-indicator');
142
+ output.style.display = 'block';
143
+ output.textContent = `πŸš€ Running ${action}...\n`;
144
+ output.className = 'output-box output-running';
80
145
  pulse.style.display = 'flex';
81
146
 
82
147
  try {
83
- const res = await fetch(`/stories/${encodeURIComponent(storyName)}/${action}?project=<%= project.id %>`, { method: 'POST' });
84
-
85
- if (!res.body) {
86
- status.textContent += "\nError: The server did not return a readable stream.";
87
- pulse.style.display = 'none';
88
- return;
89
- }
90
-
148
+ const res = await fetch(`/stories/<%= story.name %>/${action}?project=<%= project.id %>`, { method: 'POST' });
149
+ if (!res.body) { output.textContent += '\nError: No stream'; pulse.style.display = 'none'; return; }
91
150
  const reader = res.body.getReader();
92
151
  const decoder = new TextDecoder('utf-8');
93
-
94
152
  while (true) {
95
153
  const { done, value } = await reader.read();
96
154
  if (done) break;
97
-
98
- const chunk = decoder.decode(value, { stream: true });
99
- status.textContent += chunk;
100
- status.scrollTop = status.scrollHeight; // Force Autoscroll
155
+ output.textContent += decoder.decode(value, { stream: true });
156
+ output.scrollTop = output.scrollHeight;
101
157
  }
102
-
103
- // Hide pulse loader once done
104
158
  pulse.style.display = 'none';
105
-
106
- // Parse final status
107
- const finishedText = status.textContent.toLowerCase();
108
- if (finishedText.includes('[finished: success]')) {
109
- status.className = 'output-box output-success';
110
- // Reload page to reflect new plans/specs in EJS
111
- setTimeout(() => { location.reload(); }, 2000);
112
- } else {
113
- status.className = 'output-box output-error';
114
- }
115
-
116
- } catch (error) {
117
- status.textContent += `\n\nNetwork or server connection error: ${error.message}`;
118
- status.className = 'output-box output-error';
159
+ const isSuccess = output.textContent.toLowerCase().includes('finished: success');
160
+ output.className = `output-box ${isSuccess ? 'output-success' : 'output-error'}`;
161
+ if (isSuccess) setTimeout(() => location.reload(), 2000);
162
+ } catch (err) {
163
+ output.textContent += `\nError: ${err.message}`;
164
+ output.className = 'output-box output-error';
119
165
  pulse.style.display = 'none';
120
166
  }
121
167
  }
122
168
 
123
- function triggerPlan(s) { callPipeline('plan', s); }
124
- function triggerGenerate(s) { callPipeline('generate', s); }
125
- function triggerExecute(s) { callPipeline('execute', s); }
126
- function triggerFull(s) { callPipeline('full-pipeline', s); }
127
-
169
+ // ─── Auto-execute on load ───
128
170
  document.addEventListener('DOMContentLoaded', () => {
129
171
  const urlParams = new URLSearchParams(window.location.search);
130
172
  if (urlParams.get('autoRun') === 'true') {
131
- // Nettoyer l'URL afin qu'un rechargement manuel ne relance pas le processus
132
173
  const newUrl = window.location.pathname + '?project=' + urlParams.get('project');
133
174
  window.history.replaceState({}, '', newUrl);
134
-
135
- // DΓ©clencher l'exΓ©cution des tests
136
- triggerExecute('<%= story.name %>');
175
+ triggerPhase('execute');
137
176
  }
138
177
  });
139
- </script>
178
+ </script>