viberadar 0.2.2 → 0.3.1
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.
- package/dist/scanner/index.d.ts +23 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +110 -8
- package/dist/scanner/index.js.map +1 -1
- package/dist/ui/dashboard.html +571 -402
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -7,141 +7,217 @@
|
|
|
7
7
|
<style>
|
|
8
8
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
9
|
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0d1117;
|
|
12
|
+
--bg-card: #161b22;
|
|
13
|
+
--bg-hover: #1c2230;
|
|
14
|
+
--border: #21262d;
|
|
15
|
+
--text: #e6edf3;
|
|
16
|
+
--muted: #7d8590;
|
|
17
|
+
--dim: #484f58;
|
|
18
|
+
--blue: #58a6ff;
|
|
19
|
+
--green: #3fb950;
|
|
20
|
+
--red: #f85149;
|
|
21
|
+
--yellow: #e3b341;
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
body {
|
|
11
|
-
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
12
|
-
background:
|
|
13
|
-
color:
|
|
14
|
-
|
|
25
|
+
font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
height: 100vh;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
15
32
|
}
|
|
16
33
|
|
|
34
|
+
/* ── Header ──────────────────────────────────────────────────────────────── */
|
|
17
35
|
header {
|
|
18
36
|
display: flex;
|
|
19
37
|
align-items: center;
|
|
20
|
-
gap:
|
|
21
|
-
padding:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
header h1 {
|
|
27
|
-
font-size: 20px;
|
|
28
|
-
font-weight: 700;
|
|
29
|
-
letter-spacing: -0.5px;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
header .project-name {
|
|
33
|
-
font-size: 13px;
|
|
34
|
-
color: #7d8590;
|
|
35
|
-
margin-left: auto;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
header .scanned-at {
|
|
39
|
-
font-size: 12px;
|
|
40
|
-
color: #484f58;
|
|
38
|
+
gap: 10px;
|
|
39
|
+
padding: 12px 20px;
|
|
40
|
+
background: var(--bg-card);
|
|
41
|
+
border-bottom: 1px solid var(--border);
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
z-index: 10;
|
|
41
44
|
}
|
|
45
|
+
header h1 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; }
|
|
46
|
+
.header-project { margin-left: auto; font-size: 13px; color: var(--muted); }
|
|
47
|
+
.header-time { font-size: 12px; color: var(--dim); }
|
|
42
48
|
|
|
49
|
+
/* ── Stats bar ───────────────────────────────────────────────────────────── */
|
|
43
50
|
.stats-bar {
|
|
44
51
|
display: flex;
|
|
45
|
-
gap:
|
|
46
|
-
padding:
|
|
47
|
-
background:
|
|
48
|
-
border-bottom: 1px solid
|
|
52
|
+
gap: 28px;
|
|
53
|
+
padding: 12px 20px;
|
|
54
|
+
background: var(--bg-card);
|
|
55
|
+
border-bottom: 1px solid var(--border);
|
|
56
|
+
flex-shrink: 0;
|
|
49
57
|
flex-wrap: wrap;
|
|
50
58
|
}
|
|
59
|
+
.stat { display: flex; flex-direction: column; gap: 2px; }
|
|
60
|
+
.stat-value { font-size: 20px; font-weight: 700; color: var(--blue); }
|
|
61
|
+
.stat-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
flex-direction: column;
|
|
55
|
-
gap: 2px;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.stat-value {
|
|
59
|
-
font-size: 22px;
|
|
60
|
-
font-weight: 700;
|
|
61
|
-
color: #58a6ff;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.stat-label {
|
|
65
|
-
font-size: 11px;
|
|
66
|
-
color: #7d8590;
|
|
67
|
-
text-transform: uppercase;
|
|
68
|
-
letter-spacing: 0.5px;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.main {
|
|
72
|
-
display: grid;
|
|
73
|
-
grid-template-columns: 260px 1fr;
|
|
74
|
-
height: calc(100vh - 110px);
|
|
75
|
-
}
|
|
63
|
+
/* ── Layout ──────────────────────────────────────────────────────────────── */
|
|
64
|
+
.layout { display: flex; flex: 1; overflow: hidden; }
|
|
76
65
|
|
|
66
|
+
/* ── Sidebar ─────────────────────────────────────────────────────────────── */
|
|
77
67
|
.sidebar {
|
|
78
|
-
|
|
68
|
+
width: 220px;
|
|
69
|
+
flex-shrink: 0;
|
|
70
|
+
border-right: 1px solid var(--border);
|
|
79
71
|
overflow-y: auto;
|
|
80
|
-
padding:
|
|
72
|
+
padding: 14px 10px;
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: 14px;
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
.
|
|
84
|
-
|
|
78
|
+
.view-tabs {
|
|
79
|
+
display: flex;
|
|
80
|
+
background: var(--bg);
|
|
81
|
+
border-radius: 6px;
|
|
82
|
+
padding: 3px;
|
|
83
|
+
gap: 2px;
|
|
85
84
|
}
|
|
85
|
+
.view-tab {
|
|
86
|
+
flex: 1;
|
|
87
|
+
padding: 5px 0;
|
|
88
|
+
text-align: center;
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
border-radius: 4px;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
color: var(--muted);
|
|
94
|
+
transition: background 0.15s, color 0.15s;
|
|
95
|
+
user-select: none;
|
|
96
|
+
}
|
|
97
|
+
.view-tab.active { background: var(--bg-card); color: var(--text); }
|
|
98
|
+
.view-tab.disabled { opacity: 0.4; pointer-events: none; }
|
|
86
99
|
|
|
87
|
-
.
|
|
88
|
-
font-size:
|
|
89
|
-
color:
|
|
100
|
+
.sidebar-label {
|
|
101
|
+
font-size: 10px;
|
|
102
|
+
color: var(--muted);
|
|
90
103
|
text-transform: uppercase;
|
|
91
104
|
letter-spacing: 0.5px;
|
|
92
|
-
|
|
93
|
-
margin-bottom: 6px;
|
|
105
|
+
padding: 0 6px;
|
|
94
106
|
}
|
|
95
107
|
|
|
96
|
-
.
|
|
108
|
+
.search-input {
|
|
97
109
|
width: 100%;
|
|
98
|
-
padding:
|
|
99
|
-
background:
|
|
100
|
-
border: 1px solid
|
|
110
|
+
padding: 7px 10px;
|
|
111
|
+
background: var(--bg);
|
|
112
|
+
border: 1px solid var(--border);
|
|
101
113
|
border-radius: 6px;
|
|
102
|
-
color:
|
|
114
|
+
color: var(--text);
|
|
103
115
|
font-size: 13px;
|
|
104
116
|
outline: none;
|
|
105
117
|
}
|
|
118
|
+
.search-input:focus { border-color: var(--blue); }
|
|
119
|
+
.search-input::placeholder { color: var(--dim); }
|
|
120
|
+
|
|
121
|
+
.type-filter {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 7px;
|
|
125
|
+
padding: 5px 8px;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
font-size: 12px;
|
|
129
|
+
color: var(--muted);
|
|
130
|
+
transition: background 0.1s, color 0.1s;
|
|
131
|
+
}
|
|
132
|
+
.type-filter:hover { background: var(--border); color: var(--text); }
|
|
133
|
+
.type-filter.active { background: var(--border); color: var(--text); }
|
|
134
|
+
.type-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
|
135
|
+
.type-count { margin-left: auto; font-size: 11px; color: var(--dim); }
|
|
136
|
+
|
|
137
|
+
/* ── Content area ────────────────────────────────────────────────────────── */
|
|
138
|
+
.content { flex: 1; overflow-y: auto; padding: 18px 20px; }
|
|
106
139
|
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
/* ── Feature cards ───────────────────────────────────────────────────────── */
|
|
141
|
+
.features-grid {
|
|
142
|
+
display: grid;
|
|
143
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
144
|
+
gap: 12px;
|
|
109
145
|
}
|
|
110
146
|
|
|
111
|
-
.
|
|
112
|
-
|
|
147
|
+
.feature-card {
|
|
148
|
+
background: var(--bg-card);
|
|
149
|
+
border: 1px solid var(--border);
|
|
150
|
+
border-radius: 8px;
|
|
151
|
+
cursor: pointer;
|
|
113
152
|
display: flex;
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
transition: background 0.15s, transform 0.1s, border-color 0.15s;
|
|
116
155
|
}
|
|
156
|
+
.feature-card:hover { background: var(--bg-hover); transform: translateY(-1px); }
|
|
157
|
+
.feature-card.active { background: var(--bg-hover); border-color: #30363d; }
|
|
117
158
|
|
|
118
|
-
.
|
|
159
|
+
.feature-accent { width: 4px; flex-shrink: 0; }
|
|
160
|
+
|
|
161
|
+
.feature-body { padding: 14px 16px; flex: 1; min-width: 0; }
|
|
162
|
+
|
|
163
|
+
.feature-title {
|
|
164
|
+
font-size: 14px;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
margin-bottom: 5px;
|
|
119
167
|
display: flex;
|
|
120
168
|
align-items: center;
|
|
169
|
+
justify-content: space-between;
|
|
121
170
|
gap: 8px;
|
|
122
|
-
padding: 4px 6px;
|
|
123
|
-
border-radius: 4px;
|
|
124
|
-
cursor: pointer;
|
|
125
|
-
font-size: 13px;
|
|
126
|
-
color: #7d8590;
|
|
127
|
-
transition: background 0.1s;
|
|
128
171
|
}
|
|
172
|
+
.feature-file-count { font-size: 11px; color: var(--muted); white-space: nowrap; flex-shrink: 0; }
|
|
129
173
|
|
|
130
|
-
.
|
|
131
|
-
|
|
174
|
+
.feature-desc {
|
|
175
|
+
font-size: 12px;
|
|
176
|
+
color: var(--muted);
|
|
177
|
+
margin-bottom: 12px;
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
display: -webkit-box;
|
|
180
|
+
-webkit-line-clamp: 2;
|
|
181
|
+
-webkit-box-orient: vertical;
|
|
182
|
+
line-height: 1.4;
|
|
183
|
+
}
|
|
132
184
|
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
185
|
+
.feature-progress-wrap { display: flex; align-items: center; gap: 8px; }
|
|
186
|
+
.feature-progress-bar {
|
|
187
|
+
flex: 1;
|
|
188
|
+
height: 5px;
|
|
189
|
+
background: var(--border);
|
|
190
|
+
border-radius: 3px;
|
|
191
|
+
overflow: hidden;
|
|
138
192
|
}
|
|
193
|
+
.feature-progress-fill { height: 100%; border-radius: 3px; transition: width 0.4s; }
|
|
194
|
+
.feature-progress-label { font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
139
195
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
196
|
+
/* ── No config banner ────────────────────────────────────────────────────── */
|
|
197
|
+
.no-config {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
align-items: center;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
gap: 16px;
|
|
203
|
+
text-align: center;
|
|
204
|
+
padding: 60px 20px;
|
|
205
|
+
color: var(--muted);
|
|
206
|
+
}
|
|
207
|
+
.no-config-icon { font-size: 48px; }
|
|
208
|
+
.no-config h2 { font-size: 18px; color: var(--text); }
|
|
209
|
+
.no-config p { font-size: 14px; max-width: 420px; line-height: 1.6; }
|
|
210
|
+
.no-config-cmd {
|
|
211
|
+
background: var(--bg-card);
|
|
212
|
+
border: 1px solid var(--border);
|
|
213
|
+
border-radius: 8px;
|
|
214
|
+
padding: 12px 28px;
|
|
215
|
+
font-family: monospace;
|
|
216
|
+
font-size: 15px;
|
|
217
|
+
color: var(--blue);
|
|
143
218
|
}
|
|
144
219
|
|
|
220
|
+
/* ── Module grid (Files view) ────────────────────────────────────────────── */
|
|
145
221
|
.module-grid {
|
|
146
222
|
display: grid;
|
|
147
223
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
@@ -149,240 +225,165 @@
|
|
|
149
225
|
}
|
|
150
226
|
|
|
151
227
|
.module-card {
|
|
152
|
-
background:
|
|
153
|
-
border: 1px solid
|
|
228
|
+
background: var(--bg-card);
|
|
229
|
+
border: 1px solid var(--border);
|
|
154
230
|
border-radius: 8px;
|
|
155
231
|
padding: 12px;
|
|
156
232
|
cursor: pointer;
|
|
157
233
|
transition: border-color 0.15s, transform 0.1s;
|
|
158
234
|
}
|
|
159
|
-
|
|
160
|
-
.module-card:hover
|
|
161
|
-
border-color: #58a6ff;
|
|
162
|
-
transform: translateY(-1px);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.module-card.selected {
|
|
166
|
-
border-color: #58a6ff;
|
|
167
|
-
background: #1c2230;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.module-card-header {
|
|
171
|
-
display: flex;
|
|
172
|
-
align-items: flex-start;
|
|
173
|
-
gap: 8px;
|
|
174
|
-
margin-bottom: 8px;
|
|
175
|
-
}
|
|
235
|
+
.module-card:hover { border-color: var(--blue); transform: translateY(-1px); }
|
|
236
|
+
.module-card.active { border-color: var(--blue); background: var(--bg-hover); }
|
|
176
237
|
|
|
177
238
|
.module-name {
|
|
178
239
|
font-size: 13px;
|
|
179
240
|
font-weight: 600;
|
|
180
|
-
|
|
181
|
-
flex: 1;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.badge {
|
|
185
|
-
font-size: 10px;
|
|
186
|
-
padding: 2px 6px;
|
|
187
|
-
border-radius: 10px;
|
|
188
|
-
font-weight: 600;
|
|
189
|
-
white-space: nowrap;
|
|
190
|
-
flex-shrink: 0;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.badge-green { background: #1a3a2a; color: #3fb950; }
|
|
194
|
-
.badge-red { background: #3a1a1a; color: #f85149; }
|
|
195
|
-
.badge-gray { background: #21262d; color: #7d8590; }
|
|
196
|
-
|
|
197
|
-
.module-path {
|
|
198
|
-
font-size: 11px;
|
|
199
|
-
color: #484f58;
|
|
200
|
-
margin-bottom: 8px;
|
|
241
|
+
margin-bottom: 4px;
|
|
201
242
|
word-break: break-all;
|
|
202
243
|
}
|
|
244
|
+
.module-path { font-size: 11px; color: var(--dim); margin-bottom: 8px; word-break: break-all; }
|
|
245
|
+
.module-meta { display: flex; align-items: center; justify-content: space-between; font-size: 11px; color: var(--muted); }
|
|
203
246
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
margin-top: 6px;
|
|
210
|
-
}
|
|
247
|
+
/* ── Badges ──────────────────────────────────────────────────────────────── */
|
|
248
|
+
.badge { font-size: 10px; padding: 2px 6px; border-radius: 10px; font-weight: 600; white-space: nowrap; }
|
|
249
|
+
.badge-green { background: #1a3a2a; color: var(--green); }
|
|
250
|
+
.badge-red { background: #3a1a1a; color: var(--red); }
|
|
251
|
+
.badge-gray { background: var(--border); color: var(--muted); }
|
|
211
252
|
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
border-radius: 2px;
|
|
215
|
-
transition: width 0.3s;
|
|
216
|
-
}
|
|
253
|
+
.cov-bar { height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; margin-top: 8px; }
|
|
254
|
+
.cov-fill { height: 100%; border-radius: 2px; }
|
|
217
255
|
|
|
218
|
-
|
|
256
|
+
/* ── Right panel ─────────────────────────────────────────────────────────── */
|
|
257
|
+
.panel {
|
|
219
258
|
position: fixed;
|
|
220
|
-
right: 0;
|
|
221
|
-
|
|
222
|
-
width: 380px;
|
|
259
|
+
top: 0; right: 0;
|
|
260
|
+
width: 420px;
|
|
223
261
|
height: 100vh;
|
|
224
|
-
background:
|
|
225
|
-
border-left: 1px solid
|
|
226
|
-
padding: 20px;
|
|
262
|
+
background: var(--bg-card);
|
|
263
|
+
border-left: 1px solid var(--border);
|
|
227
264
|
overflow-y: auto;
|
|
228
265
|
transform: translateX(100%);
|
|
229
|
-
transition: transform 0.2s;
|
|
266
|
+
transition: transform 0.2s ease;
|
|
230
267
|
z-index: 100;
|
|
268
|
+
padding: 20px;
|
|
231
269
|
}
|
|
270
|
+
.panel.open { transform: translateX(0); }
|
|
232
271
|
|
|
233
|
-
.
|
|
234
|
-
transform: translateX(0);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.detail-close {
|
|
272
|
+
.panel-close {
|
|
238
273
|
position: absolute;
|
|
239
|
-
top: 16px;
|
|
240
|
-
right: 16px;
|
|
274
|
+
top: 16px; right: 16px;
|
|
241
275
|
background: none;
|
|
242
276
|
border: none;
|
|
243
|
-
color:
|
|
277
|
+
color: var(--muted);
|
|
244
278
|
cursor: pointer;
|
|
245
|
-
font-size: 18px;
|
|
246
|
-
padding: 4px;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.detail-close:hover { color: #e6edf3; }
|
|
250
|
-
|
|
251
|
-
.detail-title {
|
|
252
279
|
font-size: 16px;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
.detail-path {
|
|
259
|
-
font-size: 11px;
|
|
260
|
-
color: #484f58;
|
|
261
|
-
margin-bottom: 16px;
|
|
262
|
-
word-break: break-all;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
.detail-section {
|
|
266
|
-
margin-bottom: 16px;
|
|
280
|
+
padding: 5px 8px;
|
|
281
|
+
border-radius: 4px;
|
|
282
|
+
line-height: 1;
|
|
267
283
|
}
|
|
284
|
+
.panel-close:hover { background: var(--border); color: var(--text); }
|
|
268
285
|
|
|
269
|
-
.
|
|
270
|
-
|
|
271
|
-
text-transform: uppercase;
|
|
272
|
-
letter-spacing: 0.5px;
|
|
273
|
-
color: #7d8590;
|
|
274
|
-
margin-bottom: 8px;
|
|
275
|
-
}
|
|
286
|
+
.panel-title { font-size: 17px; font-weight: 700; padding-right: 36px; margin-bottom: 4px; }
|
|
287
|
+
.panel-subtitle { font-size: 12px; color: var(--muted); margin-bottom: 20px; line-height: 1.5; }
|
|
276
288
|
|
|
277
|
-
.
|
|
289
|
+
.panel-stats {
|
|
278
290
|
display: flex;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
padding:
|
|
282
|
-
border-bottom: 1px solid
|
|
291
|
+
gap: 0;
|
|
292
|
+
margin-bottom: 20px;
|
|
293
|
+
padding-bottom: 16px;
|
|
294
|
+
border-bottom: 1px solid var(--border);
|
|
283
295
|
}
|
|
296
|
+
.panel-stat { flex: 1; text-align: center; }
|
|
297
|
+
.panel-stat-val { font-size: 22px; font-weight: 700; }
|
|
298
|
+
.panel-stat-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
284
299
|
|
|
285
|
-
.
|
|
300
|
+
.panel-section { margin-bottom: 20px; }
|
|
301
|
+
.panel-section-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); margin-bottom: 8px; }
|
|
286
302
|
|
|
287
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
303
|
+
.file-list { display: flex; flex-direction: column; gap: 3px; }
|
|
304
|
+
.file-item {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: flex-start;
|
|
290
307
|
gap: 8px;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
.coverage-metric-item {
|
|
294
|
-
background: #21262d;
|
|
308
|
+
padding: 7px 10px;
|
|
309
|
+
background: var(--bg);
|
|
295
310
|
border-radius: 6px;
|
|
296
|
-
|
|
297
|
-
text-align: center;
|
|
311
|
+
font-size: 12px;
|
|
298
312
|
}
|
|
313
|
+
.file-item-icon { font-size: 11px; flex-shrink: 0; padding-top: 2px; }
|
|
314
|
+
.file-item-name { font-weight: 600; color: var(--text); word-break: break-all; line-height: 1.3; }
|
|
315
|
+
.file-item-dir { color: var(--dim); font-size: 11px; word-break: break-all; }
|
|
299
316
|
|
|
300
|
-
.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
317
|
+
.cov-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
318
|
+
.cov-metric { background: var(--bg); border-radius: 6px; padding: 10px; text-align: center; }
|
|
319
|
+
.cov-metric-val { font-size: 20px; font-weight: 700; }
|
|
320
|
+
.cov-metric-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; }
|
|
304
321
|
|
|
305
|
-
.
|
|
306
|
-
font-size: 10px;
|
|
307
|
-
color: #7d8590;
|
|
308
|
-
text-transform: uppercase;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.deps-list {
|
|
322
|
+
.detail-row {
|
|
312
323
|
display: flex;
|
|
313
|
-
|
|
314
|
-
|
|
324
|
+
justify-content: space-between;
|
|
325
|
+
align-items: center;
|
|
326
|
+
font-size: 13px;
|
|
327
|
+
padding: 6px 0;
|
|
328
|
+
border-bottom: 1px solid var(--border);
|
|
315
329
|
}
|
|
330
|
+
.detail-row:last-child { border-bottom: none; }
|
|
331
|
+
.detail-row-right { font-size: 11px; color: var(--muted); text-align: right; max-width: 60%; word-break: break-all; }
|
|
316
332
|
|
|
317
333
|
.dep-item {
|
|
318
334
|
font-size: 12px;
|
|
319
|
-
color:
|
|
335
|
+
color: var(--muted);
|
|
320
336
|
padding: 3px 8px;
|
|
321
|
-
background:
|
|
337
|
+
background: var(--bg);
|
|
322
338
|
border-radius: 4px;
|
|
323
339
|
font-family: monospace;
|
|
324
340
|
}
|
|
325
341
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
justify-content: center;
|
|
330
|
-
height: 200px;
|
|
331
|
-
color: #7d8590;
|
|
332
|
-
font-size: 14px;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
.type-colors {
|
|
336
|
-
component: '#58a6ff';
|
|
337
|
-
service: '#d2a8ff';
|
|
338
|
-
util: '#ffa657';
|
|
339
|
-
test: '#3fb950';
|
|
340
|
-
config: '#f85149';
|
|
341
|
-
other: '#484f58';
|
|
342
|
-
}
|
|
342
|
+
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
343
|
+
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
344
|
+
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
343
345
|
</style>
|
|
344
346
|
</head>
|
|
345
347
|
<body>
|
|
346
348
|
|
|
347
349
|
<header>
|
|
348
|
-
<span style="font-size:
|
|
350
|
+
<span style="font-size:20px">🔭</span>
|
|
349
351
|
<h1>VibeRadar</h1>
|
|
350
|
-
<span class="project
|
|
351
|
-
<span class="
|
|
352
|
+
<span class="header-project" id="projectName">—</span>
|
|
353
|
+
<span class="header-time" id="scannedAt"></span>
|
|
352
354
|
</header>
|
|
353
355
|
|
|
354
|
-
<div class="stats-bar">
|
|
355
|
-
<div class="stat"><span class="stat-value" id="statModules">—</span><span class="stat-label">Modules</span></div>
|
|
356
|
-
<div class="stat"><span class="stat-value" id="statWithTests">—</span><span class="stat-label">With Tests</span></div>
|
|
357
|
-
<div class="stat"><span class="stat-value" id="statCoverage">—</span><span class="stat-label">Avg Coverage</span></div>
|
|
358
|
-
<div class="stat"><span class="stat-value" id="statTests">—</span><span class="stat-label">Test Files</span></div>
|
|
359
|
-
</div>
|
|
360
|
-
|
|
361
|
-
<div class="main">
|
|
362
|
-
<div class="sidebar">
|
|
363
|
-
<div class="filter-group">
|
|
364
|
-
<label>Search</label>
|
|
365
|
-
<input id="searchInput" type="text" placeholder="Filter modules..." />
|
|
366
|
-
</div>
|
|
356
|
+
<div class="stats-bar" id="statsBar"></div>
|
|
367
357
|
|
|
368
|
-
|
|
369
|
-
|
|
358
|
+
<div class="layout">
|
|
359
|
+
<aside class="sidebar">
|
|
360
|
+
<div class="view-tabs" id="viewTabs">
|
|
361
|
+
<div class="view-tab" data-view="features">Features</div>
|
|
362
|
+
<div class="view-tab" data-view="files">Files</div>
|
|
370
363
|
</div>
|
|
371
|
-
<
|
|
372
|
-
|
|
364
|
+
<input class="search-input" id="searchInput" type="text" placeholder="Search…" />
|
|
365
|
+
<div id="sidebarExtra"></div>
|
|
366
|
+
</aside>
|
|
373
367
|
|
|
374
|
-
<
|
|
375
|
-
<div class="loading" id="loading">Loading
|
|
376
|
-
|
|
377
|
-
</div>
|
|
368
|
+
<main class="content" id="content">
|
|
369
|
+
<div class="loading" id="loading">Loading…</div>
|
|
370
|
+
</main>
|
|
378
371
|
</div>
|
|
379
372
|
|
|
380
|
-
<div class="
|
|
381
|
-
<button class="
|
|
382
|
-
<div id="
|
|
373
|
+
<div class="panel" id="panel">
|
|
374
|
+
<button class="panel-close" id="panelClose">✕</button>
|
|
375
|
+
<div id="panelContent"></div>
|
|
383
376
|
</div>
|
|
384
377
|
|
|
385
378
|
<script>
|
|
379
|
+
// ─── State ────────────────────────────────────────────────────────────────────
|
|
380
|
+
let D = null;
|
|
381
|
+
let view = 'features';
|
|
382
|
+
let searchQuery = '';
|
|
383
|
+
let activeTypes = new Set();
|
|
384
|
+
let activePanelKey = null;
|
|
385
|
+
|
|
386
|
+
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
386
387
|
const TYPE_COLORS = {
|
|
387
388
|
component: '#58a6ff',
|
|
388
389
|
service: '#d2a8ff',
|
|
@@ -392,193 +393,361 @@ const TYPE_COLORS = {
|
|
|
392
393
|
other: '#484f58',
|
|
393
394
|
};
|
|
394
395
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
function coverageColor(pct) {
|
|
401
|
-
if (pct === undefined) return '#484f58';
|
|
402
|
-
if (pct >= 80) return '#3fb950';
|
|
403
|
-
if (pct >= 50) return '#e3b341';
|
|
396
|
+
function covColor(pct) {
|
|
397
|
+
if (pct == null) return '#484f58';
|
|
398
|
+
if (pct >= 70) return '#3fb950';
|
|
399
|
+
if (pct >= 30) return '#e3b341';
|
|
404
400
|
return '#f85149';
|
|
405
401
|
}
|
|
406
402
|
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
403
|
+
function fmt(b) {
|
|
404
|
+
return b < 1024 ? b + ' B' : (b / 1024).toFixed(1) + ' KB';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function pluralFiles(n) {
|
|
408
|
+
const m10 = n % 10, m100 = n % 100;
|
|
409
|
+
if (m100 >= 11 && m100 <= 14) return 'файлов';
|
|
410
|
+
if (m10 === 1) return 'файл';
|
|
411
|
+
if (m10 >= 2 && m10 <= 4) return 'файла';
|
|
412
|
+
return 'файлов';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ─── Init ─────────────────────────────────────────────────────────────────────
|
|
416
|
+
async function init() {
|
|
417
|
+
try {
|
|
418
|
+
const res = await fetch('/api/data');
|
|
419
|
+
D = await res.json();
|
|
420
|
+
|
|
421
|
+
document.getElementById('projectName').textContent = D.projectName;
|
|
422
|
+
document.getElementById('scannedAt').textContent =
|
|
423
|
+
new Date(D.scannedAt).toLocaleTimeString();
|
|
424
|
+
|
|
425
|
+
view = D.hasConfig ? 'features' : 'files';
|
|
426
|
+
|
|
427
|
+
if (!D.hasConfig) {
|
|
428
|
+
document.querySelector('[data-view="features"]').classList.add('disabled');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
document.getElementById('loading').style.display = 'none';
|
|
432
|
+
renderStats();
|
|
433
|
+
renderSidebar();
|
|
434
|
+
renderContent();
|
|
435
|
+
} catch (err) {
|
|
436
|
+
document.getElementById('loading').textContent = '❌ Failed to load: ' + err.message;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ─── Stats ────────────────────────────────────────────────────────────────────
|
|
441
|
+
function renderStats() {
|
|
442
|
+
const src = D.modules.filter(m => m.type !== 'test');
|
|
443
|
+
const tested = src.filter(m => m.hasTests).length;
|
|
444
|
+
const testFiles = D.modules.filter(m => m.type === 'test').length;
|
|
445
|
+
const pct = src.length ? Math.round(tested / src.length * 100) : 0;
|
|
446
|
+
|
|
447
|
+
let items;
|
|
448
|
+
if (D.hasConfig && D.features) {
|
|
449
|
+
const unmapped = src.filter(m => !m.featureKeys || m.featureKeys.length === 0).length;
|
|
450
|
+
items = [
|
|
451
|
+
{ v: D.features.length, l: 'Features' },
|
|
452
|
+
{ v: src.length, l: 'Source Files' },
|
|
453
|
+
{ v: pct + '%', l: 'With Tests' },
|
|
454
|
+
{ v: testFiles, l: 'Test Files' },
|
|
455
|
+
unmapped > 0 ? { v: unmapped, l: 'Unmapped', c: '#e3b341' } : null,
|
|
456
|
+
].filter(Boolean);
|
|
457
|
+
} else {
|
|
458
|
+
items = [
|
|
459
|
+
{ v: D.modules.length, l: 'Modules' },
|
|
460
|
+
{ v: tested, l: 'With Tests' },
|
|
461
|
+
{ v: pct + '%', l: 'Test Coverage' },
|
|
462
|
+
{ v: testFiles, l: 'Test Files' },
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
document.getElementById('statsBar').innerHTML = items.map(s =>
|
|
467
|
+
`<div class="stat">
|
|
468
|
+
<span class="stat-value"${s.c ? ` style="color:${s.c}"` : ''}>${s.v}</span>
|
|
469
|
+
<span class="stat-label">${s.l}</span>
|
|
470
|
+
</div>`
|
|
471
|
+
).join('');
|
|
410
472
|
}
|
|
411
473
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
474
|
+
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
|
475
|
+
function renderSidebar() {
|
|
476
|
+
document.querySelectorAll('.view-tab').forEach(t =>
|
|
477
|
+
t.classList.toggle('active', t.dataset.view === view)
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const extra = document.getElementById('sidebarExtra');
|
|
481
|
+
if (view !== 'files') { extra.innerHTML = ''; return; }
|
|
482
|
+
|
|
483
|
+
const types = [...new Set(D.modules.map(m => m.type))].sort();
|
|
484
|
+
extra.innerHTML = `
|
|
485
|
+
<div class="sidebar-label">Type</div>
|
|
486
|
+
<div>
|
|
487
|
+
<div class="type-filter ${activeTypes.size === 0 ? 'active' : ''}" data-t="all">
|
|
488
|
+
<span class="type-dot" style="background:#7d8590"></span>
|
|
489
|
+
All
|
|
490
|
+
<span class="type-count">${D.modules.length}</span>
|
|
491
|
+
</div>
|
|
492
|
+
${types.map(t => `
|
|
493
|
+
<div class="type-filter ${activeTypes.has(t) ? 'active' : ''}" data-t="${t}">
|
|
494
|
+
<span class="type-dot" style="background:${TYPE_COLORS[t] || '#484f58'}"></span>
|
|
495
|
+
${t}
|
|
496
|
+
<span class="type-count">${D.modules.filter(m => m.type === t).length}</span>
|
|
497
|
+
</div>
|
|
498
|
+
`).join('')}
|
|
499
|
+
</div>`;
|
|
500
|
+
|
|
501
|
+
extra.querySelectorAll('.type-filter').forEach(el => {
|
|
502
|
+
el.onclick = () => {
|
|
503
|
+
const t = el.dataset.t;
|
|
504
|
+
if (t === 'all') activeTypes.clear();
|
|
505
|
+
else if (activeTypes.has(t)) activeTypes.delete(t);
|
|
506
|
+
else activeTypes.add(t);
|
|
507
|
+
renderSidebar();
|
|
508
|
+
renderContent();
|
|
432
509
|
};
|
|
433
|
-
container.appendChild(btn);
|
|
434
510
|
});
|
|
435
511
|
}
|
|
436
512
|
|
|
437
|
-
|
|
438
|
-
|
|
513
|
+
// ─── Content ──────────────────────────────────────────────────────────────────
|
|
514
|
+
function renderContent() {
|
|
515
|
+
const c = document.getElementById('content');
|
|
516
|
+
view === 'features' ? renderFeatureCards(c) : renderModuleGrid(c);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function renderFeatureCards(c) {
|
|
520
|
+
if (!D.hasConfig || !D.features) {
|
|
521
|
+
c.innerHTML = `
|
|
522
|
+
<div class="no-config">
|
|
523
|
+
<div class="no-config-icon">🗺️</div>
|
|
524
|
+
<h2>Карта фич не настроена</h2>
|
|
525
|
+
<p>Запусти команду ниже, вставь промпт в AI-агента —<br>он создаст <code>viberadar.config.json</code> с описанием твоих фич</p>
|
|
526
|
+
<div class="no-config-cmd">npx viberadar init</div>
|
|
527
|
+
<p style="font-size:13px;color:#484f58">После создания конфига перезапусти VibeRadar</p>
|
|
528
|
+
</div>`;
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const q = searchQuery.toLowerCase();
|
|
533
|
+
const list = D.features.filter(f =>
|
|
534
|
+
!q || f.label.toLowerCase().includes(q) || f.description.toLowerCase().includes(q)
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
538
|
+
|
|
539
|
+
c.innerHTML = '<div class="features-grid" id="featGrid"></div>';
|
|
540
|
+
const grid = document.getElementById('featGrid');
|
|
541
|
+
|
|
542
|
+
list.forEach(f => {
|
|
543
|
+
const pct = f.fileCount > 0 ? Math.round(f.testedCount / f.fileCount * 100) : 0;
|
|
544
|
+
const isActive = activePanelKey === f.key;
|
|
545
|
+
|
|
546
|
+
const card = document.createElement('div');
|
|
547
|
+
card.className = 'feature-card' + (isActive ? ' active' : '');
|
|
548
|
+
card.innerHTML = `
|
|
549
|
+
<div class="feature-accent" style="background:${f.color}"></div>
|
|
550
|
+
<div class="feature-body">
|
|
551
|
+
<div class="feature-title">
|
|
552
|
+
<span>${f.label}</span>
|
|
553
|
+
<span class="feature-file-count">${f.fileCount} ${pluralFiles(f.fileCount)}</span>
|
|
554
|
+
</div>
|
|
555
|
+
${f.description ? `<div class="feature-desc">${f.description}</div>` : ''}
|
|
556
|
+
<div class="feature-progress-wrap">
|
|
557
|
+
<div class="feature-progress-bar">
|
|
558
|
+
<div class="feature-progress-fill" style="width:${pct}%;background:${f.color}"></div>
|
|
559
|
+
</div>
|
|
560
|
+
<span class="feature-progress-label" style="color:${covColor(pct)}">${f.testedCount}/${f.fileCount} ✓</span>
|
|
561
|
+
</div>
|
|
562
|
+
</div>`;
|
|
563
|
+
card.onclick = () => openFeaturePanel(f.key);
|
|
564
|
+
grid.appendChild(card);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function renderModuleGrid(c) {
|
|
569
|
+
const q = searchQuery.toLowerCase();
|
|
570
|
+
const list = D.modules.filter(m => {
|
|
439
571
|
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
440
|
-
if (
|
|
572
|
+
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
441
573
|
return true;
|
|
442
574
|
});
|
|
443
|
-
}
|
|
444
575
|
|
|
445
|
-
|
|
446
|
-
const grid = document.getElementById('moduleGrid');
|
|
447
|
-
const filtered = getFilteredModules();
|
|
448
|
-
grid.innerHTML = '';
|
|
576
|
+
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
449
577
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
card.className = 'module-card' + (selectedModule?.id === m.id ? ' selected' : '');
|
|
578
|
+
c.innerHTML = '<div class="module-grid" id="modGrid"></div>';
|
|
579
|
+
const grid = document.getElementById('modGrid');
|
|
453
580
|
|
|
581
|
+
list.forEach(m => {
|
|
454
582
|
const cov = m.coverage?.lines;
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
583
|
+
const isActive = activePanelKey === m.id;
|
|
584
|
+
const card = document.createElement('div');
|
|
585
|
+
card.className = 'module-card' + (isActive ? ' active' : '');
|
|
459
586
|
card.innerHTML = `
|
|
460
|
-
<div class="module-
|
|
461
|
-
<span class="module-name">${m.name}</span>
|
|
462
|
-
<span class="badge ${badgeClass}">${badgeText}</span>
|
|
463
|
-
</div>
|
|
587
|
+
<div class="module-name">${m.name}</div>
|
|
464
588
|
<div class="module-path">${m.relativePath}</div>
|
|
465
|
-
<div
|
|
589
|
+
<div class="module-meta">
|
|
466
590
|
<span style="display:flex;align-items:center;gap:4px">
|
|
467
|
-
<span class="type-dot" style="background:${TYPE_COLORS[m.type] || '#484f58'}"></span
|
|
468
|
-
${m.type}
|
|
591
|
+
<span class="type-dot" style="background:${TYPE_COLORS[m.type] || '#484f58'}"></span>${m.type}
|
|
469
592
|
</span>
|
|
470
|
-
<span>${
|
|
593
|
+
<span class="badge ${m.hasTests ? 'badge-green' : 'badge-red'}">${m.hasTests ? '✓' : '✗'}</span>
|
|
471
594
|
</div>
|
|
472
|
-
${cov
|
|
473
|
-
|
|
474
|
-
<div class="coverage-fill" style="width:${cov}%;background:${coverageColor(cov)}"></div>
|
|
475
|
-
</div>
|
|
476
|
-
` : ''}
|
|
477
|
-
`;
|
|
478
|
-
|
|
479
|
-
card.onclick = () => openDetail(m);
|
|
595
|
+
${cov != null ? `<div class="cov-bar"><div class="cov-fill" style="width:${cov}%;background:${covColor(cov)}"></div></div>` : ''}`;
|
|
596
|
+
card.onclick = () => openModulePanel(m);
|
|
480
597
|
grid.appendChild(card);
|
|
481
598
|
});
|
|
482
599
|
}
|
|
483
600
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
601
|
+
// ─── Panels ───────────────────────────────────────────────────────────────────
|
|
602
|
+
function openFeaturePanel(key) {
|
|
603
|
+
activePanelKey = key;
|
|
604
|
+
renderContent();
|
|
605
|
+
|
|
606
|
+
const feat = D.features.find(f => f.key === key);
|
|
607
|
+
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(key));
|
|
608
|
+
const src = mods.filter(m => m.type !== 'test');
|
|
609
|
+
const tst = mods.filter(m => m.type === 'test');
|
|
610
|
+
const testedSrc = src.filter(m => m.hasTests).length;
|
|
611
|
+
const pct = src.length > 0 ? Math.round(testedSrc / src.length * 100) : 0;
|
|
612
|
+
|
|
613
|
+
document.getElementById('panelContent').innerHTML = `
|
|
614
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
|
|
615
|
+
<div style="width:12px;height:12px;border-radius:50%;background:${feat.color};flex-shrink:0"></div>
|
|
616
|
+
<div class="panel-title">${feat.label}</div>
|
|
617
|
+
</div>
|
|
618
|
+
${feat.description ? `<div class="panel-subtitle">${feat.description}</div>` : ''}
|
|
619
|
+
|
|
620
|
+
<div class="panel-stats">
|
|
621
|
+
<div class="panel-stat">
|
|
622
|
+
<div class="panel-stat-val">${src.length}</div>
|
|
623
|
+
<div class="panel-stat-lbl">Файлов</div>
|
|
624
|
+
</div>
|
|
625
|
+
<div class="panel-stat">
|
|
626
|
+
<div class="panel-stat-val" style="color:${covColor(pct)}">${pct}%</div>
|
|
627
|
+
<div class="panel-stat-lbl">С тестами</div>
|
|
628
|
+
</div>
|
|
629
|
+
<div class="panel-stat">
|
|
630
|
+
<div class="panel-stat-val">${tst.length}</div>
|
|
631
|
+
<div class="panel-stat-lbl">Тест-файлов</div>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
<div class="panel-section">
|
|
636
|
+
<div class="panel-section-label">Исходные файлы (${src.length})</div>
|
|
637
|
+
<div class="file-list">
|
|
638
|
+
${src.length === 0
|
|
639
|
+
? '<div style="font-size:13px;color:var(--dim);padding:8px 0">Нет файлов — возможно паттерны не совпадают</div>'
|
|
640
|
+
: src.map(m => fileItem(m)).join('')
|
|
641
|
+
}
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
${tst.length > 0 ? `
|
|
646
|
+
<div class="panel-section">
|
|
647
|
+
<div class="panel-section-label">Тест-файлы (${tst.length})</div>
|
|
648
|
+
<div class="file-list">${tst.map(m => fileItem(m, true)).join('')}</div>
|
|
649
|
+
</div>` : ''}`;
|
|
650
|
+
|
|
651
|
+
document.getElementById('panel').classList.add('open');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function fileItem(m, isTest = false) {
|
|
655
|
+
const parts = m.relativePath.replace(/\\/g, '/').split('/');
|
|
656
|
+
const name = parts[parts.length - 1];
|
|
657
|
+
const dir = parts.slice(0, -1).join('/');
|
|
658
|
+
const icon = isTest ? '🧪' : (m.hasTests ? '✅' : '⬜');
|
|
659
|
+
return `
|
|
660
|
+
<div class="file-item">
|
|
661
|
+
<span class="file-item-icon">${icon}</span>
|
|
662
|
+
<div>
|
|
663
|
+
<div class="file-item-name">${name}</div>
|
|
664
|
+
${dir ? `<div class="file-item-dir">${dir}</div>` : ''}
|
|
665
|
+
</div>
|
|
666
|
+
</div>`;
|
|
487
667
|
}
|
|
488
668
|
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
|
|
669
|
+
function openModulePanel(m) {
|
|
670
|
+
activePanelKey = m.id;
|
|
671
|
+
renderContent();
|
|
492
672
|
|
|
493
673
|
const cov = m.coverage;
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
<div class="
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
<div class="
|
|
504
|
-
<div class="detail-row"
|
|
505
|
-
|
|
506
|
-
|
|
674
|
+
const featureLabels = m.featureKeys && m.featureKeys.length > 0
|
|
675
|
+
? m.featureKeys.map(k => D.features?.find(f => f.key === k)?.label || k).join(', ')
|
|
676
|
+
: null;
|
|
677
|
+
|
|
678
|
+
document.getElementById('panelContent').innerHTML = `
|
|
679
|
+
<div class="panel-title">${m.name}</div>
|
|
680
|
+
<div class="panel-subtitle">${m.relativePath}</div>
|
|
681
|
+
|
|
682
|
+
<div class="panel-section">
|
|
683
|
+
<div class="panel-section-label">Информация</div>
|
|
684
|
+
<div class="detail-row">
|
|
685
|
+
<span>Тип</span>
|
|
686
|
+
<span style="color:${TYPE_COLORS[m.type] || '#484f58'}">${m.type}</span>
|
|
687
|
+
</div>
|
|
688
|
+
<div class="detail-row">
|
|
689
|
+
<span>Размер</span>
|
|
690
|
+
<span>${fmt(m.size)}</span>
|
|
691
|
+
</div>
|
|
692
|
+
<div class="detail-row">
|
|
693
|
+
<span>Тесты</span>
|
|
694
|
+
<span class="badge ${m.hasTests ? 'badge-green' : 'badge-red'}">${m.hasTests ? '✓ есть' : '✗ нет'}</span>
|
|
695
|
+
</div>
|
|
696
|
+
${m.testFile ? `<div class="detail-row"><span>Тест-файл</span><span class="detail-row-right">${m.testFile}</span></div>` : ''}
|
|
697
|
+
${featureLabels ? `<div class="detail-row"><span>Фичи</span><span class="detail-row-right">${featureLabels}</span></div>` : ''}
|
|
507
698
|
</div>
|
|
508
699
|
|
|
509
700
|
${cov ? `
|
|
510
|
-
<div class="
|
|
511
|
-
<div class="
|
|
512
|
-
<div class="
|
|
701
|
+
<div class="panel-section">
|
|
702
|
+
<div class="panel-section-label">Coverage</div>
|
|
703
|
+
<div class="cov-grid">
|
|
513
704
|
${['lines','statements','functions','branches'].map(k => `
|
|
514
|
-
<div class="
|
|
515
|
-
<div class="
|
|
516
|
-
<div class="
|
|
517
|
-
</div
|
|
518
|
-
`).join('')}
|
|
705
|
+
<div class="cov-metric">
|
|
706
|
+
<div class="cov-metric-val" style="color:${covColor(cov[k])}">${cov[k]?.toFixed(1)}%</div>
|
|
707
|
+
<div class="cov-metric-lbl">${k}</div>
|
|
708
|
+
</div>`).join('')}
|
|
519
709
|
</div>
|
|
520
|
-
</div
|
|
521
|
-
` : `
|
|
522
|
-
<div class="detail-section">
|
|
523
|
-
<div class="detail-section-title">Coverage</div>
|
|
524
|
-
<div style="font-size:13px;color:#484f58">No coverage data. Run tests with coverage to see metrics.</div>
|
|
525
|
-
</div>
|
|
526
|
-
`}
|
|
710
|
+
</div>` : ''}
|
|
527
711
|
|
|
528
712
|
${m.dependencies.length > 0 ? `
|
|
529
|
-
<div class="
|
|
530
|
-
<div class="
|
|
531
|
-
<div
|
|
713
|
+
<div class="panel-section">
|
|
714
|
+
<div class="panel-section-label">Импорты (${m.dependencies.length})</div>
|
|
715
|
+
<div style="display:flex;flex-direction:column;gap:4px">
|
|
532
716
|
${m.dependencies.map(d => `<div class="dep-item">${d}</div>`).join('')}
|
|
533
717
|
</div>
|
|
534
|
-
</div
|
|
535
|
-
` : ''}
|
|
536
|
-
`;
|
|
718
|
+
</div>` : ''}`;
|
|
537
719
|
|
|
538
|
-
panel.classList.add('open');
|
|
720
|
+
document.getElementById('panel').classList.add('open');
|
|
539
721
|
}
|
|
540
722
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
723
|
+
function closePanel() {
|
|
724
|
+
activePanelKey = null;
|
|
725
|
+
document.getElementById('panel').classList.remove('open');
|
|
726
|
+
renderContent();
|
|
727
|
+
}
|
|
546
728
|
|
|
547
|
-
|
|
729
|
+
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
730
|
+
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
731
|
+
tab.onclick = () => {
|
|
732
|
+
if (tab.classList.contains('disabled')) return;
|
|
733
|
+
view = tab.dataset.view;
|
|
734
|
+
activePanelKey = null;
|
|
735
|
+
searchQuery = '';
|
|
736
|
+
activeTypes.clear();
|
|
737
|
+
document.getElementById('searchInput').value = '';
|
|
738
|
+
document.getElementById('panel').classList.remove('open');
|
|
739
|
+
renderSidebar();
|
|
740
|
+
renderContent();
|
|
741
|
+
};
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
document.getElementById('searchInput').oninput = e => {
|
|
548
745
|
searchQuery = e.target.value.toLowerCase();
|
|
549
|
-
|
|
746
|
+
renderContent();
|
|
550
747
|
};
|
|
551
748
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const res = await fetch('/api/data');
|
|
555
|
-
const data = await res.json();
|
|
556
|
-
|
|
557
|
-
allModules = data.modules;
|
|
558
|
-
|
|
559
|
-
document.getElementById('projectName').textContent = data.projectName;
|
|
560
|
-
document.getElementById('scannedAt').textContent = new Date(data.scannedAt).toLocaleTimeString();
|
|
561
|
-
|
|
562
|
-
const testCount = allModules.filter(m => m.type === 'test').length;
|
|
563
|
-
const withTests = allModules.filter(m => m.hasTests && m.type !== 'test').length;
|
|
564
|
-
const covModules = allModules.filter(m => m.coverage?.lines !== undefined);
|
|
565
|
-
const avgCov = covModules.length
|
|
566
|
-
? (covModules.reduce((s, m) => s + m.coverage.lines, 0) / covModules.length).toFixed(0) + '%'
|
|
567
|
-
: '—';
|
|
568
|
-
|
|
569
|
-
document.getElementById('statModules').textContent = allModules.length;
|
|
570
|
-
document.getElementById('statWithTests').textContent = withTests;
|
|
571
|
-
document.getElementById('statCoverage').textContent = avgCov;
|
|
572
|
-
document.getElementById('statTests').textContent = testCount;
|
|
573
|
-
|
|
574
|
-
document.getElementById('loading').style.display = 'none';
|
|
575
|
-
document.getElementById('moduleGrid').style.display = 'grid';
|
|
576
|
-
|
|
577
|
-
renderAll();
|
|
578
|
-
} catch (err) {
|
|
579
|
-
document.getElementById('loading').textContent = 'Failed to load data: ' + err.message;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
749
|
+
document.getElementById('panelClose').onclick = closePanel;
|
|
750
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); });
|
|
582
751
|
|
|
583
752
|
init();
|
|
584
753
|
</script>
|