elspais 0.11.2__py3-none-any.whl → 0.43.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elspais/__init__.py +1 -10
- elspais/{sponsors/__init__.py → associates.py} +102 -56
- elspais/cli.py +366 -69
- elspais/commands/__init__.py +9 -3
- elspais/commands/analyze.py +118 -169
- elspais/commands/changed.py +12 -23
- elspais/commands/config_cmd.py +10 -13
- elspais/commands/edit.py +33 -13
- elspais/commands/example_cmd.py +319 -0
- elspais/commands/hash_cmd.py +161 -183
- elspais/commands/health.py +1177 -0
- elspais/commands/index.py +98 -115
- elspais/commands/init.py +99 -22
- elspais/commands/reformat_cmd.py +41 -433
- elspais/commands/rules_cmd.py +2 -2
- elspais/commands/trace.py +443 -324
- elspais/commands/validate.py +193 -411
- elspais/config/__init__.py +799 -5
- elspais/{core/content_rules.py → content_rules.py} +20 -2
- elspais/docs/cli/assertions.md +67 -0
- elspais/docs/cli/commands.md +304 -0
- elspais/docs/cli/config.md +262 -0
- elspais/docs/cli/format.md +66 -0
- elspais/docs/cli/git.md +45 -0
- elspais/docs/cli/health.md +190 -0
- elspais/docs/cli/hierarchy.md +60 -0
- elspais/docs/cli/ignore.md +72 -0
- elspais/docs/cli/mcp.md +245 -0
- elspais/docs/cli/quickstart.md +58 -0
- elspais/docs/cli/traceability.md +89 -0
- elspais/docs/cli/validation.md +96 -0
- elspais/graph/GraphNode.py +383 -0
- elspais/graph/__init__.py +40 -0
- elspais/graph/annotators.py +927 -0
- elspais/graph/builder.py +1886 -0
- elspais/graph/deserializer.py +248 -0
- elspais/graph/factory.py +284 -0
- elspais/graph/metrics.py +127 -0
- elspais/graph/mutations.py +161 -0
- elspais/graph/parsers/__init__.py +156 -0
- elspais/graph/parsers/code.py +213 -0
- elspais/graph/parsers/comments.py +112 -0
- elspais/graph/parsers/config_helpers.py +29 -0
- elspais/graph/parsers/heredocs.py +225 -0
- elspais/graph/parsers/journey.py +131 -0
- elspais/graph/parsers/remainder.py +79 -0
- elspais/graph/parsers/requirement.py +347 -0
- elspais/graph/parsers/results/__init__.py +6 -0
- elspais/graph/parsers/results/junit_xml.py +229 -0
- elspais/graph/parsers/results/pytest_json.py +313 -0
- elspais/graph/parsers/test.py +305 -0
- elspais/graph/relations.py +78 -0
- elspais/graph/serialize.py +216 -0
- elspais/html/__init__.py +8 -0
- elspais/html/generator.py +731 -0
- elspais/html/templates/trace_view.html.j2 +2151 -0
- elspais/mcp/__init__.py +45 -29
- elspais/mcp/__main__.py +5 -1
- elspais/mcp/file_mutations.py +138 -0
- elspais/mcp/server.py +1998 -244
- elspais/testing/__init__.py +3 -3
- elspais/testing/config.py +3 -0
- elspais/testing/mapper.py +1 -1
- elspais/testing/scanner.py +301 -12
- elspais/utilities/__init__.py +1 -0
- elspais/utilities/docs_loader.py +115 -0
- elspais/utilities/git.py +607 -0
- elspais/{core → utilities}/hasher.py +8 -22
- elspais/utilities/md_renderer.py +189 -0
- elspais/{core → utilities}/patterns.py +56 -51
- elspais/utilities/reference_config.py +626 -0
- elspais/validation/__init__.py +19 -0
- elspais/validation/format.py +264 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
- elspais-0.43.5.dist-info/RECORD +80 -0
- elspais/config/defaults.py +0 -179
- elspais/config/loader.py +0 -494
- elspais/core/__init__.py +0 -21
- elspais/core/git.py +0 -346
- elspais/core/models.py +0 -320
- elspais/core/parser.py +0 -639
- elspais/core/rules.py +0 -509
- elspais/mcp/context.py +0 -172
- elspais/mcp/serializers.py +0 -112
- elspais/reformat/__init__.py +0 -50
- elspais/reformat/detector.py +0 -112
- elspais/reformat/hierarchy.py +0 -247
- elspais/reformat/line_breaks.py +0 -218
- elspais/reformat/prompts.py +0 -133
- elspais/reformat/transformer.py +0 -266
- elspais/trace_view/__init__.py +0 -55
- elspais/trace_view/coverage.py +0 -183
- elspais/trace_view/generators/__init__.py +0 -12
- elspais/trace_view/generators/base.py +0 -334
- elspais/trace_view/generators/csv.py +0 -118
- elspais/trace_view/generators/markdown.py +0 -170
- elspais/trace_view/html/__init__.py +0 -33
- elspais/trace_view/html/generator.py +0 -1140
- elspais/trace_view/html/templates/base.html +0 -283
- elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
- elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
- elspais/trace_view/html/templates/components/legend_modal.html +0 -69
- elspais/trace_view/html/templates/components/review_panel.html +0 -118
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
- elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
- elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
- elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
- elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
- elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
- elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
- elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
- elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
- elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
- elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
- elspais/trace_view/html/templates/partials/scripts.js +0 -1741
- elspais/trace_view/html/templates/partials/styles.css +0 -1756
- elspais/trace_view/models.py +0 -378
- elspais/trace_view/review/__init__.py +0 -63
- elspais/trace_view/review/branches.py +0 -1142
- elspais/trace_view/review/models.py +0 -1200
- elspais/trace_view/review/position.py +0 -591
- elspais/trace_view/review/server.py +0 -1032
- elspais/trace_view/review/status.py +0 -455
- elspais/trace_view/review/storage.py +0 -1343
- elspais/trace_view/scanning.py +0 -213
- elspais/trace_view/specs/README.md +0 -84
- elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
- elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
- elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
- elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
- elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
- elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
- elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
- elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
- elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
- elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
- elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
- elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
- elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
- elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
- elspais-0.11.2.dist-info/RECORD +0 -101
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
- {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,2151 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Requirements Traceability</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
+
CSS STYLES - Trace View
|
|
10
|
+
═══════════════════════════════════════════════════════════════════════════ */
|
|
11
|
+
|
|
12
|
+
* {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
20
|
+
font-size: 14px;
|
|
21
|
+
line-height: 1.5;
|
|
22
|
+
color: #333;
|
|
23
|
+
background: #f8f9fa;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
Header
|
|
28
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
.header {
|
|
31
|
+
background: white;
|
|
32
|
+
border-bottom: 1px solid #e9ecef;
|
|
33
|
+
padding: 16px 24px;
|
|
34
|
+
display: flex;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
align-items: center;
|
|
37
|
+
flex-wrap: wrap;
|
|
38
|
+
gap: 16px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.header-title {
|
|
42
|
+
font-size: 1.5rem;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
color: #212529;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.header-stats {
|
|
48
|
+
display: flex;
|
|
49
|
+
gap: 8px;
|
|
50
|
+
align-items: center;
|
|
51
|
+
flex-wrap: wrap;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.stat-badge {
|
|
55
|
+
padding: 4px 12px;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
font-size: 0.875rem;
|
|
58
|
+
font-weight: 600;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition: opacity 0.15s, transform 0.1s;
|
|
61
|
+
user-select: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.stat-badge:hover {
|
|
65
|
+
opacity: 0.85;
|
|
66
|
+
transform: scale(1.02);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.stat-badge.active {
|
|
70
|
+
/* When filter is active (hiding that level), show hollow/outline appearance */
|
|
71
|
+
box-shadow: inset 0 0 0 2px currentColor;
|
|
72
|
+
background: transparent !important;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Level badges need dark text when active (background becomes transparent) */
|
|
76
|
+
.stat-badge.prd.active { color: #212529; }
|
|
77
|
+
.stat-badge.ops.active { color: #fd7e14; }
|
|
78
|
+
.stat-badge.dev.active { color: #20c997; }
|
|
79
|
+
.stat-badge.core.active { color: #0d6efd; }
|
|
80
|
+
|
|
81
|
+
.stat-badge.prd { background: #212529; color: white; }
|
|
82
|
+
.stat-badge.ops { background: #fd7e14; color: white; }
|
|
83
|
+
.stat-badge.dev { background: #20c997; color: white; }
|
|
84
|
+
.stat-badge.core { background: #0d6efd; color: white; }
|
|
85
|
+
.stat-badge.code {
|
|
86
|
+
background: #6c757d;
|
|
87
|
+
color: white;
|
|
88
|
+
}
|
|
89
|
+
.stat-badge.code.active {
|
|
90
|
+
color: #6c757d;
|
|
91
|
+
}
|
|
92
|
+
.stat-badge.tests {
|
|
93
|
+
background: #198754;
|
|
94
|
+
color: white;
|
|
95
|
+
}
|
|
96
|
+
.stat-badge.tests.active {
|
|
97
|
+
color: #198754;
|
|
98
|
+
}
|
|
99
|
+
.stat-badge.results {
|
|
100
|
+
background: #6610f2;
|
|
101
|
+
color: white;
|
|
102
|
+
}
|
|
103
|
+
.stat-badge.results.active {
|
|
104
|
+
color: #6610f2;
|
|
105
|
+
}
|
|
106
|
+
.stat-badge.associated {
|
|
107
|
+
background: #6f42c1;
|
|
108
|
+
color: white;
|
|
109
|
+
}
|
|
110
|
+
.stat-badge.associated.active {
|
|
111
|
+
color: #6f42c1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.assertion-summary {
|
|
115
|
+
font-size: 0.85em;
|
|
116
|
+
color: #6c757d;
|
|
117
|
+
padding: 4px 0;
|
|
118
|
+
border-top: 1px solid #dee2e6;
|
|
119
|
+
margin-top: 8px;
|
|
120
|
+
}
|
|
121
|
+
.assertion-summary strong {
|
|
122
|
+
color: #212529;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.header-meta {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 16px;
|
|
128
|
+
align-items: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.version-badge {
|
|
132
|
+
padding: 4px 8px;
|
|
133
|
+
background: #e9ecef;
|
|
134
|
+
border-radius: 4px;
|
|
135
|
+
font-size: 0.75rem;
|
|
136
|
+
color: #6c757d;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.legend-btn {
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 4px;
|
|
143
|
+
padding: 6px 12px;
|
|
144
|
+
background: #6c757d;
|
|
145
|
+
color: white;
|
|
146
|
+
border: none;
|
|
147
|
+
border-radius: 4px;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
font-size: 0.875rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.legend-btn:hover {
|
|
153
|
+
background: #5a6268;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
Toolbar
|
|
158
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
159
|
+
|
|
160
|
+
.toolbar {
|
|
161
|
+
background: white;
|
|
162
|
+
border-bottom: 1px solid #e9ecef;
|
|
163
|
+
padding: 12px 24px;
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-wrap: wrap;
|
|
166
|
+
gap: 12px;
|
|
167
|
+
align-items: center;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.btn-group {
|
|
171
|
+
display: flex;
|
|
172
|
+
border: 1px solid #dee2e6;
|
|
173
|
+
border-radius: 4px;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.btn-group .btn {
|
|
178
|
+
border: none;
|
|
179
|
+
border-right: 1px solid #dee2e6;
|
|
180
|
+
background: white;
|
|
181
|
+
padding: 8px 16px;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
font-size: 0.875rem;
|
|
184
|
+
color: #495057;
|
|
185
|
+
transition: background 0.15s;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.btn-group .btn:last-child {
|
|
189
|
+
border-right: none;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.btn-group .btn:hover {
|
|
193
|
+
background: #f8f9fa;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.btn-group .btn.active {
|
|
197
|
+
background: #0d6efd;
|
|
198
|
+
color: white;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.toggle-group {
|
|
202
|
+
display: flex;
|
|
203
|
+
gap: 16px;
|
|
204
|
+
align-items: center;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.toggle-item {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 6px;
|
|
211
|
+
font-size: 0.875rem;
|
|
212
|
+
color: #495057;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.toggle-item input[type="checkbox"] {
|
|
217
|
+
width: 16px;
|
|
218
|
+
height: 16px;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.toolbar-actions {
|
|
223
|
+
display: flex;
|
|
224
|
+
gap: 8px;
|
|
225
|
+
margin-left: auto;
|
|
226
|
+
align-items: center;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.btn {
|
|
230
|
+
padding: 8px 16px;
|
|
231
|
+
border: 1px solid #dee2e6;
|
|
232
|
+
background: white;
|
|
233
|
+
border-radius: 4px;
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
font-size: 0.875rem;
|
|
236
|
+
color: #495057;
|
|
237
|
+
transition: background 0.15s;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.btn:hover {
|
|
241
|
+
background: #f8f9fa;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.btn-icon {
|
|
245
|
+
padding: 8px 12px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.counter {
|
|
249
|
+
font-size: 0.875rem;
|
|
250
|
+
color: #6c757d;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
254
|
+
Tabs
|
|
255
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
256
|
+
|
|
257
|
+
.tabs {
|
|
258
|
+
background: white;
|
|
259
|
+
border-bottom: 1px solid #e9ecef;
|
|
260
|
+
padding: 0 24px;
|
|
261
|
+
display: flex;
|
|
262
|
+
gap: 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.tab {
|
|
266
|
+
padding: 12px 24px;
|
|
267
|
+
border: none;
|
|
268
|
+
background: none;
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
font-size: 0.875rem;
|
|
271
|
+
color: #6c757d;
|
|
272
|
+
border-bottom: 2px solid transparent;
|
|
273
|
+
transition: all 0.15s;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.tab:hover {
|
|
277
|
+
color: #495057;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.tab.active {
|
|
281
|
+
color: #0d6efd;
|
|
282
|
+
border-bottom-color: #0d6efd;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Tab Content */
|
|
286
|
+
.tab-content {
|
|
287
|
+
display: none;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.tab-content.active {
|
|
291
|
+
display: block;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Journey Cards */
|
|
295
|
+
.journey-toolbar {
|
|
296
|
+
display: flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: 16px;
|
|
299
|
+
margin-bottom: 16px;
|
|
300
|
+
padding: 12px 16px;
|
|
301
|
+
background: #f8f9fa;
|
|
302
|
+
border-radius: 8px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.journey-search {
|
|
306
|
+
flex: 1;
|
|
307
|
+
max-width: 400px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.journey-search input {
|
|
311
|
+
width: 100%;
|
|
312
|
+
padding: 8px 12px;
|
|
313
|
+
border: 1px solid #dee2e6;
|
|
314
|
+
border-radius: 6px;
|
|
315
|
+
font-size: 0.875rem;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.journey-search input:focus {
|
|
319
|
+
outline: none;
|
|
320
|
+
border-color: #4299e1;
|
|
321
|
+
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.15);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.journey-count {
|
|
325
|
+
font-size: 0.875rem;
|
|
326
|
+
color: #6c757d;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.journey-list {
|
|
330
|
+
display: grid;
|
|
331
|
+
gap: 16px;
|
|
332
|
+
padding: 16px 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.journey-card {
|
|
336
|
+
background: white;
|
|
337
|
+
border: 1px solid #e9ecef;
|
|
338
|
+
border-radius: 8px;
|
|
339
|
+
padding: 20px;
|
|
340
|
+
transition: box-shadow 0.2s, border-color 0.2s;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.journey-card:hover {
|
|
344
|
+
border-color: #4299e1;
|
|
345
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.journey-card.hidden {
|
|
349
|
+
display: none;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.journey-card-header {
|
|
353
|
+
display: flex;
|
|
354
|
+
align-items: flex-start;
|
|
355
|
+
gap: 12px;
|
|
356
|
+
margin-bottom: 12px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.journey-card .journey-id {
|
|
360
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
361
|
+
font-size: 0.75rem;
|
|
362
|
+
color: white;
|
|
363
|
+
background: #6c757d;
|
|
364
|
+
padding: 4px 8px;
|
|
365
|
+
border-radius: 4px;
|
|
366
|
+
flex-shrink: 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.journey-card h3 {
|
|
370
|
+
font-size: 1.125rem;
|
|
371
|
+
font-weight: 600;
|
|
372
|
+
color: #212529;
|
|
373
|
+
margin: 0;
|
|
374
|
+
line-height: 1.4;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.journey-meta {
|
|
378
|
+
display: grid;
|
|
379
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
380
|
+
gap: 12px;
|
|
381
|
+
margin-bottom: 12px;
|
|
382
|
+
padding: 12px;
|
|
383
|
+
background: #f8f9fa;
|
|
384
|
+
border-radius: 6px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.journey-meta-item {
|
|
388
|
+
display: flex;
|
|
389
|
+
flex-direction: column;
|
|
390
|
+
gap: 2px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.journey-meta-label {
|
|
394
|
+
font-size: 0.7rem;
|
|
395
|
+
font-weight: 600;
|
|
396
|
+
text-transform: uppercase;
|
|
397
|
+
letter-spacing: 0.5px;
|
|
398
|
+
color: #6c757d;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.journey-meta-value {
|
|
402
|
+
font-size: 0.875rem;
|
|
403
|
+
color: #212529;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.journey-card .journey-description {
|
|
407
|
+
font-size: 0.875rem;
|
|
408
|
+
color: #495057;
|
|
409
|
+
line-height: 1.6;
|
|
410
|
+
white-space: pre-wrap;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.no-journeys {
|
|
414
|
+
text-align: center;
|
|
415
|
+
padding: 48px;
|
|
416
|
+
color: #6c757d;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* Journey Grouping */
|
|
420
|
+
.journey-group-controls {
|
|
421
|
+
display: flex;
|
|
422
|
+
align-items: center;
|
|
423
|
+
gap: 8px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.journey-group-controls label {
|
|
427
|
+
font-size: 0.8rem;
|
|
428
|
+
color: #6c757d;
|
|
429
|
+
font-weight: 500;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.journey-group-controls select {
|
|
433
|
+
padding: 6px 10px;
|
|
434
|
+
border: 1px solid #dee2e6;
|
|
435
|
+
border-radius: 6px;
|
|
436
|
+
font-size: 0.8rem;
|
|
437
|
+
background: white;
|
|
438
|
+
cursor: pointer;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.journey-group-controls select:focus {
|
|
442
|
+
outline: none;
|
|
443
|
+
border-color: #4299e1;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.journey-group {
|
|
447
|
+
margin-bottom: 24px;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.journey-group-header {
|
|
451
|
+
display: flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
gap: 10px;
|
|
454
|
+
padding: 10px 14px;
|
|
455
|
+
background: linear-gradient(to right, #e9ecef, #f8f9fa);
|
|
456
|
+
border-radius: 8px;
|
|
457
|
+
margin-bottom: 12px;
|
|
458
|
+
cursor: pointer;
|
|
459
|
+
user-select: none;
|
|
460
|
+
transition: background 0.2s;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.journey-group-header:hover {
|
|
464
|
+
background: linear-gradient(to right, #dee2e6, #e9ecef);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.journey-group-header .expand-icon {
|
|
468
|
+
font-size: 0.75rem;
|
|
469
|
+
color: #6c757d;
|
|
470
|
+
transition: transform 0.2s;
|
|
471
|
+
width: 16px;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.journey-group.collapsed .expand-icon {
|
|
475
|
+
transform: rotate(-90deg);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.journey-group-header h4 {
|
|
479
|
+
margin: 0;
|
|
480
|
+
font-size: 0.9rem;
|
|
481
|
+
font-weight: 600;
|
|
482
|
+
color: #495057;
|
|
483
|
+
flex: 1;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.journey-group-count {
|
|
487
|
+
font-size: 0.75rem;
|
|
488
|
+
color: #6c757d;
|
|
489
|
+
background: white;
|
|
490
|
+
padding: 2px 8px;
|
|
491
|
+
border-radius: 10px;
|
|
492
|
+
border: 1px solid #dee2e6;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.journey-group-cards {
|
|
496
|
+
display: grid;
|
|
497
|
+
gap: 12px;
|
|
498
|
+
padding-left: 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.journey-group.collapsed .journey-group-cards {
|
|
502
|
+
display: none;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* Compact card variant for grouped view */
|
|
506
|
+
.journey-card.compact {
|
|
507
|
+
padding: 14px 16px;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.journey-card.compact .journey-card-header {
|
|
511
|
+
margin-bottom: 8px;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.journey-card.compact h3 {
|
|
515
|
+
font-size: 1rem;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.journey-card.compact .journey-description {
|
|
519
|
+
font-size: 0.8rem;
|
|
520
|
+
max-height: 80px;
|
|
521
|
+
overflow: hidden;
|
|
522
|
+
position: relative;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.journey-card.compact .journey-description::after {
|
|
526
|
+
content: '';
|
|
527
|
+
position: absolute;
|
|
528
|
+
bottom: 0;
|
|
529
|
+
left: 0;
|
|
530
|
+
right: 0;
|
|
531
|
+
height: 30px;
|
|
532
|
+
background: linear-gradient(transparent, white);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.journey-card.compact.expanded .journey-description {
|
|
536
|
+
max-height: none;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.journey-card.compact.expanded .journey-description::after {
|
|
540
|
+
display: none;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
544
|
+
Main Content
|
|
545
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
546
|
+
|
|
547
|
+
.main-content {
|
|
548
|
+
padding: 24px;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.section-title {
|
|
552
|
+
font-size: 1rem;
|
|
553
|
+
font-weight: 600;
|
|
554
|
+
color: #495057;
|
|
555
|
+
margin-bottom: 16px;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.table-container {
|
|
559
|
+
background: white;
|
|
560
|
+
border-radius: 8px;
|
|
561
|
+
border: 1px solid #e9ecef;
|
|
562
|
+
overflow: hidden;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
566
|
+
Traceability Table
|
|
567
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
568
|
+
|
|
569
|
+
.trace-table {
|
|
570
|
+
width: 100%;
|
|
571
|
+
border-collapse: collapse;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.trace-table th {
|
|
575
|
+
background: #f8f9fa;
|
|
576
|
+
padding: 12px 16px;
|
|
577
|
+
text-align: left;
|
|
578
|
+
font-weight: 600;
|
|
579
|
+
color: #495057;
|
|
580
|
+
border-bottom: 1px solid #e9ecef;
|
|
581
|
+
position: sticky;
|
|
582
|
+
top: 0;
|
|
583
|
+
z-index: 10;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.trace-table td {
|
|
587
|
+
padding: 10px 16px;
|
|
588
|
+
border-bottom: 1px solid #f1f3f4;
|
|
589
|
+
vertical-align: middle;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.trace-table tr:hover {
|
|
593
|
+
background: #f8f9fa;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.trace-table tr.hidden {
|
|
597
|
+
display: none;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* Column widths */
|
|
601
|
+
.col-id { width: 180px; }
|
|
602
|
+
.col-title { min-width: 200px; }
|
|
603
|
+
.col-level { width: 70px; }
|
|
604
|
+
.col-status { width: 100px; }
|
|
605
|
+
.col-cov { width: 60px; text-align: center; }
|
|
606
|
+
.col-topic { width: 180px; }
|
|
607
|
+
|
|
608
|
+
/* Filter inputs in header */
|
|
609
|
+
.filter-input {
|
|
610
|
+
width: 100%;
|
|
611
|
+
padding: 6px 8px;
|
|
612
|
+
border: 1px solid #dee2e6;
|
|
613
|
+
border-radius: 4px;
|
|
614
|
+
font-size: 0.75rem;
|
|
615
|
+
margin-top: 6px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.filter-select {
|
|
619
|
+
width: 100%;
|
|
620
|
+
padding: 6px 8px;
|
|
621
|
+
border: 1px solid #dee2e6;
|
|
622
|
+
border-radius: 4px;
|
|
623
|
+
font-size: 0.75rem;
|
|
624
|
+
margin-top: 6px;
|
|
625
|
+
background: white;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
629
|
+
Tree Structure
|
|
630
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
631
|
+
|
|
632
|
+
.tree-cell {
|
|
633
|
+
display: flex;
|
|
634
|
+
align-items: center;
|
|
635
|
+
gap: 4px;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.tree-indent {
|
|
639
|
+
display: inline-block;
|
|
640
|
+
width: 20px;
|
|
641
|
+
flex-shrink: 0;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.tree-toggle {
|
|
645
|
+
width: 20px;
|
|
646
|
+
height: 20px;
|
|
647
|
+
display: flex;
|
|
648
|
+
align-items: center;
|
|
649
|
+
justify-content: center;
|
|
650
|
+
cursor: pointer;
|
|
651
|
+
color: #6c757d;
|
|
652
|
+
flex-shrink: 0;
|
|
653
|
+
border: none;
|
|
654
|
+
background: none;
|
|
655
|
+
font-size: 10px;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.tree-toggle:hover {
|
|
659
|
+
color: #495057;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.tree-toggle.collapsed::before {
|
|
663
|
+
content: "▶";
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.tree-toggle.expanded::before {
|
|
667
|
+
content: "▼";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.tree-toggle.leaf {
|
|
671
|
+
visibility: hidden;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.req-id {
|
|
675
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
|
676
|
+
font-size: 0.875rem;
|
|
677
|
+
color: #0d6efd;
|
|
678
|
+
text-decoration: none;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.req-id:hover {
|
|
682
|
+
text-decoration: underline;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.file-link {
|
|
686
|
+
color: #198754;
|
|
687
|
+
text-decoration: none;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.file-link:hover {
|
|
691
|
+
text-decoration: underline;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.assertion-badges {
|
|
695
|
+
display: flex;
|
|
696
|
+
gap: 2px;
|
|
697
|
+
margin-left: 4px;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.assertion-badge {
|
|
701
|
+
display: inline-flex;
|
|
702
|
+
align-items: center;
|
|
703
|
+
justify-content: center;
|
|
704
|
+
width: 18px;
|
|
705
|
+
height: 18px;
|
|
706
|
+
background: #e7f1ff;
|
|
707
|
+
color: #0d6efd;
|
|
708
|
+
border-radius: 3px;
|
|
709
|
+
font-size: 0.7rem;
|
|
710
|
+
font-weight: 600;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
714
|
+
Status Badges
|
|
715
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
716
|
+
|
|
717
|
+
.status-badge {
|
|
718
|
+
display: inline-flex;
|
|
719
|
+
align-items: center;
|
|
720
|
+
gap: 4px;
|
|
721
|
+
padding: 2px 8px;
|
|
722
|
+
border-radius: 4px;
|
|
723
|
+
font-size: 0.75rem;
|
|
724
|
+
font-weight: 600;
|
|
725
|
+
text-transform: uppercase;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.status-badge.draft {
|
|
729
|
+
background: #cfe2ff;
|
|
730
|
+
color: #084298;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.status-badge.active {
|
|
734
|
+
background: #d1e7dd;
|
|
735
|
+
color: #0f5132;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.status-badge.deprecated {
|
|
739
|
+
background: #f8d7da;
|
|
740
|
+
color: #842029;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.status-badge.proposed {
|
|
744
|
+
background: #fff3cd;
|
|
745
|
+
color: #664d03;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.change-indicator {
|
|
749
|
+
color: #fd7e14;
|
|
750
|
+
font-size: 0.75rem;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
754
|
+
Coverage Indicators
|
|
755
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
756
|
+
|
|
757
|
+
.coverage-cell {
|
|
758
|
+
display: flex;
|
|
759
|
+
align-items: center;
|
|
760
|
+
justify-content: center;
|
|
761
|
+
gap: 4px;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.coverage-icon {
|
|
765
|
+
font-size: 1rem;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.coverage-icon.none { color: #adb5bd; }
|
|
769
|
+
.coverage-icon.partial { color: #6c757d; }
|
|
770
|
+
.coverage-icon.full { color: #212529; }
|
|
771
|
+
|
|
772
|
+
.warning-icon {
|
|
773
|
+
color: #ffc107;
|
|
774
|
+
font-size: 0.875rem;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
778
|
+
Topic Tags
|
|
779
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
780
|
+
|
|
781
|
+
.topic-tag {
|
|
782
|
+
font-size: 0.75rem;
|
|
783
|
+
color: #6c757d;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
787
|
+
Code Node Rows
|
|
788
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
789
|
+
|
|
790
|
+
.code-row {
|
|
791
|
+
background: #f8f9fa;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.code-row td {
|
|
795
|
+
font-size: 0.8rem;
|
|
796
|
+
color: #6c757d;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.code-icon {
|
|
800
|
+
color: #6c757d;
|
|
801
|
+
margin-right: 4px;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.test-row {
|
|
805
|
+
background: #f0fff4; /* Light green tint for tests */
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.test-row td {
|
|
809
|
+
font-size: 0.8rem;
|
|
810
|
+
color: #198754;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.test-icon {
|
|
814
|
+
margin-right: 4px;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* Test Result Rows */
|
|
818
|
+
.result-row {
|
|
819
|
+
background: #f8f9fa;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.result-row td {
|
|
823
|
+
font-size: 0.75rem;
|
|
824
|
+
color: #6c757d;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.result-row.passed {
|
|
828
|
+
background: #d1e7dd;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.result-row.passed td {
|
|
832
|
+
color: #0f5132;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.result-row.failed {
|
|
836
|
+
background: #f8d7da;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.result-row.failed td {
|
|
840
|
+
color: #842029;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.result-row.error {
|
|
844
|
+
background: #fff3cd;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.result-row.error td {
|
|
848
|
+
color: #664d03;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.result-row.skipped {
|
|
852
|
+
background: #e2e3e5;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.result-row.skipped td {
|
|
856
|
+
color: #41464b;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.result-icon {
|
|
860
|
+
margin-right: 4px;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
.result-badge {
|
|
864
|
+
display: inline-flex;
|
|
865
|
+
align-items: center;
|
|
866
|
+
gap: 4px;
|
|
867
|
+
padding: 2px 6px;
|
|
868
|
+
border-radius: 3px;
|
|
869
|
+
font-size: 0.7rem;
|
|
870
|
+
font-weight: 600;
|
|
871
|
+
text-transform: uppercase;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.result-badge.passed {
|
|
875
|
+
background: #d1e7dd;
|
|
876
|
+
color: #0f5132;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.result-badge.failed {
|
|
880
|
+
background: #f8d7da;
|
|
881
|
+
color: #842029;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.result-badge.error {
|
|
885
|
+
background: #fff3cd;
|
|
886
|
+
color: #664d03;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.result-badge.skipped {
|
|
890
|
+
background: #e2e3e5;
|
|
891
|
+
color: #41464b;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
895
|
+
Legend Modal
|
|
896
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
897
|
+
|
|
898
|
+
.modal-overlay {
|
|
899
|
+
position: fixed;
|
|
900
|
+
top: 0;
|
|
901
|
+
left: 0;
|
|
902
|
+
right: 0;
|
|
903
|
+
bottom: 0;
|
|
904
|
+
background: rgba(0, 0, 0, 0.5);
|
|
905
|
+
display: flex;
|
|
906
|
+
align-items: center;
|
|
907
|
+
justify-content: center;
|
|
908
|
+
z-index: 1000;
|
|
909
|
+
opacity: 0;
|
|
910
|
+
visibility: hidden;
|
|
911
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.modal-overlay.visible {
|
|
915
|
+
opacity: 1;
|
|
916
|
+
visibility: visible;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.modal {
|
|
920
|
+
background: white;
|
|
921
|
+
border-radius: 8px;
|
|
922
|
+
padding: 24px;
|
|
923
|
+
max-width: 500px;
|
|
924
|
+
width: 90%;
|
|
925
|
+
max-height: 80vh;
|
|
926
|
+
overflow-y: auto;
|
|
927
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.modal-header {
|
|
931
|
+
display: flex;
|
|
932
|
+
justify-content: space-between;
|
|
933
|
+
align-items: center;
|
|
934
|
+
margin-bottom: 16px;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.modal-title {
|
|
938
|
+
font-size: 1.25rem;
|
|
939
|
+
font-weight: 600;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.modal-close {
|
|
943
|
+
background: none;
|
|
944
|
+
border: none;
|
|
945
|
+
font-size: 1.5rem;
|
|
946
|
+
cursor: pointer;
|
|
947
|
+
color: #6c757d;
|
|
948
|
+
padding: 0;
|
|
949
|
+
line-height: 1;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.modal-close:hover {
|
|
953
|
+
color: #333;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.legend-section {
|
|
957
|
+
margin-bottom: 20px;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.legend-section h4 {
|
|
961
|
+
font-size: 0.875rem;
|
|
962
|
+
font-weight: 600;
|
|
963
|
+
color: #495057;
|
|
964
|
+
margin-bottom: 8px;
|
|
965
|
+
border-bottom: 1px solid #e9ecef;
|
|
966
|
+
padding-bottom: 4px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.legend-item {
|
|
970
|
+
display: flex;
|
|
971
|
+
align-items: center;
|
|
972
|
+
gap: 12px;
|
|
973
|
+
padding: 6px 0;
|
|
974
|
+
font-size: 0.875rem;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.legend-icon {
|
|
978
|
+
width: 24px;
|
|
979
|
+
text-align: center;
|
|
980
|
+
flex-shrink: 0;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
984
|
+
Responsive
|
|
985
|
+
───────────────────────────────────────────────────────────────────────────── */
|
|
986
|
+
|
|
987
|
+
@media (max-width: 768px) {
|
|
988
|
+
.header {
|
|
989
|
+
flex-direction: column;
|
|
990
|
+
align-items: flex-start;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.toolbar {
|
|
994
|
+
flex-direction: column;
|
|
995
|
+
align-items: flex-start;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.toolbar-actions {
|
|
999
|
+
margin-left: 0;
|
|
1000
|
+
width: 100%;
|
|
1001
|
+
justify-content: space-between;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
</style>
|
|
1006
|
+
</head>
|
|
1007
|
+
<body>
|
|
1008
|
+
|
|
1009
|
+
<!-- ═══════════════════════════════════════════════════════════════════════════
|
|
1010
|
+
HEADER
|
|
1011
|
+
═══════════════════════════════════════════════════════════════════════════ -->
|
|
1012
|
+
|
|
1013
|
+
<header class="header">
|
|
1014
|
+
<h1 class="header-title">Requirements Traceability</h1>
|
|
1015
|
+
|
|
1016
|
+
<div class="header-stats">
|
|
1017
|
+
<span class="stat-badge prd" id="badge-prd" onclick="toggleLevelFilter('PRD')" title="Click to filter PRD only">PRD: {{ stats.prd_count }}</span>
|
|
1018
|
+
<span class="stat-badge ops" id="badge-ops" onclick="toggleLevelFilter('OPS')" title="Click to filter OPS only">OPS: {{ stats.ops_count }}</span>
|
|
1019
|
+
<span class="stat-badge dev" id="badge-dev" onclick="toggleLevelFilter('DEV')" title="Click to filter DEV only">DEV: {{ stats.dev_count }}</span>
|
|
1020
|
+
<span style="color: #adb5bd;">|</span>
|
|
1021
|
+
<span class="stat-badge core" id="badge-core" onclick="toggleFilter('core')" title="Click to hide core (non-associated) requirements">Core: {{ stats.total_count - stats.associated_count }}</span>
|
|
1022
|
+
<span style="color: #adb5bd;">|</span>
|
|
1023
|
+
<span class="stat-badge code" id="badge-code" onclick="toggleFilter('code')" title="Click to hide code nodes">Code: {{ stats.code_count }}</span>
|
|
1024
|
+
<span class="stat-badge tests" id="badge-tests" onclick="toggleFilter('tests')" title="Click to hide test nodes">Tests: {{ stats.test_count }}</span>
|
|
1025
|
+
{% if stats.test_result_count > 0 %}
|
|
1026
|
+
<span class="stat-badge results" id="badge-results" onclick="toggleFilter('results')" title="Click to show test results">Results: {{ stats.test_passed_count }}✓ {{ stats.test_failed_count }}✗</span>
|
|
1027
|
+
{% endif %}
|
|
1028
|
+
<span class="stat-badge associated" id="badge-associated" onclick="toggleFilter('associated')" title="Click to filter associated repos">Assoc: {{ stats.associated_count|default(0) }}</span>
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
1031
|
+
<div class="assertion-summary">
|
|
1032
|
+
Assertions: <strong>{{ stats.assertion_count }}</strong> unique |
|
|
1033
|
+
Implemented: <strong>{{ stats.assertions_implemented }}</strong> |
|
|
1034
|
+
Tested: <strong>{{ stats.assertions_tested }}</strong> |
|
|
1035
|
+
Validated: <strong>{{ stats.assertions_validated }}</strong>
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
<div class="header-meta">
|
|
1039
|
+
<span class="version-badge">v{{ version }}</span>
|
|
1040
|
+
<button class="legend-btn" onclick="toggleLegend()">
|
|
1041
|
+
<span>ℹ</span> Legend
|
|
1042
|
+
</button>
|
|
1043
|
+
<button class="legend-btn" onclick="clearAllCookies()" title="Clear saved preferences">
|
|
1044
|
+
<span>🍪</span> Reset
|
|
1045
|
+
</button>
|
|
1046
|
+
</div>
|
|
1047
|
+
</header>
|
|
1048
|
+
|
|
1049
|
+
<!-- ═══════════════════════════════════════════════════════════════════════════
|
|
1050
|
+
TOOLBAR
|
|
1051
|
+
═══════════════════════════════════════════════════════════════════════════ -->
|
|
1052
|
+
|
|
1053
|
+
<div class="toolbar">
|
|
1054
|
+
<!-- View Mode -->
|
|
1055
|
+
<div class="btn-group" role="group">
|
|
1056
|
+
<button class="btn" data-view="flat" onclick="setViewMode('flat')">Flat View</button>
|
|
1057
|
+
<button class="btn active" data-view="hierarchical" onclick="setViewMode('hierarchical')">Hierarchical View</button>
|
|
1058
|
+
</div>
|
|
1059
|
+
|
|
1060
|
+
<!-- Git Filters -->
|
|
1061
|
+
<div class="btn-group" role="group">
|
|
1062
|
+
<button class="btn" id="btn-uncommitted" onclick="toggleFilter('uncommitted')">Uncommitted</button>
|
|
1063
|
+
<button class="btn" id="btn-changed" onclick="toggleFilter('changed')">Changed vs Main</button>
|
|
1064
|
+
</div>
|
|
1065
|
+
|
|
1066
|
+
<!-- Content Toggles -->
|
|
1067
|
+
<div class="toggle-group">
|
|
1068
|
+
<label class="toggle-item">
|
|
1069
|
+
<input type="checkbox" id="toggle-leaf" onchange="applyFilters()">
|
|
1070
|
+
<span>🌿 Leaf Only</span>
|
|
1071
|
+
</label>
|
|
1072
|
+
<label class="toggle-item">
|
|
1073
|
+
<input type="checkbox" id="toggle-deprecated" onchange="applyFilters()">
|
|
1074
|
+
<span>Include deprecated</span>
|
|
1075
|
+
</label>
|
|
1076
|
+
<label class="toggle-item">
|
|
1077
|
+
<input type="checkbox" id="toggle-roadmap" onchange="applyFilters()">
|
|
1078
|
+
<span>Include roadmap</span>
|
|
1079
|
+
</label>
|
|
1080
|
+
<label class="toggle-item">
|
|
1081
|
+
<input type="checkbox" id="toggle-code" onchange="applyFilters()">
|
|
1082
|
+
<span>Show code refs</span>
|
|
1083
|
+
</label>
|
|
1084
|
+
</div>
|
|
1085
|
+
|
|
1086
|
+
<!-- Actions -->
|
|
1087
|
+
<div class="toolbar-actions">
|
|
1088
|
+
<button class="btn btn-icon" onclick="expandAll()" title="Expand All">▼ Expand All</button>
|
|
1089
|
+
<button class="btn btn-icon" onclick="collapseAll()" title="Collapse All">▶ Collapse All</button>
|
|
1090
|
+
<button class="btn" onclick="clearFilters()">Clear</button>
|
|
1091
|
+
<span class="counter" id="counter">Showing {{ stats.total_count }} of {{ stats.total_count }} requirements</span>
|
|
1092
|
+
</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
|
|
1095
|
+
<!-- ═══════════════════════════════════════════════════════════════════════════
|
|
1096
|
+
TABS
|
|
1097
|
+
═══════════════════════════════════════════════════════════════════════════ -->
|
|
1098
|
+
|
|
1099
|
+
<div class="tabs">
|
|
1100
|
+
<button class="tab active" data-tab="requirements" onclick="switchTab('requirements')">Requirements</button>
|
|
1101
|
+
<button class="tab" data-tab="journeys" onclick="switchTab('journeys')">User Journeys</button>
|
|
1102
|
+
</div>
|
|
1103
|
+
|
|
1104
|
+
<!-- ═══════════════════════════════════════════════════════════════════════════
|
|
1105
|
+
MAIN CONTENT
|
|
1106
|
+
═══════════════════════════════════════════════════════════════════════════ -->
|
|
1107
|
+
|
|
1108
|
+
<main class="main-content">
|
|
1109
|
+
<!-- Requirements Tab Content -->
|
|
1110
|
+
<div class="tab-content active" id="content-requirements">
|
|
1111
|
+
<h2 class="section-title">Traceability Tree - <span id="view-mode-label">Hierarchical View</span></h2>
|
|
1112
|
+
|
|
1113
|
+
<div class="table-container">
|
|
1114
|
+
<table class="trace-table" id="trace-table">
|
|
1115
|
+
<thead>
|
|
1116
|
+
<tr>
|
|
1117
|
+
<th class="col-id">
|
|
1118
|
+
ID
|
|
1119
|
+
<input type="text" class="filter-input" id="filter-id" placeholder="e.g. p00001" oninput="applyFilters()">
|
|
1120
|
+
</th>
|
|
1121
|
+
<th class="col-title">
|
|
1122
|
+
TITLE
|
|
1123
|
+
<input type="text" class="filter-input" id="filter-title" placeholder="Search title..." oninput="applyFilters()">
|
|
1124
|
+
</th>
|
|
1125
|
+
<th class="col-level">
|
|
1126
|
+
LEVEL
|
|
1127
|
+
<select class="filter-select" id="filter-level" onchange="applyFilters()">
|
|
1128
|
+
<option value="">All</option>
|
|
1129
|
+
<option value="PRD">PRD</option>
|
|
1130
|
+
<option value="OPS">OPS</option>
|
|
1131
|
+
<option value="DEV">DEV</option>
|
|
1132
|
+
</select>
|
|
1133
|
+
</th>
|
|
1134
|
+
<th class="col-status">
|
|
1135
|
+
STATUS
|
|
1136
|
+
<select class="filter-select" id="filter-status" onchange="applyFilters()">
|
|
1137
|
+
<option value="">All</option>
|
|
1138
|
+
{% for status in statuses %}
|
|
1139
|
+
<option value="{{ status }}">{{ status }}</option>
|
|
1140
|
+
{% endfor %}
|
|
1141
|
+
</select>
|
|
1142
|
+
</th>
|
|
1143
|
+
<th class="col-cov">
|
|
1144
|
+
COV
|
|
1145
|
+
<select class="filter-select" id="filter-cov" onchange="applyFilters()">
|
|
1146
|
+
<option value="">All</option>
|
|
1147
|
+
<option value="none">○ None</option>
|
|
1148
|
+
<option value="partial">◐ Partial</option>
|
|
1149
|
+
<option value="full">● Full</option>
|
|
1150
|
+
</select>
|
|
1151
|
+
</th>
|
|
1152
|
+
<th class="col-topic">
|
|
1153
|
+
TOPIC
|
|
1154
|
+
<select class="filter-select" id="filter-topic" onchange="applyFilters()">
|
|
1155
|
+
<option value="">All</option>
|
|
1156
|
+
{% for topic in topics %}
|
|
1157
|
+
<option value="{{ topic }}">{{ topic }}</option>
|
|
1158
|
+
{% endfor %}
|
|
1159
|
+
</select>
|
|
1160
|
+
</th>
|
|
1161
|
+
</tr>
|
|
1162
|
+
</thead>
|
|
1163
|
+
<tbody id="table-body">
|
|
1164
|
+
{% for row in rows %}
|
|
1165
|
+
<tr class="req-row {{ 'code-row' if row.is_code else '' }}{{ 'test-row' if row.is_test else '' }}{{ 'result-row ' + row.result_status if row.is_test_result else '' }}"
|
|
1166
|
+
data-id="{{ row.id }}"
|
|
1167
|
+
data-parent="{{ row.parent_id or '' }}"
|
|
1168
|
+
data-level="{{ row.level }}"
|
|
1169
|
+
data-status="{{ row.status }}"
|
|
1170
|
+
data-coverage="{{ row.coverage }}"
|
|
1171
|
+
data-topic="{{ row.topic }}"
|
|
1172
|
+
data-depth="{{ row.depth }}"
|
|
1173
|
+
data-is-leaf="{{ 'true' if row.is_leaf else 'false' }}"
|
|
1174
|
+
data-is-changed="{{ 'true' if row.is_changed else 'false' }}"
|
|
1175
|
+
data-is-uncommitted="{{ 'true' if row.is_uncommitted else 'false' }}"
|
|
1176
|
+
data-is-roadmap="{{ 'true' if row.is_roadmap else 'false' }}"
|
|
1177
|
+
data-is-code="{{ 'true' if row.is_code else 'false' }}"
|
|
1178
|
+
data-is-test="{{ 'true' if row.is_test else 'false' }}"
|
|
1179
|
+
data-is-test-result="{{ 'true' if row.is_test_result else 'false' }}"
|
|
1180
|
+
data-result-status="{{ row.result_status }}"
|
|
1181
|
+
data-is-associated="{{ 'true' if row.is_associated else 'false' }}"
|
|
1182
|
+
data-has-children="{{ 'true' if row.has_children else 'false' }}"
|
|
1183
|
+
data-source-file="{{ row.source_file }}"
|
|
1184
|
+
data-source-line="{{ row.source_line }}">
|
|
1185
|
+
<td class="col-id">
|
|
1186
|
+
<div class="tree-cell">
|
|
1187
|
+
{% for i in range(row.depth) %}
|
|
1188
|
+
<span class="tree-indent"></span>
|
|
1189
|
+
{% endfor %}
|
|
1190
|
+
<button class="tree-toggle {{ 'expanded' if row.has_children else 'leaf' }}"
|
|
1191
|
+
onclick="toggleNode('{{ row.id }}')"
|
|
1192
|
+
data-node-id="{{ row.id }}"></button>
|
|
1193
|
+
{% if row.is_code %}
|
|
1194
|
+
<span class="code-icon">📄</span>
|
|
1195
|
+
{% elif row.is_test %}
|
|
1196
|
+
<span class="test-icon">🧪</span>
|
|
1197
|
+
{% elif row.is_test_result %}
|
|
1198
|
+
<span class="result-icon">{% if row.result_status == 'passed' %}✅{% elif row.result_status in ['failed', 'failure'] %}❌{% elif row.result_status == 'error' %}⚠️{% elif row.result_status == 'skipped' %}⏭️{% else %}📋{% endif %}</span>
|
|
1199
|
+
{% endif %}
|
|
1200
|
+
{% if row.source_file %}
|
|
1201
|
+
<a href="vscode://file/{{ row.source_file }}:{{ row.source_line }}:1" class="req-id" title="Open in VS Code: {{ row.source_file }}:{{ row.source_line }}">{{ row.display_id | replace('REQ-', '') }}</a>
|
|
1202
|
+
{% else %}
|
|
1203
|
+
<span class="req-id">{{ row.display_id | replace('REQ-', '') }}</span>
|
|
1204
|
+
{% endif %}
|
|
1205
|
+
{% if row.assertions %}
|
|
1206
|
+
<span class="assertion-badges">
|
|
1207
|
+
{% for a in row.assertions %}
|
|
1208
|
+
<span class="assertion-badge">{{ a }}</span>
|
|
1209
|
+
{% endfor %}
|
|
1210
|
+
</span>
|
|
1211
|
+
{% endif %}
|
|
1212
|
+
</div>
|
|
1213
|
+
</td>
|
|
1214
|
+
<td class="col-title">
|
|
1215
|
+
{% if row.is_test_result %}
|
|
1216
|
+
{% if row.source_file %}
|
|
1217
|
+
<a href="vscode://file/{{ row.source_file }}:{{ row.source_line }}:1" class="file-link" title="Open in VS Code">{{ row.title }}</a>
|
|
1218
|
+
{% else %}
|
|
1219
|
+
{{ row.title }}
|
|
1220
|
+
{% endif %}
|
|
1221
|
+
<span class="result-badge {{ row.result_status }}">{{ row.result_status }}</span>
|
|
1222
|
+
{% elif (row.is_test or row.is_code) and row.source_file %}
|
|
1223
|
+
<a href="vscode://file/{{ row.source_file }}:{{ row.source_line }}:1" class="file-link" title="Open in VS Code">{{ row.title }}</a>
|
|
1224
|
+
{% else %}
|
|
1225
|
+
{{ row.title }}
|
|
1226
|
+
{% endif %}
|
|
1227
|
+
</td>
|
|
1228
|
+
<td class="col-level">{{ row.level }}</td>
|
|
1229
|
+
<td class="col-status">
|
|
1230
|
+
{% if row.status %}
|
|
1231
|
+
<span class="status-badge {{ row.status|lower }}">
|
|
1232
|
+
{{ row.status }}
|
|
1233
|
+
{% if row.is_changed %}<span class="change-indicator">◆</span>{% endif %}
|
|
1234
|
+
</span>
|
|
1235
|
+
{% endif %}
|
|
1236
|
+
</td>
|
|
1237
|
+
<td class="col-cov">
|
|
1238
|
+
<div class="coverage-cell">
|
|
1239
|
+
{% if row.coverage == 'full' %}
|
|
1240
|
+
<span class="coverage-icon full">●</span>
|
|
1241
|
+
{% elif row.coverage == 'partial' %}
|
|
1242
|
+
<span class="coverage-icon partial">◐</span>
|
|
1243
|
+
{% else %}
|
|
1244
|
+
<span class="coverage-icon none">○</span>
|
|
1245
|
+
{% endif %}
|
|
1246
|
+
{% if row.has_failures %}
|
|
1247
|
+
<span class="warning-icon">⚡</span>
|
|
1248
|
+
{% endif %}
|
|
1249
|
+
</div>
|
|
1250
|
+
</td>
|
|
1251
|
+
<td class="col-topic">
|
|
1252
|
+
<span class="topic-tag">{{ row.topic }}</span>
|
|
1253
|
+
</td>
|
|
1254
|
+
</tr>
|
|
1255
|
+
{% endfor %}
|
|
1256
|
+
</tbody>
|
|
1257
|
+
</table>
|
|
1258
|
+
</div>
|
|
1259
|
+
</div>
|
|
1260
|
+
|
|
1261
|
+
<!-- User Journeys Tab Content -->
|
|
1262
|
+
<div class="tab-content" id="content-journeys">
|
|
1263
|
+
<h2 class="section-title">User Journeys</h2>
|
|
1264
|
+
{% if journeys %}
|
|
1265
|
+
<div class="journey-toolbar">
|
|
1266
|
+
<div class="journey-search">
|
|
1267
|
+
<input type="text" id="journey-search" placeholder="Search journeys by ID, title, actor, or goal..." oninput="filterJourneys()">
|
|
1268
|
+
</div>
|
|
1269
|
+
<div class="journey-group-controls">
|
|
1270
|
+
<label for="journey-group-by">Group by:</label>
|
|
1271
|
+
<select id="journey-group-by" onchange="regroupJourneys()">
|
|
1272
|
+
<option value="none">None</option>
|
|
1273
|
+
<option value="descriptor">Descriptor</option>
|
|
1274
|
+
<option value="actor">Actor</option>
|
|
1275
|
+
<option value="file">File</option>
|
|
1276
|
+
</select>
|
|
1277
|
+
</div>
|
|
1278
|
+
<div class="journey-count">
|
|
1279
|
+
<span id="journey-visible-count">{{ journeys|length }}</span> of {{ journeys|length }} journeys
|
|
1280
|
+
</div>
|
|
1281
|
+
</div>
|
|
1282
|
+
<div class="journey-list" id="journey-list">
|
|
1283
|
+
{% for journey in journeys %}
|
|
1284
|
+
<div class="journey-card"
|
|
1285
|
+
data-id="{{ journey.id|lower }}"
|
|
1286
|
+
data-title="{{ journey.title|lower }}"
|
|
1287
|
+
data-actor="{{ (journey.actor or '')|lower }}"
|
|
1288
|
+
data-goal="{{ (journey.goal or '')|lower }}"
|
|
1289
|
+
data-descriptor="{{ journey.descriptor|lower }}"
|
|
1290
|
+
data-file="{{ journey.file|lower }}">
|
|
1291
|
+
<div class="journey-card-header">
|
|
1292
|
+
<span class="journey-id">{{ journey.id }}</span>
|
|
1293
|
+
<h3>{{ journey.title }}</h3>
|
|
1294
|
+
</div>
|
|
1295
|
+
{% if journey.actor or journey.goal or journey.descriptor or journey.file %}
|
|
1296
|
+
<div class="journey-meta">
|
|
1297
|
+
{% if journey.descriptor %}
|
|
1298
|
+
<div class="journey-meta-item">
|
|
1299
|
+
<span class="journey-meta-label">Topic</span>
|
|
1300
|
+
<span class="journey-meta-value">{{ journey.descriptor }}</span>
|
|
1301
|
+
</div>
|
|
1302
|
+
{% endif %}
|
|
1303
|
+
{% if journey.actor %}
|
|
1304
|
+
<div class="journey-meta-item">
|
|
1305
|
+
<span class="journey-meta-label">Actor</span>
|
|
1306
|
+
<span class="journey-meta-value">{{ journey.actor }}</span>
|
|
1307
|
+
</div>
|
|
1308
|
+
{% endif %}
|
|
1309
|
+
{% if journey.goal %}
|
|
1310
|
+
<div class="journey-meta-item">
|
|
1311
|
+
<span class="journey-meta-label">Goal</span>
|
|
1312
|
+
<span class="journey-meta-value">{{ journey.goal }}</span>
|
|
1313
|
+
</div>
|
|
1314
|
+
{% endif %}
|
|
1315
|
+
{% if journey.file %}
|
|
1316
|
+
<div class="journey-meta-item">
|
|
1317
|
+
<span class="journey-meta-label">Source</span>
|
|
1318
|
+
<span class="journey-meta-value">{{ journey.file }}</span>
|
|
1319
|
+
</div>
|
|
1320
|
+
{% endif %}
|
|
1321
|
+
</div>
|
|
1322
|
+
{% endif %}
|
|
1323
|
+
{% if journey.description %}
|
|
1324
|
+
<div class="journey-description">{{ journey.description }}</div>
|
|
1325
|
+
{% endif %}
|
|
1326
|
+
</div>
|
|
1327
|
+
{% endfor %}
|
|
1328
|
+
</div>
|
|
1329
|
+
{% else %}
|
|
1330
|
+
<div class="no-journeys">
|
|
1331
|
+
<p>No user journeys found in specification files.</p>
|
|
1332
|
+
<p style="font-size: 0.875rem; margin-top: 8px;">User journeys are defined with <code>## JNY-xxx: Title</code> headers in spec files.</p>
|
|
1333
|
+
<p style="font-size: 0.8rem; margin-top: 12px; color: #868e96;">
|
|
1334
|
+
Example format:<br>
|
|
1335
|
+
<code style="display: block; margin-top: 8px; padding: 12px; background: #f1f3f5; border-radius: 4px; text-align: left;">
|
|
1336
|
+
## JNY-LOGIN-01: User Login<br>
|
|
1337
|
+
**Actor**: End User<br>
|
|
1338
|
+
**Goal**: Access the system securely<br><br>
|
|
1339
|
+
Steps...<br><br>
|
|
1340
|
+
*End* *JNY-LOGIN-01*
|
|
1341
|
+
</code>
|
|
1342
|
+
</p>
|
|
1343
|
+
</div>
|
|
1344
|
+
{% endif %}
|
|
1345
|
+
</div>
|
|
1346
|
+
</main>
|
|
1347
|
+
|
|
1348
|
+
<!-- ═══════════════════════════════════════════════════════════════════════════
|
|
1349
|
+
LEGEND MODAL
|
|
1350
|
+
═══════════════════════════════════════════════════════════════════════════ -->
|
|
1351
|
+
|
|
1352
|
+
<div class="modal-overlay" id="legend-modal" onclick="closeLegendOnOverlay(event)">
|
|
1353
|
+
<div class="modal" onclick="event.stopPropagation()">
|
|
1354
|
+
<div class="modal-header">
|
|
1355
|
+
<h3 class="modal-title">Legend</h3>
|
|
1356
|
+
<button class="modal-close" onclick="toggleLegend()">×</button>
|
|
1357
|
+
</div>
|
|
1358
|
+
|
|
1359
|
+
<div class="legend-section">
|
|
1360
|
+
<h4>Coverage Status</h4>
|
|
1361
|
+
<div class="legend-item">
|
|
1362
|
+
<span class="legend-icon">○</span>
|
|
1363
|
+
<span>No implementations - no code references this requirement</span>
|
|
1364
|
+
</div>
|
|
1365
|
+
<div class="legend-item">
|
|
1366
|
+
<span class="legend-icon">◐</span>
|
|
1367
|
+
<span>Partial - some assertions have code implementations</span>
|
|
1368
|
+
</div>
|
|
1369
|
+
<div class="legend-item">
|
|
1370
|
+
<span class="legend-icon">●</span>
|
|
1371
|
+
<span>Full - all assertions have code implementations</span>
|
|
1372
|
+
</div>
|
|
1373
|
+
<div class="legend-item">
|
|
1374
|
+
<span class="legend-icon" style="color: #ffc107;">⚡</span>
|
|
1375
|
+
<span>Test failures or warnings detected</span>
|
|
1376
|
+
</div>
|
|
1377
|
+
</div>
|
|
1378
|
+
|
|
1379
|
+
<div class="legend-section">
|
|
1380
|
+
<h4>Change Indicators</h4>
|
|
1381
|
+
<div class="legend-item">
|
|
1382
|
+
<span class="legend-icon" style="color: #fd7e14;">◆</span>
|
|
1383
|
+
<span>Changed vs remote main branch</span>
|
|
1384
|
+
</div>
|
|
1385
|
+
</div>
|
|
1386
|
+
|
|
1387
|
+
<div class="legend-section">
|
|
1388
|
+
<h4>Assertion Badges</h4>
|
|
1389
|
+
<div class="legend-item">
|
|
1390
|
+
<span class="legend-icon"><span class="assertion-badge">A</span></span>
|
|
1391
|
+
<span>Implements specific assertion(s) from parent requirement</span>
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>
|
|
1394
|
+
|
|
1395
|
+
<div class="legend-section">
|
|
1396
|
+
<h4>Status Badges</h4>
|
|
1397
|
+
<div class="legend-item">
|
|
1398
|
+
<span class="status-badge draft">DRAFT</span>
|
|
1399
|
+
<span>Work in progress</span>
|
|
1400
|
+
</div>
|
|
1401
|
+
<div class="legend-item">
|
|
1402
|
+
<span class="status-badge active">ACTIVE</span>
|
|
1403
|
+
<span>Approved and active</span>
|
|
1404
|
+
</div>
|
|
1405
|
+
<div class="legend-item">
|
|
1406
|
+
<span class="status-badge deprecated">DEPRECATED</span>
|
|
1407
|
+
<span>No longer applicable</span>
|
|
1408
|
+
</div>
|
|
1409
|
+
</div>
|
|
1410
|
+
|
|
1411
|
+
<div class="legend-section">
|
|
1412
|
+
<h4>Levels</h4>
|
|
1413
|
+
<div class="legend-item">
|
|
1414
|
+
<span class="stat-badge prd" style="font-size: 0.75rem;">PRD</span>
|
|
1415
|
+
<span>Product Requirements - business-level needs</span>
|
|
1416
|
+
</div>
|
|
1417
|
+
<div class="legend-item">
|
|
1418
|
+
<span class="stat-badge ops" style="font-size: 0.75rem;">OPS</span>
|
|
1419
|
+
<span>Operations Requirements - operational constraints</span>
|
|
1420
|
+
</div>
|
|
1421
|
+
<div class="legend-item">
|
|
1422
|
+
<span class="stat-badge dev" style="font-size: 0.75rem;">DEV</span>
|
|
1423
|
+
<span>Development Requirements - technical specifications</span>
|
|
1424
|
+
</div>
|
|
1425
|
+
</div>
|
|
1426
|
+
</div>
|
|
1427
|
+
</div>
|
|
1428
|
+
|
|
1429
|
+
<!-- ═══════════════════════════════════════════════════════════════════════════
|
|
1430
|
+
EMBEDDED DATA
|
|
1431
|
+
═══════════════════════════════════════════════════════════════════════════ -->
|
|
1432
|
+
|
|
1433
|
+
<script type="application/json" id="tree-data">
|
|
1434
|
+
{{ tree_data | tojson }}
|
|
1435
|
+
</script>
|
|
1436
|
+
|
|
1437
|
+
<!-- ═══════════════════════════════════════════════════════════════════════════
|
|
1438
|
+
JAVASCRIPT
|
|
1439
|
+
═══════════════════════════════════════════════════════════════════════════ -->
|
|
1440
|
+
|
|
1441
|
+
<script>
|
|
1442
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1443
|
+
// State
|
|
1444
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1445
|
+
|
|
1446
|
+
const state = {
|
|
1447
|
+
viewMode: 'hierarchical',
|
|
1448
|
+
activeFilters: {
|
|
1449
|
+
uncommitted: false,
|
|
1450
|
+
changed: false,
|
|
1451
|
+
code: false,
|
|
1452
|
+
tests: false,
|
|
1453
|
+
results: false,
|
|
1454
|
+
associated: false,
|
|
1455
|
+
core: false,
|
|
1456
|
+
levelPrd: false,
|
|
1457
|
+
levelOps: false,
|
|
1458
|
+
levelDev: false
|
|
1459
|
+
},
|
|
1460
|
+
collapsedNodes: new Set(),
|
|
1461
|
+
totalCount: {{ stats.total_count }},
|
|
1462
|
+
activeTab: 'requirements',
|
|
1463
|
+
treeData: JSON.parse(document.getElementById('tree-data').textContent)
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1467
|
+
// Cookie Persistence
|
|
1468
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1469
|
+
|
|
1470
|
+
const COOKIE_NAME = 'elspais_trace_view_state';
|
|
1471
|
+
const COOKIE_DAYS = 30;
|
|
1472
|
+
|
|
1473
|
+
function saveStateToCookie() {
|
|
1474
|
+
const journeyGroupBy = document.getElementById('journey-group-by');
|
|
1475
|
+
const journeySearch = document.getElementById('journey-search');
|
|
1476
|
+
|
|
1477
|
+
const stateToSave = {
|
|
1478
|
+
viewMode: state.viewMode,
|
|
1479
|
+
activeFilters: state.activeFilters,
|
|
1480
|
+
collapsedNodes: Array.from(state.collapsedNodes), // Convert Set to Array for JSON
|
|
1481
|
+
toggleLeaf: document.getElementById('toggle-leaf').checked,
|
|
1482
|
+
toggleDeprecated: document.getElementById('toggle-deprecated').checked,
|
|
1483
|
+
toggleRoadmap: document.getElementById('toggle-roadmap').checked,
|
|
1484
|
+
toggleCode: document.getElementById('toggle-code').checked,
|
|
1485
|
+
filterLevel: document.getElementById('filter-level').value,
|
|
1486
|
+
filterStatus: document.getElementById('filter-status').value,
|
|
1487
|
+
filterCov: document.getElementById('filter-cov').value,
|
|
1488
|
+
filterTopic: document.getElementById('filter-topic').value,
|
|
1489
|
+
activeTab: state.activeTab,
|
|
1490
|
+
// Journey state
|
|
1491
|
+
journeyGroupBy: journeyGroupBy ? journeyGroupBy.value : 'none',
|
|
1492
|
+
journeySearch: journeySearch ? journeySearch.value : '',
|
|
1493
|
+
collapsedJourneyGroups: Array.from(collapsedJourneyGroups)
|
|
1494
|
+
};
|
|
1495
|
+
const expires = new Date(Date.now() + COOKIE_DAYS * 864e5).toUTCString();
|
|
1496
|
+
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(stateToSave))}; expires=${expires}; path=/; SameSite=Lax`;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function loadStateFromCookie() {
|
|
1500
|
+
const match = document.cookie.match(new RegExp('(^| )' + COOKIE_NAME + '=([^;]+)'));
|
|
1501
|
+
if (!match) return null;
|
|
1502
|
+
try {
|
|
1503
|
+
return JSON.parse(decodeURIComponent(match[2]));
|
|
1504
|
+
} catch {
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function restoreState() {
|
|
1510
|
+
const saved = loadStateFromCookie();
|
|
1511
|
+
if (!saved) return;
|
|
1512
|
+
|
|
1513
|
+
// Restore view mode
|
|
1514
|
+
if (saved.viewMode) {
|
|
1515
|
+
state.viewMode = saved.viewMode;
|
|
1516
|
+
document.querySelectorAll('[data-view]').forEach(btn => {
|
|
1517
|
+
btn.classList.toggle('active', btn.dataset.view === saved.viewMode);
|
|
1518
|
+
});
|
|
1519
|
+
document.getElementById('view-mode-label').textContent =
|
|
1520
|
+
saved.viewMode === 'flat' ? 'Flat View' : 'Hierarchical View';
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Restore active filters
|
|
1524
|
+
if (saved.activeFilters) {
|
|
1525
|
+
Object.assign(state.activeFilters, saved.activeFilters);
|
|
1526
|
+
Object.keys(state.activeFilters).forEach(key => {
|
|
1527
|
+
const btn = document.getElementById('btn-' + key);
|
|
1528
|
+
if (btn) btn.classList.toggle('active', state.activeFilters[key]);
|
|
1529
|
+
});
|
|
1530
|
+
// Update level badges
|
|
1531
|
+
document.getElementById('badge-prd')?.classList.toggle('active', state.activeFilters.levelPrd);
|
|
1532
|
+
document.getElementById('badge-ops')?.classList.toggle('active', state.activeFilters.levelOps);
|
|
1533
|
+
document.getElementById('badge-dev')?.classList.toggle('active', state.activeFilters.levelDev);
|
|
1534
|
+
document.getElementById('badge-code')?.classList.toggle('active', state.activeFilters.code);
|
|
1535
|
+
document.getElementById('badge-tests')?.classList.toggle('active', state.activeFilters.tests);
|
|
1536
|
+
document.getElementById('badge-results')?.classList.toggle('active', state.activeFilters.results);
|
|
1537
|
+
document.getElementById('badge-associated')?.classList.toggle('active', state.activeFilters.associated);
|
|
1538
|
+
document.getElementById('badge-core')?.classList.toggle('active', state.activeFilters.core);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Restore checkboxes
|
|
1542
|
+
if (saved.toggleLeaf !== undefined) document.getElementById('toggle-leaf').checked = saved.toggleLeaf;
|
|
1543
|
+
if (saved.toggleDeprecated !== undefined) document.getElementById('toggle-deprecated').checked = saved.toggleDeprecated;
|
|
1544
|
+
if (saved.toggleRoadmap !== undefined) document.getElementById('toggle-roadmap').checked = saved.toggleRoadmap;
|
|
1545
|
+
if (saved.toggleCode !== undefined) document.getElementById('toggle-code').checked = saved.toggleCode;
|
|
1546
|
+
|
|
1547
|
+
// Restore dropdowns
|
|
1548
|
+
if (saved.filterLevel) document.getElementById('filter-level').value = saved.filterLevel;
|
|
1549
|
+
if (saved.filterStatus) document.getElementById('filter-status').value = saved.filterStatus;
|
|
1550
|
+
if (saved.filterCov) document.getElementById('filter-cov').value = saved.filterCov;
|
|
1551
|
+
if (saved.filterTopic) document.getElementById('filter-topic').value = saved.filterTopic;
|
|
1552
|
+
|
|
1553
|
+
// Restore tab
|
|
1554
|
+
if (saved.activeTab) {
|
|
1555
|
+
state.activeTab = saved.activeTab;
|
|
1556
|
+
switchTab(saved.activeTab, false);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Restore collapsed nodes (tree expand/collapse state)
|
|
1560
|
+
if (saved.collapsedNodes && Array.isArray(saved.collapsedNodes)) {
|
|
1561
|
+
state.collapsedNodes = new Set(saved.collapsedNodes);
|
|
1562
|
+
// Update toggle button visuals
|
|
1563
|
+
document.querySelectorAll('.tree-toggle').forEach(toggle => {
|
|
1564
|
+
const nodeId = toggle.dataset.nodeId;
|
|
1565
|
+
if (state.collapsedNodes.has(nodeId)) {
|
|
1566
|
+
toggle.classList.remove('expanded');
|
|
1567
|
+
toggle.classList.add('collapsed');
|
|
1568
|
+
} else if (!toggle.classList.contains('leaf')) {
|
|
1569
|
+
toggle.classList.remove('collapsed');
|
|
1570
|
+
toggle.classList.add('expanded');
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Restore journey state
|
|
1576
|
+
if (saved.collapsedJourneyGroups && Array.isArray(saved.collapsedJourneyGroups)) {
|
|
1577
|
+
collapsedJourneyGroups = new Set(saved.collapsedJourneyGroups);
|
|
1578
|
+
}
|
|
1579
|
+
if (saved.journeySearch) {
|
|
1580
|
+
const journeySearch = document.getElementById('journey-search');
|
|
1581
|
+
if (journeySearch) journeySearch.value = saved.journeySearch;
|
|
1582
|
+
}
|
|
1583
|
+
if (saved.journeyGroupBy) {
|
|
1584
|
+
const journeyGroupBy = document.getElementById('journey-group-by');
|
|
1585
|
+
if (journeyGroupBy) {
|
|
1586
|
+
journeyGroupBy.value = saved.journeyGroupBy;
|
|
1587
|
+
// Apply grouping after DOM is ready
|
|
1588
|
+
setTimeout(() => regroupJourneys(), 0);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function clearAllCookies() {
|
|
1594
|
+
document.cookie = `${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
1595
|
+
// Reset to defaults
|
|
1596
|
+
state.viewMode = 'hierarchical';
|
|
1597
|
+
state.activeFilters = {
|
|
1598
|
+
uncommitted: false, changed: false, code: false, tests: false, results: false, associated: false, core: false,
|
|
1599
|
+
levelPrd: false, levelOps: false, levelDev: false
|
|
1600
|
+
};
|
|
1601
|
+
state.collapsedNodes.clear();
|
|
1602
|
+
state.activeTab = 'requirements';
|
|
1603
|
+
|
|
1604
|
+
// Reset UI
|
|
1605
|
+
document.querySelectorAll('[data-view]').forEach(btn => {
|
|
1606
|
+
btn.classList.toggle('active', btn.dataset.view === 'hierarchical');
|
|
1607
|
+
});
|
|
1608
|
+
document.querySelectorAll('.stat-badge').forEach(badge => badge.classList.remove('active'));
|
|
1609
|
+
document.querySelectorAll('.btn-group .btn').forEach(btn => {
|
|
1610
|
+
if (!btn.dataset.view) btn.classList.remove('active');
|
|
1611
|
+
});
|
|
1612
|
+
document.getElementById('toggle-leaf').checked = false;
|
|
1613
|
+
document.getElementById('toggle-deprecated').checked = false;
|
|
1614
|
+
document.getElementById('toggle-roadmap').checked = false;
|
|
1615
|
+
document.getElementById('toggle-code').checked = false;
|
|
1616
|
+
document.getElementById('filter-id').value = '';
|
|
1617
|
+
document.getElementById('filter-title').value = '';
|
|
1618
|
+
document.getElementById('filter-level').value = '';
|
|
1619
|
+
document.getElementById('filter-status').value = '';
|
|
1620
|
+
document.getElementById('filter-cov').value = '';
|
|
1621
|
+
document.getElementById('filter-topic').value = '';
|
|
1622
|
+
|
|
1623
|
+
switchTab('requirements', false);
|
|
1624
|
+
collapseAll();
|
|
1625
|
+
alert('Preferences cleared! Page reset to defaults.');
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1629
|
+
// View Mode
|
|
1630
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1631
|
+
|
|
1632
|
+
function setViewMode(mode) {
|
|
1633
|
+
state.viewMode = mode;
|
|
1634
|
+
|
|
1635
|
+
// Update button states
|
|
1636
|
+
document.querySelectorAll('[data-view]').forEach(btn => {
|
|
1637
|
+
btn.classList.toggle('active', btn.dataset.view === mode);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// Update label
|
|
1641
|
+
document.getElementById('view-mode-label').textContent =
|
|
1642
|
+
mode === 'flat' ? 'Flat View' : 'Hierarchical View';
|
|
1643
|
+
|
|
1644
|
+
applyFilters();
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1648
|
+
// Filter Toggles
|
|
1649
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1650
|
+
|
|
1651
|
+
function toggleFilter(filter) {
|
|
1652
|
+
state.activeFilters[filter] = !state.activeFilters[filter];
|
|
1653
|
+
|
|
1654
|
+
const btn = document.getElementById('btn-' + filter);
|
|
1655
|
+
if (btn) btn.classList.toggle('active', state.activeFilters[filter]);
|
|
1656
|
+
|
|
1657
|
+
// Also update badges for files/associated
|
|
1658
|
+
const badge = document.getElementById('badge-' + filter);
|
|
1659
|
+
if (badge) badge.classList.toggle('active', state.activeFilters[filter]);
|
|
1660
|
+
|
|
1661
|
+
applyFilters();
|
|
1662
|
+
saveStateToCookie();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function toggleLevelFilter(level) {
|
|
1666
|
+
const key = 'level' + level.charAt(0).toUpperCase() + level.slice(1).toLowerCase();
|
|
1667
|
+
state.activeFilters[key] = !state.activeFilters[key];
|
|
1668
|
+
|
|
1669
|
+
// Update badge visual
|
|
1670
|
+
const badge = document.getElementById('badge-' + level.toLowerCase());
|
|
1671
|
+
if (badge) badge.classList.toggle('active', state.activeFilters[key]);
|
|
1672
|
+
|
|
1673
|
+
// Clear dropdown when badge filters are used (badges hide, dropdown shows - they conflict)
|
|
1674
|
+
const anyBadgeActive = state.activeFilters.levelPrd || state.activeFilters.levelOps || state.activeFilters.levelDev;
|
|
1675
|
+
if (anyBadgeActive) {
|
|
1676
|
+
document.getElementById('filter-level').value = '';
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
applyFilters();
|
|
1680
|
+
saveStateToCookie();
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function clearFilters() {
|
|
1684
|
+
// Reset all filters
|
|
1685
|
+
state.activeFilters = {
|
|
1686
|
+
uncommitted: false, changed: false, code: false, tests: false, results: false, associated: false, core: false,
|
|
1687
|
+
levelPrd: false, levelOps: false, levelDev: false
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
// Remove active class from buttons and badges
|
|
1691
|
+
document.querySelectorAll('.btn-group .btn').forEach(btn => {
|
|
1692
|
+
if (!btn.dataset.view) btn.classList.remove('active');
|
|
1693
|
+
});
|
|
1694
|
+
document.querySelectorAll('.stat-badge').forEach(badge => badge.classList.remove('active'));
|
|
1695
|
+
|
|
1696
|
+
document.getElementById('filter-id').value = '';
|
|
1697
|
+
document.getElementById('filter-title').value = '';
|
|
1698
|
+
document.getElementById('filter-level').value = '';
|
|
1699
|
+
document.getElementById('filter-status').value = '';
|
|
1700
|
+
document.getElementById('filter-cov').value = '';
|
|
1701
|
+
document.getElementById('filter-topic').value = '';
|
|
1702
|
+
|
|
1703
|
+
document.getElementById('toggle-leaf').checked = false;
|
|
1704
|
+
document.getElementById('toggle-deprecated').checked = false;
|
|
1705
|
+
document.getElementById('toggle-roadmap').checked = false;
|
|
1706
|
+
document.getElementById('toggle-code').checked = false;
|
|
1707
|
+
|
|
1708
|
+
applyFilters();
|
|
1709
|
+
saveStateToCookie();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1713
|
+
// Apply Filters
|
|
1714
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1715
|
+
|
|
1716
|
+
function applyFilters() {
|
|
1717
|
+
const rows = document.querySelectorAll('#table-body .req-row');
|
|
1718
|
+
const filterIdValue = document.getElementById('filter-id').value.toLowerCase();
|
|
1719
|
+
const filterTitleValue = document.getElementById('filter-title').value.toLowerCase();
|
|
1720
|
+
const filterLevelValue = document.getElementById('filter-level').value;
|
|
1721
|
+
const filterStatusValue = document.getElementById('filter-status').value;
|
|
1722
|
+
const filterCovValue = document.getElementById('filter-cov').value;
|
|
1723
|
+
const filterTopicValue = document.getElementById('filter-topic').value;
|
|
1724
|
+
|
|
1725
|
+
const leafOnly = document.getElementById('toggle-leaf').checked;
|
|
1726
|
+
const includeDeprecated = document.getElementById('toggle-deprecated').checked;
|
|
1727
|
+
const includeRoadmap = document.getElementById('toggle-roadmap').checked;
|
|
1728
|
+
const showCode = document.getElementById('toggle-code').checked;
|
|
1729
|
+
|
|
1730
|
+
// Check if any level badge filters are active
|
|
1731
|
+
const levelBadgeFiltersActive = state.activeFilters.levelPrd ||
|
|
1732
|
+
state.activeFilters.levelOps ||
|
|
1733
|
+
state.activeFilters.levelDev;
|
|
1734
|
+
|
|
1735
|
+
let visibleCount = 0;
|
|
1736
|
+
|
|
1737
|
+
rows.forEach(row => {
|
|
1738
|
+
const id = row.dataset.id.toLowerCase();
|
|
1739
|
+
const level = row.dataset.level;
|
|
1740
|
+
const status = row.dataset.status;
|
|
1741
|
+
const coverage = row.dataset.coverage;
|
|
1742
|
+
const topic = row.dataset.topic;
|
|
1743
|
+
const title = row.querySelector('.col-title')?.textContent.toLowerCase() || '';
|
|
1744
|
+
const isLeaf = row.dataset.isLeaf === 'true';
|
|
1745
|
+
const isChanged = row.dataset.isChanged === 'true';
|
|
1746
|
+
const isUncommitted = row.dataset.isUncommitted === 'true';
|
|
1747
|
+
const isRoadmap = row.dataset.isRoadmap === 'true';
|
|
1748
|
+
const isCode = row.dataset.isCode === 'true';
|
|
1749
|
+
const isTest = row.dataset.isTest === 'true';
|
|
1750
|
+
const isTestResult = row.dataset.isTestResult === 'true';
|
|
1751
|
+
const isAssociated = row.dataset.isAssociated === 'true';
|
|
1752
|
+
const depth = parseInt(row.dataset.depth);
|
|
1753
|
+
const parentId = row.dataset.parent;
|
|
1754
|
+
const isImplNode = isCode || isTest || isTestResult; // Implementation/evidence nodes
|
|
1755
|
+
|
|
1756
|
+
let visible = true;
|
|
1757
|
+
|
|
1758
|
+
// Code badge filter - HIDE logic: code visible by default, hide when badge is active
|
|
1759
|
+
if (isCode && state.activeFilters.code) {
|
|
1760
|
+
visible = false;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// Tests badge filter - HIDE logic: tests visible by default, hide when badge is active
|
|
1764
|
+
if (isTest && state.activeFilters.tests) {
|
|
1765
|
+
visible = false;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Results badge filter - SHOW logic: results hidden by default, show when badge is active
|
|
1769
|
+
if (isTestResult && !state.activeFilters.results) {
|
|
1770
|
+
visible = false;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Text filters
|
|
1774
|
+
if (filterIdValue && !id.includes(filterIdValue)) visible = false;
|
|
1775
|
+
if (filterTitleValue && !title.includes(filterTitleValue)) visible = false;
|
|
1776
|
+
|
|
1777
|
+
// Dropdown filters (level dropdown is separate from badge filters)
|
|
1778
|
+
if (filterLevelValue && !levelBadgeFiltersActive && level !== filterLevelValue) visible = false;
|
|
1779
|
+
if (filterStatusValue && status !== filterStatusValue) visible = false;
|
|
1780
|
+
if (filterCovValue && coverage !== filterCovValue) visible = false;
|
|
1781
|
+
if (filterTopicValue && topic !== filterTopicValue) visible = false;
|
|
1782
|
+
|
|
1783
|
+
// Level badge filters (HIDE logic - hide if that level's filter is active)
|
|
1784
|
+
if (!isImplNode) {
|
|
1785
|
+
if (state.activeFilters.levelPrd && level === 'PRD') visible = false;
|
|
1786
|
+
if (state.activeFilters.levelOps && level === 'OPS') visible = false;
|
|
1787
|
+
if (state.activeFilters.levelDev && level === 'DEV') visible = false;
|
|
1788
|
+
// Associated filter - same HIDE logic as level filters
|
|
1789
|
+
if (state.activeFilters.associated && isAssociated) visible = false;
|
|
1790
|
+
// Core filter - HIDE logic: hide non-associated (core) requirements
|
|
1791
|
+
if (state.activeFilters.core && !isAssociated) visible = false;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// Toggle filters
|
|
1795
|
+
if (leafOnly && !isLeaf && !isImplNode) visible = false;
|
|
1796
|
+
if (!includeDeprecated && status === 'DEPRECATED') visible = false;
|
|
1797
|
+
if (!includeRoadmap && isRoadmap) visible = false;
|
|
1798
|
+
|
|
1799
|
+
// Git filters
|
|
1800
|
+
if (state.activeFilters.uncommitted && !isUncommitted) visible = false;
|
|
1801
|
+
if (state.activeFilters.changed && !isChanged) visible = false;
|
|
1802
|
+
|
|
1803
|
+
// Hierarchical mode: hide if parent is collapsed
|
|
1804
|
+
if (state.viewMode === 'hierarchical' && parentId && depth > 0) {
|
|
1805
|
+
if (isParentCollapsed(parentId)) {
|
|
1806
|
+
visible = false;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Flat mode: reset indentation visually
|
|
1811
|
+
if (state.viewMode === 'flat') {
|
|
1812
|
+
// In flat mode, all rows are at depth 0 visually
|
|
1813
|
+
// We keep the data but hide tree structure
|
|
1814
|
+
row.querySelectorAll('.tree-indent').forEach(indent => {
|
|
1815
|
+
indent.style.display = 'none';
|
|
1816
|
+
});
|
|
1817
|
+
row.querySelector('.tree-toggle')?.classList.add('leaf');
|
|
1818
|
+
} else {
|
|
1819
|
+
row.querySelectorAll('.tree-indent').forEach(indent => {
|
|
1820
|
+
indent.style.display = '';
|
|
1821
|
+
});
|
|
1822
|
+
const toggle = row.querySelector('.tree-toggle');
|
|
1823
|
+
if (toggle && row.dataset.hasChildren === 'true') {
|
|
1824
|
+
toggle.classList.remove('leaf');
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
row.classList.toggle('hidden', !visible);
|
|
1829
|
+
if (visible && !isImplNode) visibleCount++; // Only count requirements, not code/test
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
updateCounter(visibleCount);
|
|
1833
|
+
saveStateToCookie();
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function isParentCollapsed(parentId) {
|
|
1837
|
+
if (!parentId) return false;
|
|
1838
|
+
if (state.collapsedNodes.has(parentId)) return true;
|
|
1839
|
+
|
|
1840
|
+
// Check ancestors
|
|
1841
|
+
const parentRow = document.querySelector(`[data-id="${parentId}"]`);
|
|
1842
|
+
if (parentRow && parentRow.dataset.parent) {
|
|
1843
|
+
return isParentCollapsed(parentRow.dataset.parent);
|
|
1844
|
+
}
|
|
1845
|
+
return false;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function updateCounter(count) {
|
|
1849
|
+
document.getElementById('counter').textContent =
|
|
1850
|
+
`Showing ${count} of ${state.totalCount} requirements`;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1854
|
+
// Tree Expand/Collapse
|
|
1855
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1856
|
+
|
|
1857
|
+
function toggleNode(nodeId) {
|
|
1858
|
+
const toggle = document.querySelector(`[data-node-id="${nodeId}"]`);
|
|
1859
|
+
if (!toggle) return;
|
|
1860
|
+
|
|
1861
|
+
if (state.collapsedNodes.has(nodeId)) {
|
|
1862
|
+
state.collapsedNodes.delete(nodeId);
|
|
1863
|
+
toggle.classList.remove('collapsed');
|
|
1864
|
+
toggle.classList.add('expanded');
|
|
1865
|
+
} else {
|
|
1866
|
+
state.collapsedNodes.add(nodeId);
|
|
1867
|
+
toggle.classList.remove('expanded');
|
|
1868
|
+
toggle.classList.add('collapsed');
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
applyFilters();
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
function expandAll() {
|
|
1875
|
+
state.collapsedNodes.clear();
|
|
1876
|
+
document.querySelectorAll('.tree-toggle').forEach(toggle => {
|
|
1877
|
+
if (!toggle.classList.contains('leaf')) {
|
|
1878
|
+
toggle.classList.remove('collapsed');
|
|
1879
|
+
toggle.classList.add('expanded');
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
applyFilters();
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function collapseAll() {
|
|
1886
|
+
document.querySelectorAll('.tree-toggle').forEach(toggle => {
|
|
1887
|
+
if (!toggle.classList.contains('leaf')) {
|
|
1888
|
+
const nodeId = toggle.dataset.nodeId;
|
|
1889
|
+
state.collapsedNodes.add(nodeId);
|
|
1890
|
+
toggle.classList.remove('expanded');
|
|
1891
|
+
toggle.classList.add('collapsed');
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
applyFilters();
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1898
|
+
// Tabs
|
|
1899
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1900
|
+
|
|
1901
|
+
function switchTab(tabName, save = true) {
|
|
1902
|
+
state.activeTab = tabName;
|
|
1903
|
+
|
|
1904
|
+
// Update tab buttons
|
|
1905
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
1906
|
+
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
// Switch tab content visibility
|
|
1910
|
+
document.querySelectorAll('.tab-content').forEach(content => {
|
|
1911
|
+
content.classList.remove('active');
|
|
1912
|
+
});
|
|
1913
|
+
const activeContent = document.getElementById('content-' + tabName);
|
|
1914
|
+
if (activeContent) {
|
|
1915
|
+
activeContent.classList.add('active');
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (save) {
|
|
1919
|
+
saveStateToCookie();
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1924
|
+
// Journey Filtering and Grouping
|
|
1925
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1926
|
+
|
|
1927
|
+
// Store original card order for ungrouping
|
|
1928
|
+
let originalJourneyCards = null;
|
|
1929
|
+
let collapsedJourneyGroups = new Set();
|
|
1930
|
+
|
|
1931
|
+
function filterJourneys() {
|
|
1932
|
+
const searchInput = document.getElementById('journey-search');
|
|
1933
|
+
const query = searchInput.value.toLowerCase().trim();
|
|
1934
|
+
|
|
1935
|
+
// Filter both ungrouped cards and cards within groups
|
|
1936
|
+
const allCards = document.querySelectorAll('.journey-card');
|
|
1937
|
+
let visibleCount = 0;
|
|
1938
|
+
|
|
1939
|
+
allCards.forEach(card => {
|
|
1940
|
+
if (!query) {
|
|
1941
|
+
card.classList.remove('hidden');
|
|
1942
|
+
visibleCount++;
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
const id = card.dataset.id || '';
|
|
1947
|
+
const title = card.dataset.title || '';
|
|
1948
|
+
const actor = card.dataset.actor || '';
|
|
1949
|
+
const goal = card.dataset.goal || '';
|
|
1950
|
+
const descriptor = card.dataset.descriptor || '';
|
|
1951
|
+
const file = card.dataset.file || '';
|
|
1952
|
+
|
|
1953
|
+
const matches = id.includes(query) ||
|
|
1954
|
+
title.includes(query) ||
|
|
1955
|
+
actor.includes(query) ||
|
|
1956
|
+
goal.includes(query) ||
|
|
1957
|
+
descriptor.includes(query) ||
|
|
1958
|
+
file.includes(query);
|
|
1959
|
+
|
|
1960
|
+
card.classList.toggle('hidden', !matches);
|
|
1961
|
+
if (matches) visibleCount++;
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
// Update group counts if grouped
|
|
1965
|
+
updateGroupCounts();
|
|
1966
|
+
|
|
1967
|
+
// Update visible count
|
|
1968
|
+
const countEl = document.getElementById('journey-visible-count');
|
|
1969
|
+
if (countEl) {
|
|
1970
|
+
countEl.textContent = visibleCount;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
saveStateToCookie();
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function updateGroupCounts() {
|
|
1977
|
+
const groups = document.querySelectorAll('.journey-group');
|
|
1978
|
+
groups.forEach(group => {
|
|
1979
|
+
const visibleCards = group.querySelectorAll('.journey-card:not(.hidden)');
|
|
1980
|
+
const countEl = group.querySelector('.journey-group-count');
|
|
1981
|
+
if (countEl) {
|
|
1982
|
+
countEl.textContent = visibleCards.length;
|
|
1983
|
+
}
|
|
1984
|
+
// Hide group if no visible cards
|
|
1985
|
+
group.style.display = visibleCards.length === 0 ? 'none' : '';
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function regroupJourneys() {
|
|
1990
|
+
const groupBy = document.getElementById('journey-group-by').value;
|
|
1991
|
+
const journeyList = document.getElementById('journey-list');
|
|
1992
|
+
|
|
1993
|
+
// Store original cards on first grouping
|
|
1994
|
+
if (!originalJourneyCards) {
|
|
1995
|
+
originalJourneyCards = Array.from(journeyList.querySelectorAll('.journey-card'));
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Clear current content
|
|
1999
|
+
journeyList.innerHTML = '';
|
|
2000
|
+
|
|
2001
|
+
if (groupBy === 'none') {
|
|
2002
|
+
// Restore ungrouped view
|
|
2003
|
+
originalJourneyCards.forEach(card => {
|
|
2004
|
+
card.classList.remove('compact');
|
|
2005
|
+
journeyList.appendChild(card);
|
|
2006
|
+
});
|
|
2007
|
+
} else {
|
|
2008
|
+
// Group cards by selected attribute
|
|
2009
|
+
const groups = new Map();
|
|
2010
|
+
|
|
2011
|
+
originalJourneyCards.forEach(card => {
|
|
2012
|
+
let key = card.dataset[groupBy] || '(none)';
|
|
2013
|
+
// Capitalize first letter
|
|
2014
|
+
if (key !== '(none)') {
|
|
2015
|
+
key = key.charAt(0).toUpperCase() + key.slice(1);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
if (!groups.has(key)) {
|
|
2019
|
+
groups.set(key, []);
|
|
2020
|
+
}
|
|
2021
|
+
groups.get(key).push(card);
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
// Sort groups alphabetically, but put (none) last
|
|
2025
|
+
const sortedKeys = Array.from(groups.keys()).sort((a, b) => {
|
|
2026
|
+
if (a === '(none)') return 1;
|
|
2027
|
+
if (b === '(none)') return -1;
|
|
2028
|
+
return a.localeCompare(b);
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// Create group elements
|
|
2032
|
+
sortedKeys.forEach(key => {
|
|
2033
|
+
const cards = groups.get(key);
|
|
2034
|
+
const groupEl = document.createElement('div');
|
|
2035
|
+
groupEl.className = 'journey-group';
|
|
2036
|
+
groupEl.dataset.groupKey = key.toLowerCase();
|
|
2037
|
+
|
|
2038
|
+
// Check if this group was previously collapsed
|
|
2039
|
+
if (collapsedJourneyGroups.has(key.toLowerCase())) {
|
|
2040
|
+
groupEl.classList.add('collapsed');
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const header = document.createElement('div');
|
|
2044
|
+
header.className = 'journey-group-header';
|
|
2045
|
+
header.onclick = () => toggleJourneyGroup(groupEl);
|
|
2046
|
+
header.innerHTML = `
|
|
2047
|
+
<span class="expand-icon">▼</span>
|
|
2048
|
+
<h4>${key}</h4>
|
|
2049
|
+
<span class="journey-group-count">${cards.length}</span>
|
|
2050
|
+
`;
|
|
2051
|
+
|
|
2052
|
+
const cardsContainer = document.createElement('div');
|
|
2053
|
+
cardsContainer.className = 'journey-group-cards';
|
|
2054
|
+
|
|
2055
|
+
cards.forEach(card => {
|
|
2056
|
+
card.classList.add('compact');
|
|
2057
|
+
cardsContainer.appendChild(card);
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
groupEl.appendChild(header);
|
|
2061
|
+
groupEl.appendChild(cardsContainer);
|
|
2062
|
+
journeyList.appendChild(groupEl);
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// Re-apply search filter
|
|
2067
|
+
filterJourneys();
|
|
2068
|
+
saveStateToCookie();
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function toggleJourneyGroup(groupEl) {
|
|
2072
|
+
groupEl.classList.toggle('collapsed');
|
|
2073
|
+
const key = groupEl.dataset.groupKey;
|
|
2074
|
+
|
|
2075
|
+
if (groupEl.classList.contains('collapsed')) {
|
|
2076
|
+
collapsedJourneyGroups.add(key);
|
|
2077
|
+
} else {
|
|
2078
|
+
collapsedJourneyGroups.delete(key);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
saveStateToCookie();
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function expandAllJourneyGroups() {
|
|
2085
|
+
document.querySelectorAll('.journey-group').forEach(group => {
|
|
2086
|
+
group.classList.remove('collapsed');
|
|
2087
|
+
});
|
|
2088
|
+
collapsedJourneyGroups.clear();
|
|
2089
|
+
saveStateToCookie();
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function collapseAllJourneyGroups() {
|
|
2093
|
+
document.querySelectorAll('.journey-group').forEach(group => {
|
|
2094
|
+
group.classList.add('collapsed');
|
|
2095
|
+
collapsedJourneyGroups.add(group.dataset.groupKey);
|
|
2096
|
+
});
|
|
2097
|
+
saveStateToCookie();
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2101
|
+
// Legend Modal
|
|
2102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2103
|
+
|
|
2104
|
+
function toggleLegend() {
|
|
2105
|
+
const modal = document.getElementById('legend-modal');
|
|
2106
|
+
modal.classList.toggle('visible');
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function closeLegendOnOverlay(event) {
|
|
2110
|
+
if (event.target === event.currentTarget) {
|
|
2111
|
+
toggleLegend();
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2116
|
+
// Keyboard Shortcuts
|
|
2117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2118
|
+
|
|
2119
|
+
document.addEventListener('keydown', function(e) {
|
|
2120
|
+
// Escape closes modal
|
|
2121
|
+
if (e.key === 'Escape') {
|
|
2122
|
+
const modal = document.getElementById('legend-modal');
|
|
2123
|
+
if (modal.classList.contains('visible')) {
|
|
2124
|
+
toggleLegend();
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2130
|
+
// Initialize
|
|
2131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2132
|
+
|
|
2133
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2134
|
+
// Check if there's saved state to restore
|
|
2135
|
+
const hasSavedState = loadStateFromCookie();
|
|
2136
|
+
|
|
2137
|
+
if (hasSavedState) {
|
|
2138
|
+
// Restore saved state from cookie (includes collapsed nodes)
|
|
2139
|
+
restoreState();
|
|
2140
|
+
} else {
|
|
2141
|
+
// No saved state - start with tree collapsed (default state)
|
|
2142
|
+
collapseAll();
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// Apply filters (this will also save state)
|
|
2146
|
+
applyFilters();
|
|
2147
|
+
});
|
|
2148
|
+
</script>
|
|
2149
|
+
|
|
2150
|
+
</body>
|
|
2151
|
+
</html>
|