@imenam/mcp-github 1.1.50 → 1.1.52
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/README.md +1 -0
- package/dist/config-server.d.ts.map +1 -1
- package/dist/config-server.js +66 -5
- package/dist/config-server.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +2085 -980
package/public/index.html
CHANGED
|
@@ -4,1267 +4,2372 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>GitHub MCP</title>
|
|
7
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
7
|
<style>
|
|
9
8
|
:root {
|
|
10
9
|
--bg: #0a0a0b;
|
|
10
|
+
--rail: #0f0f11;
|
|
11
11
|
--surface: #141416;
|
|
12
12
|
--surface-2: #1b1b1e;
|
|
13
|
+
--surface-3: #16161a;
|
|
13
14
|
--border: #2a2a2e;
|
|
14
|
-
--border-strong: #
|
|
15
|
+
--border-strong: #33333a;
|
|
15
16
|
--text: #ededef;
|
|
16
|
-
--
|
|
17
|
-
--
|
|
17
|
+
--dim: #9a9aa2;
|
|
18
|
+
--muted: #6b6b73;
|
|
19
|
+
--faint: #4a4a52;
|
|
18
20
|
--accent: #4d7cfe;
|
|
19
|
-
--accent-
|
|
21
|
+
--accent-soft: #6a92ff;
|
|
22
|
+
--accent-wash: rgba(77, 124, 254, 0.10);
|
|
20
23
|
--danger: #e0635e;
|
|
24
|
+
--danger-solid: #c9403b;
|
|
25
|
+
--danger-wash: rgba(224, 99, 94, 0.06);
|
|
26
|
+
--success: #3fb950;
|
|
27
|
+
--warn: #d29922;
|
|
21
28
|
--radius: 8px;
|
|
29
|
+
--radius-lg: 10px;
|
|
30
|
+
--radius-xl: 14px;
|
|
31
|
+
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Inter, sans-serif;
|
|
32
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
* {
|
|
25
|
-
|
|
26
|
-
}
|
|
35
|
+
* { box-sizing: border-box; -webkit-font-smoothing: antialiased; }
|
|
36
|
+
|
|
37
|
+
html, body { height: 100%; }
|
|
27
38
|
|
|
28
39
|
body {
|
|
40
|
+
margin: 0;
|
|
29
41
|
background-color: var(--bg);
|
|
30
42
|
color: var(--text);
|
|
31
|
-
font-family:
|
|
43
|
+
font-family: var(--sans);
|
|
44
|
+
font-size: 14px;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
button { font: inherit; cursor: pointer; }
|
|
48
|
+
input, select { font: inherit; }
|
|
49
|
+
[hidden] { display: none !important; }
|
|
50
|
+
|
|
51
|
+
.ic { width: 16px; height: 16px; flex-shrink: 0; }
|
|
52
|
+
.ic-lg { width: 18px; height: 18px; }
|
|
53
|
+
.ic-xl { width: 20px; height: 20px; }
|
|
54
|
+
|
|
55
|
+
/* ---------------------------------------------------------------- */
|
|
56
|
+
/* Barre d'application */
|
|
57
|
+
/* ---------------------------------------------------------------- */
|
|
58
|
+
.appbar {
|
|
59
|
+
position: sticky;
|
|
60
|
+
top: 0;
|
|
61
|
+
z-index: 30;
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 16px;
|
|
65
|
+
height: 56px;
|
|
66
|
+
padding: 0 24px;
|
|
67
|
+
background-color: var(--surface);
|
|
68
|
+
border-bottom: 1px solid var(--border);
|
|
41
69
|
}
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
.brand {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 10px;
|
|
75
|
+
font-size: 15px;
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
white-space: nowrap;
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
.switcher {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 8px;
|
|
84
|
+
height: 32px;
|
|
85
|
+
padding: 0 10px 0 14px;
|
|
86
|
+
background-color: var(--surface-2);
|
|
52
87
|
border: 1px solid var(--border);
|
|
53
88
|
border-radius: var(--radius);
|
|
89
|
+
color: var(--text);
|
|
90
|
+
max-width: 260px;
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
-
font-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
.switcher:hover { border-color: var(--border-strong); }
|
|
93
|
+
.switcher .prefix { color: var(--muted); font-size: 12px; }
|
|
94
|
+
.switcher .name {
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
font-weight: 500;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
text-overflow: ellipsis;
|
|
99
|
+
white-space: nowrap;
|
|
62
100
|
}
|
|
101
|
+
.switcher .ic { color: var(--muted); }
|
|
63
102
|
|
|
64
|
-
.
|
|
65
|
-
|
|
103
|
+
.status-pill {
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 8px;
|
|
107
|
+
height: 28px;
|
|
108
|
+
padding: 0 12px;
|
|
109
|
+
border-radius: 14px;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
font-weight: 500;
|
|
112
|
+
background-color: rgba(63, 185, 80, 0.10);
|
|
113
|
+
border: 1px solid rgba(63, 185, 80, 0.35);
|
|
114
|
+
color: var(--success);
|
|
115
|
+
}
|
|
116
|
+
.status-pill .dot {
|
|
117
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
118
|
+
background-color: currentColor;
|
|
66
119
|
}
|
|
120
|
+
.status-pill.is-down {
|
|
121
|
+
background-color: rgba(224, 99, 94, 0.10);
|
|
122
|
+
border-color: rgba(224, 99, 94, 0.35);
|
|
123
|
+
color: var(--danger);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.appbar-spacer { flex: 1; }
|
|
67
127
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
128
|
+
.dirty-note {
|
|
129
|
+
font-size: 12px;
|
|
130
|
+
color: var(--warn);
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
}
|
|
133
|
+
.dirty-note.is-clean { color: var(--muted); }
|
|
72
134
|
|
|
73
|
-
/*
|
|
135
|
+
/* ---------------------------------------------------------------- */
|
|
136
|
+
/* Boutons */
|
|
137
|
+
/* ---------------------------------------------------------------- */
|
|
74
138
|
.btn {
|
|
75
|
-
display: flex;
|
|
139
|
+
display: inline-flex;
|
|
76
140
|
align-items: center;
|
|
77
141
|
justify-content: center;
|
|
78
|
-
gap:
|
|
79
|
-
|
|
80
|
-
padding: 0
|
|
81
|
-
font-size: 0.875rem;
|
|
82
|
-
font-weight: 500;
|
|
83
|
-
border-radius: var(--radius);
|
|
142
|
+
gap: 8px;
|
|
143
|
+
height: 32px;
|
|
144
|
+
padding: 0 14px;
|
|
84
145
|
border: 1px solid transparent;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
cursor: not-allowed;
|
|
146
|
+
border-radius: var(--radius);
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
font-weight: 500;
|
|
149
|
+
white-space: nowrap;
|
|
150
|
+
transition: background-color .15s, border-color .15s, color .15s, opacity .15s;
|
|
91
151
|
}
|
|
152
|
+
.btn:disabled { opacity: .4; cursor: not-allowed; }
|
|
153
|
+
.btn-lg { height: 36px; padding: 0 16px; }
|
|
92
154
|
|
|
93
|
-
.btn-primary {
|
|
94
|
-
|
|
95
|
-
color: #fff;
|
|
96
|
-
}
|
|
97
|
-
.btn-primary:not(:disabled):hover { background-color: var(--accent-hover); }
|
|
155
|
+
.btn-primary { background-color: var(--accent); color: #fff; font-weight: 600; }
|
|
156
|
+
.btn-primary:not(:disabled):hover { background-color: var(--accent-soft); }
|
|
98
157
|
|
|
99
|
-
.btn-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
158
|
+
.btn-ghost { background-color: transparent; border-color: var(--border); color: var(--dim); }
|
|
159
|
+
.btn-ghost:not(:disabled):hover { background-color: var(--surface-2); border-color: var(--border-strong); color: var(--text); }
|
|
160
|
+
|
|
161
|
+
.btn-soft {
|
|
162
|
+
background-color: var(--accent-wash);
|
|
163
|
+
border-color: rgba(77, 124, 254, 0.45);
|
|
164
|
+
color: var(--accent-soft);
|
|
103
165
|
}
|
|
104
|
-
.btn-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
166
|
+
.btn-soft:not(:disabled):hover { background-color: rgba(77, 124, 254, 0.18); }
|
|
167
|
+
|
|
168
|
+
.btn-danger { background-color: var(--danger-solid); color: #fff; font-weight: 600; }
|
|
169
|
+
.btn-danger:not(:disabled):hover { background-color: #d64b46; }
|
|
170
|
+
|
|
171
|
+
/* ---------------------------------------------------------------- */
|
|
172
|
+
/* Ossature */
|
|
173
|
+
/* ---------------------------------------------------------------- */
|
|
174
|
+
.shell {
|
|
175
|
+
display: grid;
|
|
176
|
+
grid-template-columns: 260px minmax(0, 1fr);
|
|
177
|
+
min-height: calc(100vh - 56px);
|
|
108
178
|
}
|
|
109
179
|
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
180
|
+
.rail {
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
padding: 24px 12px 16px;
|
|
184
|
+
background-color: var(--rail);
|
|
185
|
+
border-right: 1px solid var(--border);
|
|
114
186
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
187
|
+
|
|
188
|
+
.rail-label {
|
|
189
|
+
padding: 0 12px;
|
|
190
|
+
margin-bottom: 8px;
|
|
191
|
+
font-size: 11px;
|
|
192
|
+
font-weight: 600;
|
|
193
|
+
letter-spacing: .08em;
|
|
194
|
+
text-transform: uppercase;
|
|
195
|
+
color: var(--muted);
|
|
118
196
|
}
|
|
119
197
|
|
|
120
|
-
.
|
|
121
|
-
|
|
198
|
+
.nav-item {
|
|
199
|
+
position: relative;
|
|
200
|
+
display: flex;
|
|
122
201
|
align-items: center;
|
|
123
|
-
|
|
124
|
-
width:
|
|
125
|
-
padding:
|
|
126
|
-
|
|
202
|
+
gap: 12px;
|
|
203
|
+
width: 100%;
|
|
204
|
+
padding: 10px 12px;
|
|
205
|
+
background-color: transparent;
|
|
206
|
+
border: none;
|
|
207
|
+
border-radius: var(--radius);
|
|
208
|
+
text-align: left;
|
|
209
|
+
color: var(--dim);
|
|
210
|
+
transition: background-color .15s;
|
|
127
211
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.
|
|
212
|
+
.nav-item:hover { background-color: rgba(255, 255, 255, 0.03); }
|
|
213
|
+
.nav-item .ic { color: var(--muted); }
|
|
214
|
+
.nav-item .nav-title { display: block; font-size: 13.5px; font-weight: 600; line-height: 18px; }
|
|
215
|
+
.nav-item .nav-summary {
|
|
131
216
|
display: block;
|
|
132
|
-
font-size:
|
|
133
|
-
|
|
134
|
-
color: var(--
|
|
135
|
-
|
|
217
|
+
font-size: 11.5px;
|
|
218
|
+
line-height: 16px;
|
|
219
|
+
color: var(--muted);
|
|
220
|
+
overflow: hidden;
|
|
221
|
+
text-overflow: ellipsis;
|
|
222
|
+
white-space: nowrap;
|
|
223
|
+
}
|
|
224
|
+
.nav-item.is-active { background-color: var(--accent-wash); }
|
|
225
|
+
.nav-item.is-active .nav-title { color: var(--text); }
|
|
226
|
+
.nav-item.is-active .nav-summary { color: var(--accent-soft); }
|
|
227
|
+
.nav-item.is-active .ic { color: var(--accent-soft); }
|
|
228
|
+
.nav-item.is-active::before {
|
|
229
|
+
content: "";
|
|
230
|
+
position: absolute;
|
|
231
|
+
left: 0; top: 10px; bottom: 10px;
|
|
232
|
+
width: 3px;
|
|
233
|
+
border-radius: 2px;
|
|
234
|
+
background-color: var(--accent);
|
|
235
|
+
}
|
|
236
|
+
.nav-item .nav-text { display: block; min-width: 0; }
|
|
237
|
+
|
|
238
|
+
/* Les libellés empilés vivent dans des <span> : sans display:block ils
|
|
239
|
+
se suivraient sur une seule ligne dès qu'ils tiennent en largeur. */
|
|
240
|
+
.item-text, .item-title, .item-subtitle,
|
|
241
|
+
.toast-text, .toast-title, .toast-detail,
|
|
242
|
+
.empty-text, .empty-title, .empty-help,
|
|
243
|
+
.repo-info, .pick-label { display: block; }
|
|
244
|
+
|
|
245
|
+
.rail-divider {
|
|
246
|
+
height: 1px;
|
|
247
|
+
margin: 16px 12px;
|
|
248
|
+
background-color: var(--border);
|
|
136
249
|
}
|
|
137
250
|
|
|
138
|
-
.
|
|
139
|
-
|
|
251
|
+
.rail-action {
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 12px;
|
|
140
255
|
width: 100%;
|
|
141
|
-
padding:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
border: 1px solid var(--border);
|
|
256
|
+
padding: 10px 12px;
|
|
257
|
+
background: transparent;
|
|
258
|
+
border: none;
|
|
145
259
|
border-radius: var(--radius);
|
|
146
260
|
color: var(--text);
|
|
147
|
-
|
|
148
|
-
|
|
261
|
+
font-size: 13px;
|
|
262
|
+
text-align: left;
|
|
149
263
|
}
|
|
150
|
-
.
|
|
151
|
-
.
|
|
152
|
-
.
|
|
153
|
-
.field-select { cursor: pointer; }
|
|
264
|
+
.rail-action .ic { color: var(--dim); }
|
|
265
|
+
.rail-action:not(:disabled):hover { background-color: rgba(255, 255, 255, 0.03); }
|
|
266
|
+
.rail-action:disabled { opacity: .4; cursor: not-allowed; }
|
|
154
267
|
|
|
155
|
-
|
|
156
|
-
|
|
268
|
+
.rail-spacer { flex: 1; min-height: 24px; }
|
|
269
|
+
|
|
270
|
+
.danger-zone {
|
|
271
|
+
padding: 12px 8px 8px;
|
|
272
|
+
background-color: var(--danger-wash);
|
|
273
|
+
border: 1px solid rgba(224, 99, 94, 0.22);
|
|
274
|
+
border-radius: var(--radius);
|
|
275
|
+
}
|
|
276
|
+
.danger-zone .zone-label {
|
|
157
277
|
display: flex;
|
|
158
278
|
align-items: center;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
279
|
+
gap: 8px;
|
|
280
|
+
padding: 0 8px 8px;
|
|
281
|
+
font-size: 11px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
letter-spacing: .08em;
|
|
284
|
+
text-transform: uppercase;
|
|
285
|
+
color: var(--danger);
|
|
162
286
|
}
|
|
163
|
-
.
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
|
|
287
|
+
.danger-zone .zone-label .ic { width: 14px; height: 14px; }
|
|
288
|
+
.danger-action {
|
|
289
|
+
display: block;
|
|
290
|
+
width: 100%;
|
|
291
|
+
padding: 8px;
|
|
292
|
+
background: transparent;
|
|
293
|
+
border: none;
|
|
294
|
+
border-radius: 6px;
|
|
295
|
+
text-align: left;
|
|
296
|
+
font-size: 12.5px;
|
|
297
|
+
color: var(--danger);
|
|
167
298
|
}
|
|
299
|
+
.danger-action:not(:disabled):hover { background-color: rgba(224, 99, 94, 0.10); }
|
|
300
|
+
.danger-action:disabled { opacity: .4; cursor: not-allowed; }
|
|
168
301
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
302
|
+
/* ---------------------------------------------------------------- */
|
|
303
|
+
/* Contenu */
|
|
304
|
+
/* ---------------------------------------------------------------- */
|
|
305
|
+
.content { padding: 32px 40px 64px; min-width: 0; }
|
|
306
|
+
|
|
307
|
+
.page-title { margin: 0; font-size: 21px; font-weight: 600; }
|
|
308
|
+
.page-subtitle { margin: 8px 0 0; font-size: 13px; color: var(--muted); }
|
|
309
|
+
|
|
310
|
+
.layout {
|
|
311
|
+
display: grid;
|
|
312
|
+
grid-template-columns: minmax(0, 1fr) 260px;
|
|
313
|
+
gap: 40px;
|
|
314
|
+
margin-top: 32px;
|
|
315
|
+
align-items: start;
|
|
316
|
+
}
|
|
317
|
+
.layout.is-single { grid-template-columns: minmax(0, 1fr); }
|
|
318
|
+
.col-main { display: flex; flex-direction: column; gap: 24px; min-width: 0; }
|
|
319
|
+
.col-side { display: flex; flex-direction: column; gap: 24px; }
|
|
320
|
+
|
|
321
|
+
.card {
|
|
322
|
+
background-color: var(--surface);
|
|
176
323
|
border: 1px solid var(--border);
|
|
177
|
-
border-radius: var(--radius);
|
|
324
|
+
border-radius: var(--radius-lg);
|
|
325
|
+
padding: 24px;
|
|
178
326
|
}
|
|
327
|
+
.card-title { margin: 0; font-size: 14px; font-weight: 600; }
|
|
328
|
+
.card-help { margin: 6px 0 0; font-size: 12px; color: var(--muted); }
|
|
179
329
|
|
|
180
|
-
|
|
181
|
-
|
|
330
|
+
.side-label {
|
|
331
|
+
font-size: 11px;
|
|
332
|
+
font-weight: 600;
|
|
333
|
+
letter-spacing: .08em;
|
|
334
|
+
text-transform: uppercase;
|
|
335
|
+
color: var(--muted);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Carte dépôt */
|
|
339
|
+
.repo-row { display: flex; align-items: center; gap: 16px; }
|
|
340
|
+
.repo-avatar {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
justify-content: center;
|
|
344
|
+
width: 56px; height: 56px;
|
|
345
|
+
flex-shrink: 0;
|
|
346
|
+
background-color: var(--surface-2);
|
|
182
347
|
border: 1px solid var(--border);
|
|
183
|
-
border-radius: var(--radius);
|
|
348
|
+
border-radius: var(--radius-lg);
|
|
349
|
+
color: var(--accent-soft);
|
|
350
|
+
}
|
|
351
|
+
.repo-avatar .ic { width: 26px; height: 26px; }
|
|
352
|
+
.repo-info { flex: 1; min-width: 0; }
|
|
353
|
+
.repo-name {
|
|
354
|
+
font-size: 16px;
|
|
355
|
+
font-weight: 600;
|
|
184
356
|
overflow: hidden;
|
|
357
|
+
text-overflow: ellipsis;
|
|
358
|
+
white-space: nowrap;
|
|
185
359
|
}
|
|
186
|
-
.
|
|
360
|
+
.repo-meta {
|
|
187
361
|
display: flex;
|
|
188
362
|
align-items: center;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
363
|
+
gap: 10px;
|
|
364
|
+
margin-top: 6px;
|
|
365
|
+
font-size: 12px;
|
|
366
|
+
color: var(--muted);
|
|
367
|
+
flex-wrap: wrap;
|
|
368
|
+
}
|
|
369
|
+
.chip-static {
|
|
370
|
+
display: inline-flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
gap: 6px;
|
|
373
|
+
height: 22px;
|
|
374
|
+
padding: 0 10px;
|
|
375
|
+
border-radius: 11px;
|
|
192
376
|
background-color: var(--surface-2);
|
|
193
|
-
border:
|
|
194
|
-
|
|
195
|
-
font-size:
|
|
196
|
-
font-weight:
|
|
197
|
-
|
|
377
|
+
border: 1px solid var(--border);
|
|
378
|
+
color: var(--dim);
|
|
379
|
+
font-size: 11px;
|
|
380
|
+
font-weight: 500;
|
|
381
|
+
}
|
|
382
|
+
.chip-static .ic { width: 12px; height: 12px; }
|
|
383
|
+
.lang-dot { width: 8px; height: 8px; border-radius: 50%; background-color: var(--dim); }
|
|
384
|
+
|
|
385
|
+
.empty-state { display: flex; align-items: center; gap: 16px; }
|
|
386
|
+
.empty-state .empty-text { flex: 1; }
|
|
387
|
+
.empty-state .empty-title { font-size: 14px; font-weight: 600; }
|
|
388
|
+
.empty-state .empty-help { margin-top: 4px; font-size: 12px; color: var(--muted); }
|
|
389
|
+
|
|
390
|
+
/* Flux de branches */
|
|
391
|
+
.branch-flow {
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: stretch;
|
|
394
|
+
gap: 16px;
|
|
395
|
+
margin-top: 20px;
|
|
396
|
+
}
|
|
397
|
+
.branch-arrow {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
color: var(--faint);
|
|
401
|
+
}
|
|
402
|
+
.branch-picker {
|
|
403
|
+
flex: 1;
|
|
404
|
+
min-width: 0;
|
|
405
|
+
display: block;
|
|
406
|
+
padding: 12px 14px;
|
|
407
|
+
background-color: var(--rail);
|
|
408
|
+
border: 1px solid var(--border);
|
|
409
|
+
border-radius: var(--radius);
|
|
410
|
+
text-align: left;
|
|
198
411
|
color: var(--text);
|
|
199
|
-
transition: background-color .15s
|
|
412
|
+
transition: border-color .15s, background-color .15s;
|
|
200
413
|
}
|
|
201
|
-
.
|
|
202
|
-
.
|
|
203
|
-
.
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
background-color: var(--surface-2);
|
|
207
|
-
font-size: 0.8125rem;
|
|
414
|
+
.branch-picker:not(:disabled):hover { border-color: var(--border-strong); }
|
|
415
|
+
.branch-picker:disabled { opacity: .5; cursor: not-allowed; }
|
|
416
|
+
.branch-picker.is-target { border-color: var(--accent); }
|
|
417
|
+
.branch-picker .pick-label {
|
|
418
|
+
font-size: 10.5px;
|
|
208
419
|
font-weight: 600;
|
|
209
|
-
letter-spacing:
|
|
210
|
-
|
|
420
|
+
letter-spacing: .08em;
|
|
421
|
+
text-transform: uppercase;
|
|
422
|
+
color: var(--muted);
|
|
211
423
|
}
|
|
212
|
-
.
|
|
424
|
+
.branch-picker.is-target .pick-label { color: var(--accent-soft); }
|
|
425
|
+
.branch-picker .pick-value {
|
|
426
|
+
display: flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
gap: 8px;
|
|
429
|
+
margin-top: 6px;
|
|
430
|
+
font-size: 14.5px;
|
|
431
|
+
font-weight: 500;
|
|
432
|
+
min-width: 0;
|
|
433
|
+
}
|
|
434
|
+
.branch-picker .pick-value .name {
|
|
435
|
+
overflow: hidden;
|
|
436
|
+
text-overflow: ellipsis;
|
|
437
|
+
white-space: nowrap;
|
|
438
|
+
}
|
|
439
|
+
.branch-picker .pick-value .ic { color: var(--dim); }
|
|
440
|
+
.branch-picker .pick-value .chevrons { margin-left: auto; color: var(--muted); }
|
|
441
|
+
.branch-picker .placeholder { color: var(--muted); font-weight: 400; }
|
|
213
442
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
display: inline-block;
|
|
218
|
-
width: 40px;
|
|
443
|
+
.badge-new {
|
|
444
|
+
display: inline-flex;
|
|
445
|
+
align-items: center;
|
|
219
446
|
height: 22px;
|
|
220
|
-
|
|
447
|
+
padding: 0 10px;
|
|
448
|
+
border-radius: 11px;
|
|
449
|
+
background-color: rgba(77, 124, 254, 0.14);
|
|
450
|
+
color: var(--accent-soft);
|
|
451
|
+
font-size: 11px;
|
|
452
|
+
font-weight: 500;
|
|
221
453
|
}
|
|
454
|
+
|
|
455
|
+
.flow-hint { margin: 16px 0 0; font-size: 12px; color: var(--muted); }
|
|
456
|
+
|
|
457
|
+
/* Lignes à bascule */
|
|
458
|
+
.toggle-row {
|
|
459
|
+
display: flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
gap: 20px;
|
|
462
|
+
padding: 16px 0;
|
|
463
|
+
}
|
|
464
|
+
.toggle-row + .toggle-row { border-top: 1px solid var(--border); }
|
|
465
|
+
.toggle-row:first-of-type { padding-top: 4px; }
|
|
466
|
+
.toggle-row .toggle-text { flex: 1; min-width: 0; }
|
|
467
|
+
.toggle-row .toggle-title { font-size: 13.5px; font-weight: 500; }
|
|
468
|
+
.toggle-row .toggle-help { margin-top: 4px; font-size: 12px; color: var(--muted); }
|
|
469
|
+
|
|
470
|
+
.switch { position: relative; display: inline-block; width: 40px; height: 22px; flex-shrink: 0; }
|
|
222
471
|
.switch input { opacity: 0; width: 0; height: 0; }
|
|
223
|
-
.slider {
|
|
472
|
+
.switch .slider {
|
|
224
473
|
position: absolute;
|
|
225
|
-
cursor: pointer;
|
|
226
474
|
inset: 0;
|
|
475
|
+
cursor: pointer;
|
|
227
476
|
background-color: var(--border-strong);
|
|
228
|
-
transition: .25s;
|
|
229
477
|
border-radius: 22px;
|
|
478
|
+
transition: background-color .2s;
|
|
230
479
|
}
|
|
231
|
-
.slider
|
|
232
|
-
position: absolute;
|
|
480
|
+
.switch .slider::before {
|
|
233
481
|
content: "";
|
|
234
|
-
|
|
235
|
-
width: 16px;
|
|
236
|
-
left: 3px;
|
|
237
|
-
bottom: 3px;
|
|
482
|
+
position: absolute;
|
|
483
|
+
height: 16px; width: 16px;
|
|
484
|
+
left: 3px; bottom: 3px;
|
|
238
485
|
background-color: #fff;
|
|
239
|
-
transition: .25s;
|
|
240
486
|
border-radius: 50%;
|
|
487
|
+
transition: transform .2s;
|
|
241
488
|
}
|
|
242
|
-
input:checked + .slider { background-color: var(--accent); }
|
|
243
|
-
input:checked + .slider
|
|
489
|
+
.switch input:checked + .slider { background-color: var(--accent); }
|
|
490
|
+
.switch input:checked + .slider::before { transform: translateX(18px); }
|
|
491
|
+
.switch input:focus-visible + .slider { outline: 2px solid var(--accent-soft); outline-offset: 2px; }
|
|
244
492
|
|
|
245
|
-
/*
|
|
246
|
-
.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
493
|
+
/* Sélecteur segmenté */
|
|
494
|
+
.segmented {
|
|
495
|
+
display: grid;
|
|
496
|
+
grid-template-columns: 1fr 1fr;
|
|
497
|
+
gap: 3px;
|
|
498
|
+
width: 440px;
|
|
499
|
+
max-width: 100%;
|
|
500
|
+
padding: 3px;
|
|
501
|
+
margin-top: 16px;
|
|
502
|
+
background-color: var(--rail);
|
|
253
503
|
border: 1px solid var(--border);
|
|
254
|
-
border-radius: var(--radius);
|
|
255
|
-
margin-top: 0.25rem;
|
|
256
|
-
max-height: 15rem;
|
|
257
|
-
overflow-y: auto;
|
|
258
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
504
|
+
border-radius: var(--radius-lg);
|
|
259
505
|
}
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
506
|
+
.segmented button {
|
|
507
|
+
display: flex;
|
|
508
|
+
align-items: center;
|
|
509
|
+
justify-content: center;
|
|
510
|
+
gap: 8px;
|
|
511
|
+
height: 34px;
|
|
512
|
+
background: transparent;
|
|
513
|
+
border: none;
|
|
514
|
+
border-radius: 8px;
|
|
515
|
+
font-size: 13px;
|
|
516
|
+
font-weight: 500;
|
|
517
|
+
color: var(--muted);
|
|
518
|
+
transition: background-color .15s, color .15s;
|
|
266
519
|
}
|
|
267
|
-
.
|
|
268
|
-
|
|
520
|
+
.segmented button:hover { color: var(--dim); }
|
|
521
|
+
.segmented button.is-active { background-color: var(--border); color: var(--text); }
|
|
522
|
+
|
|
523
|
+
/* Champs */
|
|
524
|
+
.field-grid { display: flex; gap: 16px; flex-wrap: wrap; }
|
|
525
|
+
.field { min-width: 0; }
|
|
526
|
+
.field-label {
|
|
527
|
+
display: block;
|
|
528
|
+
margin-bottom: 6px;
|
|
529
|
+
font-size: 10.5px;
|
|
530
|
+
font-weight: 600;
|
|
531
|
+
letter-spacing: .08em;
|
|
532
|
+
text-transform: uppercase;
|
|
533
|
+
color: var(--muted);
|
|
534
|
+
}
|
|
535
|
+
.input {
|
|
536
|
+
width: 100%;
|
|
537
|
+
height: 36px;
|
|
538
|
+
padding: 0 14px;
|
|
539
|
+
background-color: var(--rail);
|
|
540
|
+
border: 1px solid var(--border);
|
|
541
|
+
border-radius: var(--radius);
|
|
269
542
|
color: var(--text);
|
|
543
|
+
outline: none;
|
|
544
|
+
transition: border-color .15s;
|
|
270
545
|
}
|
|
271
|
-
.
|
|
272
|
-
|
|
546
|
+
.input::placeholder { color: var(--faint); }
|
|
547
|
+
.input:focus { border-color: var(--accent); }
|
|
548
|
+
.input.is-mono { font-family: var(--mono); font-size: 13.5px; }
|
|
549
|
+
|
|
550
|
+
.input-with-button { display: flex; gap: 8px; }
|
|
551
|
+
.input-with-button .input { flex: 1; }
|
|
552
|
+
|
|
553
|
+
.test-row {
|
|
554
|
+
display: flex;
|
|
555
|
+
align-items: center;
|
|
556
|
+
gap: 16px;
|
|
557
|
+
margin-top: 20px;
|
|
558
|
+
flex-wrap: wrap;
|
|
559
|
+
}
|
|
560
|
+
.test-result {
|
|
561
|
+
display: inline-flex;
|
|
562
|
+
align-items: center;
|
|
563
|
+
gap: 8px;
|
|
564
|
+
font-size: 12.5px;
|
|
565
|
+
}
|
|
566
|
+
.test-result.is-ok { color: var(--success); }
|
|
567
|
+
.test-result.is-ko { color: var(--danger); }
|
|
568
|
+
.test-result.is-pending { color: var(--dim); }
|
|
569
|
+
|
|
570
|
+
.cmd-preview {
|
|
571
|
+
margin: 16px 0 0;
|
|
572
|
+
font-family: var(--mono);
|
|
573
|
+
font-size: 11.5px;
|
|
574
|
+
line-height: 18px;
|
|
575
|
+
color: var(--muted);
|
|
576
|
+
word-break: break-all;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* Résumé */
|
|
580
|
+
.summary-row { padding: 14px 0; }
|
|
581
|
+
.summary-row + .summary-row { border-top: 1px solid var(--border); }
|
|
582
|
+
.summary-row:first-of-type { padding-top: 8px; }
|
|
583
|
+
.summary-row .sum-label { font-size: 11.5px; color: var(--muted); }
|
|
584
|
+
.summary-row .sum-value {
|
|
585
|
+
margin-top: 4px;
|
|
586
|
+
font-size: 13px;
|
|
587
|
+
overflow-wrap: anywhere;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.notice {
|
|
591
|
+
display: flex;
|
|
592
|
+
align-items: center;
|
|
593
|
+
gap: 10px;
|
|
594
|
+
margin-top: 16px;
|
|
595
|
+
padding: 9px 12px;
|
|
596
|
+
border-radius: var(--radius);
|
|
597
|
+
font-size: 12px;
|
|
598
|
+
font-weight: 500;
|
|
599
|
+
background-color: rgba(210, 153, 34, 0.08);
|
|
600
|
+
border: 1px solid rgba(210, 153, 34, 0.30);
|
|
601
|
+
color: var(--warn);
|
|
602
|
+
}
|
|
603
|
+
.notice .ic { width: 14px; height: 14px; }
|
|
604
|
+
|
|
605
|
+
/* Journal d'activité */
|
|
606
|
+
.log-entry {
|
|
607
|
+
display: flex;
|
|
608
|
+
align-items: center;
|
|
609
|
+
gap: 12px;
|
|
610
|
+
padding: 14px 0;
|
|
611
|
+
font-size: 13px;
|
|
612
|
+
}
|
|
613
|
+
.log-entry + .log-entry { border-top: 1px solid var(--border); }
|
|
614
|
+
.log-entry:first-of-type { padding-top: 8px; }
|
|
615
|
+
.log-entry .log-label { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
616
|
+
.log-entry .log-time { font-size: 11.5px; color: var(--muted); white-space: nowrap; }
|
|
617
|
+
.log-entry.is-ok .ic { color: var(--success); }
|
|
618
|
+
.log-entry.is-ko .ic { color: var(--danger); }
|
|
619
|
+
.log-entry.is-info .ic { color: var(--accent-soft); }
|
|
620
|
+
.log-entry.is-neutral .ic { color: var(--dim); }
|
|
621
|
+
.log-empty { padding: 8px 0; font-size: 12.5px; color: var(--muted); }
|
|
622
|
+
|
|
623
|
+
/* ---------------------------------------------------------------- */
|
|
624
|
+
/* Palette et modale */
|
|
625
|
+
/* ---------------------------------------------------------------- */
|
|
626
|
+
.overlay {
|
|
627
|
+
position: fixed;
|
|
628
|
+
inset: 0;
|
|
629
|
+
z-index: 60;
|
|
630
|
+
display: flex;
|
|
631
|
+
justify-content: center;
|
|
632
|
+
background-color: rgba(4, 4, 6, 0.74);
|
|
633
|
+
}
|
|
634
|
+
.overlay.overlay-top { align-items: flex-start; padding-top: 120px; }
|
|
635
|
+
.overlay.overlay-center { align-items: center; }
|
|
636
|
+
|
|
637
|
+
.palette {
|
|
638
|
+
display: flex;
|
|
639
|
+
flex-direction: column;
|
|
640
|
+
width: 680px;
|
|
641
|
+
max-width: calc(100vw - 48px);
|
|
642
|
+
max-height: 560px;
|
|
643
|
+
background-color: var(--surface-3);
|
|
644
|
+
border: 1px solid var(--border-strong);
|
|
645
|
+
border-radius: var(--radius-xl);
|
|
646
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, .6);
|
|
647
|
+
overflow: hidden;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.palette-search {
|
|
651
|
+
display: flex;
|
|
652
|
+
align-items: center;
|
|
653
|
+
gap: 12px;
|
|
654
|
+
padding: 0 24px;
|
|
655
|
+
height: 64px;
|
|
656
|
+
border-bottom: 1px solid var(--border);
|
|
657
|
+
}
|
|
658
|
+
.palette-search .ic { color: var(--muted); }
|
|
659
|
+
.palette-search input {
|
|
660
|
+
flex: 1;
|
|
661
|
+
background: transparent;
|
|
662
|
+
border: none;
|
|
663
|
+
outline: none;
|
|
273
664
|
color: var(--text);
|
|
665
|
+
font-size: 16px;
|
|
274
666
|
}
|
|
275
|
-
.search
|
|
276
|
-
|
|
277
|
-
|
|
667
|
+
.palette-search input::placeholder { color: var(--faint); }
|
|
668
|
+
.kbd {
|
|
669
|
+
padding: 4px 8px;
|
|
670
|
+
background-color: #1f1f24;
|
|
671
|
+
border: 1px solid var(--border-strong);
|
|
672
|
+
border-radius: 6px;
|
|
673
|
+
font-size: 11px;
|
|
674
|
+
color: var(--muted);
|
|
278
675
|
}
|
|
279
676
|
|
|
280
|
-
|
|
281
|
-
|
|
677
|
+
.palette-filters {
|
|
678
|
+
display: flex;
|
|
679
|
+
align-items: center;
|
|
680
|
+
gap: 8px;
|
|
681
|
+
padding: 14px 24px;
|
|
682
|
+
border-bottom: 1px solid var(--border);
|
|
683
|
+
}
|
|
684
|
+
.chip {
|
|
685
|
+
height: 28px;
|
|
686
|
+
padding: 0 14px;
|
|
687
|
+
background: transparent;
|
|
688
|
+
border: 1px solid var(--border-strong);
|
|
689
|
+
border-radius: 14px;
|
|
690
|
+
color: var(--dim);
|
|
691
|
+
font-size: 12px;
|
|
692
|
+
}
|
|
693
|
+
.chip:hover { color: var(--text); }
|
|
694
|
+
.chip.is-on { background-color: var(--border); border-color: var(--border); color: var(--text); font-weight: 500; }
|
|
695
|
+
.palette-count { margin-left: auto; font-size: 12px; color: var(--muted); }
|
|
696
|
+
|
|
697
|
+
.palette-list { flex: 1; overflow-y: auto; padding: 12px; }
|
|
698
|
+
.palette-group-label {
|
|
699
|
+
padding: 8px 12px 6px;
|
|
700
|
+
font-size: 10.5px;
|
|
701
|
+
font-weight: 600;
|
|
702
|
+
letter-spacing: .08em;
|
|
703
|
+
text-transform: uppercase;
|
|
704
|
+
color: var(--muted);
|
|
705
|
+
}
|
|
706
|
+
.palette-item {
|
|
707
|
+
display: flex;
|
|
708
|
+
align-items: center;
|
|
709
|
+
gap: 14px;
|
|
710
|
+
width: 100%;
|
|
711
|
+
padding: 10px 12px;
|
|
712
|
+
background: transparent;
|
|
713
|
+
border: none;
|
|
714
|
+
border-radius: var(--radius-lg);
|
|
715
|
+
text-align: left;
|
|
716
|
+
color: var(--text);
|
|
717
|
+
}
|
|
718
|
+
.palette-item .item-icon {
|
|
719
|
+
display: flex;
|
|
720
|
+
align-items: center;
|
|
721
|
+
justify-content: center;
|
|
722
|
+
width: 32px; height: 32px;
|
|
723
|
+
flex-shrink: 0;
|
|
724
|
+
background-color: #1f1f24;
|
|
725
|
+
border-radius: var(--radius);
|
|
726
|
+
color: var(--muted);
|
|
727
|
+
}
|
|
728
|
+
.palette-item .item-text { flex: 1; min-width: 0; }
|
|
729
|
+
.palette-item .item-title {
|
|
730
|
+
font-size: 14px;
|
|
731
|
+
font-weight: 500;
|
|
732
|
+
overflow: hidden;
|
|
733
|
+
text-overflow: ellipsis;
|
|
734
|
+
white-space: nowrap;
|
|
735
|
+
}
|
|
736
|
+
.palette-item .item-subtitle {
|
|
737
|
+
margin-top: 2px;
|
|
738
|
+
font-size: 11.5px;
|
|
739
|
+
color: var(--muted);
|
|
740
|
+
overflow: hidden;
|
|
741
|
+
text-overflow: ellipsis;
|
|
742
|
+
white-space: nowrap;
|
|
743
|
+
}
|
|
744
|
+
.palette-item .item-badge {
|
|
282
745
|
display: inline-flex;
|
|
283
746
|
align-items: center;
|
|
284
|
-
|
|
285
|
-
|
|
747
|
+
height: 24px;
|
|
748
|
+
padding: 0 12px;
|
|
749
|
+
border-radius: 12px;
|
|
750
|
+
font-size: 11px;
|
|
751
|
+
background-color: #1f1f24;
|
|
752
|
+
border: 1px solid var(--border-strong);
|
|
753
|
+
color: var(--dim);
|
|
754
|
+
}
|
|
755
|
+
.palette-item.is-current .item-icon { color: var(--accent-soft); }
|
|
756
|
+
.palette-item.is-current .item-badge {
|
|
757
|
+
background-color: rgba(77, 124, 254, 0.16);
|
|
758
|
+
border-color: transparent;
|
|
759
|
+
color: var(--accent-soft);
|
|
286
760
|
font-weight: 500;
|
|
287
|
-
|
|
288
|
-
|
|
761
|
+
}
|
|
762
|
+
.palette-item.is-active { background-color: var(--accent-wash); }
|
|
763
|
+
.palette-item.is-create .item-icon { color: var(--accent-soft); }
|
|
764
|
+
|
|
765
|
+
.palette-empty { padding: 32px 12px; text-align: center; font-size: 13px; color: var(--muted); }
|
|
766
|
+
|
|
767
|
+
.palette-footer {
|
|
768
|
+
display: flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
gap: 16px;
|
|
771
|
+
padding: 16px 24px;
|
|
772
|
+
border-top: 1px solid var(--border);
|
|
773
|
+
font-size: 11.5px;
|
|
774
|
+
color: var(--muted);
|
|
775
|
+
}
|
|
776
|
+
.palette-footer .right { margin-left: auto; }
|
|
777
|
+
|
|
778
|
+
.modal {
|
|
779
|
+
width: 540px;
|
|
780
|
+
max-width: calc(100vw - 48px);
|
|
781
|
+
padding: 28px;
|
|
782
|
+
background-color: var(--surface-3);
|
|
783
|
+
border: 1px solid var(--border-strong);
|
|
784
|
+
border-radius: var(--radius-xl);
|
|
785
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, .6);
|
|
786
|
+
}
|
|
787
|
+
.modal-head { display: flex; align-items: center; gap: 14px; }
|
|
788
|
+
.modal-icon {
|
|
789
|
+
display: flex;
|
|
790
|
+
align-items: center;
|
|
791
|
+
justify-content: center;
|
|
792
|
+
width: 40px; height: 40px;
|
|
793
|
+
flex-shrink: 0;
|
|
794
|
+
border-radius: 20px;
|
|
795
|
+
background-color: var(--accent-wash);
|
|
796
|
+
color: var(--accent-soft);
|
|
797
|
+
}
|
|
798
|
+
.modal-icon.is-danger { background-color: rgba(224, 99, 94, 0.12); color: var(--danger); }
|
|
799
|
+
.modal-title { font-size: 16px; font-weight: 600; }
|
|
800
|
+
.modal-body { margin-top: 16px; font-size: 13px; line-height: 20px; color: var(--dim); }
|
|
801
|
+
.modal-path {
|
|
802
|
+
margin-top: 16px;
|
|
803
|
+
padding: 10px 14px;
|
|
804
|
+
background-color: var(--bg);
|
|
289
805
|
border: 1px solid var(--border);
|
|
290
|
-
border-radius:
|
|
806
|
+
border-radius: var(--radius);
|
|
807
|
+
font-family: var(--mono);
|
|
808
|
+
font-size: 12.5px;
|
|
809
|
+
overflow-wrap: anywhere;
|
|
810
|
+
}
|
|
811
|
+
.modal-field { margin-top: 20px; }
|
|
812
|
+
.modal-field label { display: block; margin-bottom: 6px; font-size: 11.5px; color: var(--muted); }
|
|
813
|
+
.modal-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; }
|
|
814
|
+
|
|
815
|
+
/* ---------------------------------------------------------------- */
|
|
816
|
+
/* Toasts */
|
|
817
|
+
/* ---------------------------------------------------------------- */
|
|
818
|
+
#toasts {
|
|
819
|
+
position: fixed;
|
|
820
|
+
right: 24px;
|
|
821
|
+
bottom: 24px;
|
|
822
|
+
z-index: 80;
|
|
823
|
+
display: flex;
|
|
824
|
+
flex-direction: column;
|
|
825
|
+
gap: 12px;
|
|
826
|
+
max-width: calc(100vw - 48px);
|
|
827
|
+
}
|
|
828
|
+
.toast {
|
|
829
|
+
display: flex;
|
|
830
|
+
align-items: center;
|
|
831
|
+
gap: 14px;
|
|
832
|
+
width: 440px;
|
|
833
|
+
max-width: 100%;
|
|
834
|
+
padding: 12px 20px;
|
|
835
|
+
background-color: var(--surface-2);
|
|
836
|
+
border: 1px solid var(--border-strong);
|
|
837
|
+
border-radius: var(--radius-lg);
|
|
838
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, .5);
|
|
839
|
+
animation: slide-in .22s ease-out;
|
|
840
|
+
}
|
|
841
|
+
.toast.is-error { border-color: rgba(224, 99, 94, 0.40); }
|
|
842
|
+
.toast .toast-text { flex: 1; min-width: 0; }
|
|
843
|
+
.toast .toast-title { font-size: 13px; font-weight: 600; }
|
|
844
|
+
.toast .toast-detail { margin-top: 3px; font-size: 11.5px; color: var(--muted); }
|
|
845
|
+
.toast .toast-action {
|
|
846
|
+
background: none;
|
|
847
|
+
border: none;
|
|
848
|
+
padding: 0;
|
|
849
|
+
font-size: 12.5px;
|
|
850
|
+
font-weight: 600;
|
|
851
|
+
color: var(--accent-soft);
|
|
852
|
+
white-space: nowrap;
|
|
853
|
+
}
|
|
854
|
+
.toast .toast-close { background: none; border: none; padding: 0; color: var(--muted); display: flex; }
|
|
855
|
+
.toast.is-ok .toast-icon { color: var(--success); }
|
|
856
|
+
.toast.is-error .toast-icon { color: var(--danger); }
|
|
857
|
+
.toast.is-info .toast-icon { color: var(--accent-soft); }
|
|
858
|
+
|
|
859
|
+
@keyframes slide-in {
|
|
860
|
+
from { transform: translateX(110%); opacity: 0; }
|
|
861
|
+
to { transform: translateX(0); opacity: 1; }
|
|
291
862
|
}
|
|
292
863
|
|
|
293
|
-
/* Spinner */
|
|
294
864
|
.spinner {
|
|
295
865
|
display: inline-block;
|
|
296
|
-
width:
|
|
297
|
-
height: 1rem;
|
|
866
|
+
width: 16px; height: 16px;
|
|
298
867
|
border: 2px solid var(--border-strong);
|
|
299
868
|
border-top-color: var(--accent);
|
|
300
869
|
border-radius: 50%;
|
|
301
|
-
animation: spin
|
|
870
|
+
animation: spin .7s linear infinite;
|
|
871
|
+
flex-shrink: 0;
|
|
302
872
|
}
|
|
303
873
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
304
874
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
padding: 0.7rem 0.9rem;
|
|
311
|
-
font-size: 0.875rem;
|
|
312
|
-
background-color: var(--surface-2);
|
|
313
|
-
border: 1px solid var(--border);
|
|
314
|
-
border-left-width: 3px;
|
|
315
|
-
border-radius: var(--radius);
|
|
316
|
-
color: var(--text);
|
|
317
|
-
max-width: 22rem;
|
|
318
|
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
319
|
-
animation: slideIn 0.25s ease-out forwards, fadeOut 0.3s ease-in forwards 4.7s;
|
|
875
|
+
.skeleton {
|
|
876
|
+
background: linear-gradient(90deg, var(--surface-2) 25%, #232328 37%, var(--surface-2) 63%);
|
|
877
|
+
background-size: 400% 100%;
|
|
878
|
+
border-radius: 6px;
|
|
879
|
+
animation: shimmer 1.4s ease infinite;
|
|
320
880
|
}
|
|
321
|
-
|
|
322
|
-
.toast-error { border-left-color: var(--danger); }
|
|
881
|
+
@keyframes shimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }
|
|
323
882
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
883
|
+
/* ---------------------------------------------------------------- */
|
|
884
|
+
/* Adaptatif */
|
|
885
|
+
/* ---------------------------------------------------------------- */
|
|
886
|
+
@media (max-width: 1180px) {
|
|
887
|
+
.layout { grid-template-columns: minmax(0, 1fr); gap: 24px; }
|
|
327
888
|
}
|
|
328
|
-
@
|
|
329
|
-
|
|
330
|
-
|
|
889
|
+
@media (max-width: 900px) {
|
|
890
|
+
.shell { grid-template-columns: minmax(0, 1fr); }
|
|
891
|
+
.rail { flex-direction: row; flex-wrap: wrap; align-items: center; border-right: none; border-bottom: 1px solid var(--border); padding: 12px; }
|
|
892
|
+
.rail-label, .rail-divider, .rail-spacer, .danger-zone .zone-label { display: none; }
|
|
893
|
+
.nav-item { width: auto; }
|
|
894
|
+
.nav-item .nav-summary { display: none; }
|
|
895
|
+
.danger-zone { display: flex; gap: 4px; padding: 4px; border: none; background: none; }
|
|
896
|
+
.content { padding: 24px 16px 48px; }
|
|
897
|
+
.appbar { padding: 0 16px; gap: 10px; }
|
|
898
|
+
.brand span, .dirty-note, .switcher .prefix { display: none; }
|
|
899
|
+
.segmented { width: 100%; }
|
|
331
900
|
}
|
|
332
901
|
</style>
|
|
333
902
|
</head>
|
|
334
|
-
<body
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
903
|
+
<body>
|
|
904
|
+
<header class="appbar">
|
|
905
|
+
<div class="brand">
|
|
906
|
+
<svg class="ic ic-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg>
|
|
907
|
+
<span>GitHub MCP</span>
|
|
908
|
+
</div>
|
|
909
|
+
|
|
910
|
+
<button type="button" class="switcher" id="configSwitcher" title="Changer de configuration">
|
|
911
|
+
<span class="prefix">Config</span>
|
|
912
|
+
<span class="name" id="configName">—</span>
|
|
913
|
+
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path></svg>
|
|
914
|
+
</button>
|
|
915
|
+
|
|
916
|
+
<span class="status-pill" id="statusPill">
|
|
917
|
+
<span class="dot"></span>
|
|
918
|
+
<span id="statusLabel">Serveur actif</span>
|
|
919
|
+
</span>
|
|
920
|
+
|
|
921
|
+
<span id="agentBadge" class="chip-static" hidden></span>
|
|
922
|
+
|
|
923
|
+
<div class="appbar-spacer"></div>
|
|
924
|
+
|
|
925
|
+
<span class="dirty-note is-clean" id="dirtyNote">Aucune modification en attente</span>
|
|
926
|
+
<button type="button" class="btn btn-ghost" id="btnDiscard" disabled>Annuler</button>
|
|
927
|
+
<button type="button" class="btn btn-primary" id="btnSave" disabled>Enregistrer</button>
|
|
928
|
+
</header>
|
|
929
|
+
|
|
930
|
+
<div class="shell">
|
|
931
|
+
<nav class="rail">
|
|
932
|
+
<div class="rail-label">Configuration</div>
|
|
933
|
+
|
|
934
|
+
<button type="button" class="nav-item is-active" data-panel="repo">
|
|
935
|
+
<svg class="ic ic-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg>
|
|
936
|
+
<span class="nav-text">
|
|
937
|
+
<span class="nav-title">Dépôt & branches</span>
|
|
938
|
+
<span class="nav-summary" id="navSummaryRepo">—</span>
|
|
939
|
+
</span>
|
|
940
|
+
</button>
|
|
941
|
+
|
|
942
|
+
<button type="button" class="nav-item" data-panel="options">
|
|
943
|
+
<svg class="ic ic-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" y1="4" x2="14" y2="4"></line><line x1="10" y1="4" x2="3" y2="4"></line><line x1="21" y1="12" x2="12" y2="12"></line><line x1="8" y1="12" x2="3" y2="12"></line><line x1="21" y1="20" x2="16" y2="20"></line><line x1="12" y1="20" x2="3" y2="20"></line><line x1="14" y1="2" x2="14" y2="6"></line><line x1="8" y1="10" x2="8" y2="14"></line><line x1="16" y1="18" x2="16" y2="22"></line></svg>
|
|
944
|
+
<span class="nav-text">
|
|
945
|
+
<span class="nav-title">Options</span>
|
|
946
|
+
<span class="nav-summary" id="navSummaryOptions">—</span>
|
|
947
|
+
</span>
|
|
948
|
+
</button>
|
|
949
|
+
|
|
950
|
+
<button type="button" class="nav-item" data-panel="deploy">
|
|
951
|
+
<svg class="ic ic-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2"></rect><rect x="2" y="14" width="20" height="8" rx="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
|
|
952
|
+
<span class="nav-text">
|
|
953
|
+
<span class="nav-title">Déploiement & test</span>
|
|
954
|
+
<span class="nav-summary" id="navSummaryDeploy">—</span>
|
|
955
|
+
</span>
|
|
956
|
+
</button>
|
|
957
|
+
|
|
958
|
+
<div class="rail-divider"></div>
|
|
959
|
+
<div class="rail-label">Actions</div>
|
|
960
|
+
|
|
961
|
+
<button type="button" class="rail-action" id="btnClone">
|
|
962
|
+
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
|
963
|
+
<span id="btnCloneLabel">Cloner le dépôt</span>
|
|
964
|
+
</button>
|
|
965
|
+
|
|
966
|
+
<button type="button" class="rail-action" id="btnRestart">
|
|
967
|
+
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"></path><polyline points="21 3 21 9 15 9"></polyline></svg>
|
|
968
|
+
<span id="btnRestartLabel">Redémarrer le serveur</span>
|
|
969
|
+
</button>
|
|
970
|
+
|
|
971
|
+
<div class="rail-spacer"></div>
|
|
972
|
+
|
|
973
|
+
<div class="danger-zone">
|
|
974
|
+
<div class="zone-label">
|
|
975
|
+
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
|
|
976
|
+
<span>Zone sensible</span>
|
|
356
977
|
</div>
|
|
357
|
-
|
|
358
|
-
<
|
|
359
|
-
<h3 class="section-label mb-3">Actions</h3>
|
|
360
|
-
|
|
361
|
-
<div class="space-y-2.5">
|
|
362
|
-
<button type="button" onclick="document.getElementById('configForm').requestSubmit()" class="btn btn-primary">
|
|
363
|
-
Sauvegarder
|
|
364
|
-
</button>
|
|
365
|
-
|
|
366
|
-
<button type="button" id="cloneBtn" class="btn btn-secondary">
|
|
367
|
-
Cloner le dépôt
|
|
368
|
-
</button>
|
|
369
|
-
|
|
370
|
-
<button type="button" id="restartBtn" class="btn btn-secondary">
|
|
371
|
-
Redémarrer
|
|
372
|
-
</button>
|
|
373
|
-
|
|
374
|
-
<button type="button" id="deleteWorkingDirBtn" class="btn btn-danger">
|
|
375
|
-
Nettoyer le repo
|
|
376
|
-
</button>
|
|
377
|
-
|
|
378
|
-
<button type="button" id="stopBtn" class="btn btn-danger">
|
|
379
|
-
Arrêter le serveur
|
|
380
|
-
</button>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
</div>
|
|
384
|
-
</aside>
|
|
385
|
-
|
|
386
|
-
<!-- Main Content -->
|
|
387
|
-
<main class="card p-7">
|
|
388
|
-
<div class="flex items-center gap-3 mb-7">
|
|
389
|
-
<h1 class="text-xl font-semibold text-main flex items-center gap-2.5">
|
|
390
|
-
<svg class="w-5 h-5 text-dim" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
391
|
-
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"></path>
|
|
392
|
-
</svg>
|
|
393
|
-
GitHub MCP
|
|
394
|
-
</h1>
|
|
395
|
-
<span id="agentNameBadge" class="badge hidden"></span>
|
|
978
|
+
<button type="button" class="danger-action" id="btnClean">Nettoyer le dossier local</button>
|
|
979
|
+
<button type="button" class="danger-action" id="btnStop">Arrêter le serveur</button>
|
|
396
980
|
</div>
|
|
981
|
+
</nav>
|
|
397
982
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
<div class="spinner"></div>
|
|
403
|
-
</div>
|
|
404
|
-
</div>
|
|
405
|
-
</form>
|
|
406
|
-
|
|
407
|
-
<div class="mt-8 divider pt-4 text-xs text-muted">
|
|
408
|
-
Le serveur MCP doit être redémarré pour appliquer les changements.
|
|
409
|
-
</div>
|
|
983
|
+
<main class="content">
|
|
984
|
+
<section id="panel-repo"></section>
|
|
985
|
+
<section id="panel-options" hidden></section>
|
|
986
|
+
<section id="panel-deploy" hidden></section>
|
|
410
987
|
</main>
|
|
411
988
|
</div>
|
|
412
989
|
|
|
413
|
-
|
|
414
|
-
<div id="
|
|
990
|
+
<div id="paletteRoot"></div>
|
|
991
|
+
<div id="modalRoot"></div>
|
|
992
|
+
<div id="toasts"></div>
|
|
415
993
|
|
|
416
994
|
<script>
|
|
417
|
-
|
|
995
|
+
(function () {
|
|
996
|
+
"use strict";
|
|
997
|
+
|
|
998
|
+
/* =============================================================== */
|
|
999
|
+
/* Utilitaires */
|
|
1000
|
+
/* =============================================================== */
|
|
1001
|
+
|
|
418
1002
|
const pathname = window.location.pathname;
|
|
419
1003
|
const BASE_PATH = pathname.includes('.')
|
|
420
|
-
? pathname.replace(/\/[^/]*$/, '')
|
|
1004
|
+
? pathname.replace(/\/[^/]*$/, '')
|
|
421
1005
|
: pathname.endsWith('/')
|
|
422
|
-
? pathname.slice(0, -1)
|
|
423
|
-
: pathname === '/' ? '' : pathname;
|
|
1006
|
+
? pathname.slice(0, -1)
|
|
1007
|
+
: pathname === '/' ? '' : pathname;
|
|
424
1008
|
|
|
425
|
-
// Helper pour construire les URLs d'API
|
|
426
1009
|
const api = (endpoint) => `${BASE_PATH}/${endpoint}`.replace(/\/+/g, '/');
|
|
427
1010
|
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1011
|
+
const $ = (id) => document.getElementById(id);
|
|
1012
|
+
|
|
1013
|
+
const esc = (value) => String(value == null ? '' : value)
|
|
1014
|
+
.replace(/&/g, '&')
|
|
1015
|
+
.replace(/</g, '<')
|
|
1016
|
+
.replace(/>/g, '>')
|
|
1017
|
+
.replace(/"/g, '"');
|
|
1018
|
+
|
|
1019
|
+
const ICONS = {
|
|
1020
|
+
branch: '<line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path>',
|
|
1021
|
+
book: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>',
|
|
1022
|
+
chevrons: '<path d="m7 15 5 5 5-5"></path><path d="m7 9 5-5 5 5"></path>',
|
|
1023
|
+
arrowRight: '<line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline>',
|
|
1024
|
+
search: '<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>',
|
|
1025
|
+
plus: '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>',
|
|
1026
|
+
check: '<circle cx="12" cy="12" r="10"></circle><path d="m9 12 2 2 4-4"></path>',
|
|
1027
|
+
cross: '<circle cx="12" cy="12" r="10"></circle><path d="m15 9-6 6"></path><path d="m9 9 6 6"></path>',
|
|
1028
|
+
alert: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line>',
|
|
1029
|
+
info: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line>',
|
|
1030
|
+
refresh: '<path d="M21 12a9 9 0 1 1-2.64-6.36"></path><polyline points="21 3 21 9 15 9"></polyline>',
|
|
1031
|
+
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line>',
|
|
1032
|
+
server: '<rect x="2" y="2" width="20" height="8" rx="2"></rect><rect x="2" y="14" width="20" height="8" rx="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line>',
|
|
1033
|
+
drive: '<line x1="22" y1="12" x2="2" y2="12"></line><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path><line x1="6" y1="16" x2="6.01" y2="16"></line>',
|
|
1034
|
+
plug: '<path d="M12 22v-5"></path><path d="M9 8V2"></path><path d="M15 8V2"></path><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"></path>',
|
|
1035
|
+
lock: '<rect x="3" y="11" width="18" height="11" rx="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path>',
|
|
1036
|
+
globe: '<circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>',
|
|
1037
|
+
folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"></path>',
|
|
1038
|
+
sliders: '<line x1="21" y1="4" x2="14" y2="4"></line><line x1="10" y1="4" x2="3" y2="4"></line><line x1="21" y1="12" x2="12" y2="12"></line><line x1="8" y1="12" x2="3" y2="12"></line><line x1="21" y1="20" x2="16" y2="20"></line><line x1="12" y1="20" x2="3" y2="20"></line><line x1="14" y1="2" x2="14" y2="6"></line><line x1="8" y1="10" x2="8" y2="14"></line><line x1="16" y1="18" x2="16" y2="22"></line>',
|
|
1039
|
+
x: '<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>'
|
|
1040
|
+
};
|
|
453
1041
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
{
|
|
470
|
-
|
|
471
|
-
title: 'Déploiement & test',
|
|
472
|
-
collapsible: true,
|
|
473
|
-
fields: ['LOCAL_DEPLOY', 'SSH_HOST', 'SSH_USER', 'SSH_PORT', 'SSH_KEY_PATH', 'SSH_REMOTE_PATH', 'TEST_COMMAND']
|
|
1042
|
+
const icon = (name, cls) => `<svg class="ic ${cls || ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${ICONS[name] || ''}</svg>`;
|
|
1043
|
+
|
|
1044
|
+
function timeAgo(value) {
|
|
1045
|
+
if (!value) return '';
|
|
1046
|
+
const then = new Date(value).getTime();
|
|
1047
|
+
if (Number.isNaN(then)) return '';
|
|
1048
|
+
const seconds = Math.max(1, Math.round((Date.now() - then) / 1000));
|
|
1049
|
+
const steps = [
|
|
1050
|
+
[60, 'à l\u2019instant', 1],
|
|
1051
|
+
[3600, 'min', 60],
|
|
1052
|
+
[86400, 'h', 3600],
|
|
1053
|
+
[2592000, 'j', 86400],
|
|
1054
|
+
[31536000, 'mois', 2592000]
|
|
1055
|
+
];
|
|
1056
|
+
if (seconds < 60) return 'à l\u2019instant';
|
|
1057
|
+
for (const [limit, unit, divisor] of steps) {
|
|
1058
|
+
if (seconds < limit) return `il y a ${Math.round(seconds / divisor)} ${unit}`;
|
|
474
1059
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// État déplié/replié des sections repliables (false = replié par défaut)
|
|
478
|
-
const sectionState = { options: false, deploy: false };
|
|
1060
|
+
return `il y a ${Math.round(seconds / 31536000)} an(s)`;
|
|
1061
|
+
}
|
|
479
1062
|
|
|
480
|
-
|
|
481
|
-
|
|
1063
|
+
const isTrue = (value) => value === true || value === 'true';
|
|
1064
|
+
|
|
1065
|
+
/* =============================================================== */
|
|
1066
|
+
/* État */
|
|
1067
|
+
/* =============================================================== */
|
|
1068
|
+
|
|
1069
|
+
const FIELDS = [
|
|
1070
|
+
{ id: 'TARGET_REPO', label: 'Dépôt', type: 'text' },
|
|
1071
|
+
{ id: 'BASE_BRANCH', label: 'Branche de base', type: 'text' },
|
|
1072
|
+
{ id: 'TARGET_BRANCH', label: 'Branche de travail', type: 'text' },
|
|
1073
|
+
{ id: 'LOCAL_DEPLOY', label: 'Cible de déploiement', type: 'bool' },
|
|
1074
|
+
{ id: 'SSH_HOST', label: 'Hôte SSH', type: 'text' },
|
|
1075
|
+
{ id: 'SSH_USER', label: 'Utilisateur SSH', type: 'text' },
|
|
1076
|
+
{ id: 'SSH_PORT', label: 'Port SSH', type: 'text' },
|
|
1077
|
+
{ id: 'SSH_KEY_PATH', label: 'Clé privée SSH', type: 'text' },
|
|
1078
|
+
{ id: 'SSH_REMOTE_PATH', label: 'Chemin du projet', type: 'text' },
|
|
1079
|
+
{ id: 'TEST_COMMAND', label: 'Commande de test', type: 'text' },
|
|
1080
|
+
{ id: 'READ_ONLY', label: 'Mode lecture seule', type: 'bool' },
|
|
1081
|
+
{ id: 'INCLUDE_PUBLIC', label: 'Dépôts publics', type: 'bool' },
|
|
1082
|
+
{ id: 'INCLUDE_NOT_OWNED', label: 'Dépôts d\u2019autres propriétaires', type: 'bool' }
|
|
1083
|
+
];
|
|
482
1084
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1085
|
+
const state = {
|
|
1086
|
+
panel: 'repo',
|
|
1087
|
+
config: {},
|
|
1088
|
+
saved: {},
|
|
1089
|
+
extras: {},
|
|
1090
|
+
configNames: [],
|
|
1091
|
+
currentConfigName: '',
|
|
488
1092
|
repos: [],
|
|
489
|
-
|
|
1093
|
+
repoMeta: {},
|
|
490
1094
|
loadingRepos: false,
|
|
491
|
-
|
|
1095
|
+
branches: {},
|
|
1096
|
+
loadingBranches: {},
|
|
1097
|
+
sshTest: null,
|
|
1098
|
+
activity: [],
|
|
1099
|
+
serverOk: true
|
|
492
1100
|
};
|
|
493
1101
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1102
|
+
/* =============================================================== */
|
|
1103
|
+
/* Toasts */
|
|
1104
|
+
/* =============================================================== */
|
|
1105
|
+
|
|
1106
|
+
function toast(options) {
|
|
1107
|
+
const { type = 'info', title, detail, actionLabel, onAction, sticky } = options;
|
|
1108
|
+
const iconName = type === 'error' ? 'cross' : type === 'success' ? 'check' : 'info';
|
|
1109
|
+
const cls = type === 'error' ? 'is-error' : type === 'success' ? 'is-ok' : 'is-info';
|
|
1110
|
+
|
|
1111
|
+
const node = document.createElement('div');
|
|
1112
|
+
node.className = `toast ${cls}`;
|
|
1113
|
+
node.innerHTML = `
|
|
1114
|
+
<span class="toast-icon">${icon(iconName, 'ic-lg')}</span>
|
|
1115
|
+
<span class="toast-text">
|
|
1116
|
+
<span class="toast-title">${esc(title)}</span>
|
|
1117
|
+
${detail ? `<span class="toast-detail">${esc(detail)}</span>` : ''}
|
|
1118
|
+
</span>
|
|
1119
|
+
${actionLabel ? `<button type="button" class="toast-action">${esc(actionLabel)}</button>` : ''}
|
|
1120
|
+
<button type="button" class="toast-close" aria-label="Fermer">${icon('x')}</button>
|
|
1121
|
+
`;
|
|
501
1122
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
configNames = data.names || [];
|
|
508
|
-
currentConfigName = data.current || '';
|
|
509
|
-
renderConfigSelector();
|
|
510
|
-
}
|
|
511
|
-
} catch (error) {
|
|
512
|
-
console.warn('Error fetching configs:', error);
|
|
1123
|
+
const dismiss = () => node.remove();
|
|
1124
|
+
node.querySelector('.toast-close').addEventListener('click', dismiss);
|
|
1125
|
+
const actionBtn = node.querySelector('.toast-action');
|
|
1126
|
+
if (actionBtn) {
|
|
1127
|
+
actionBtn.addEventListener('click', () => { dismiss(); if (onAction) onAction(); });
|
|
513
1128
|
}
|
|
1129
|
+
|
|
1130
|
+
$('toasts').appendChild(node);
|
|
1131
|
+
if (!sticky && type !== 'error') setTimeout(dismiss, 6000);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function logActivity(tone, label) {
|
|
1135
|
+
state.activity.unshift({ tone, label, at: Date.now() });
|
|
1136
|
+
if (state.activity.length > 20) state.activity.length = 20;
|
|
1137
|
+
if (state.panel === 'deploy') renderActivity();
|
|
514
1138
|
}
|
|
515
1139
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1140
|
+
/* =============================================================== */
|
|
1141
|
+
/* Modale générique */
|
|
1142
|
+
/* =============================================================== */
|
|
1143
|
+
|
|
1144
|
+
function openModal(options) {
|
|
1145
|
+
const {
|
|
1146
|
+
title, body, pathText, input, confirmWord,
|
|
1147
|
+
confirmLabel = 'Confirmer', cancelLabel = 'Annuler', variant = 'default'
|
|
1148
|
+
} = options;
|
|
1149
|
+
|
|
1150
|
+
return new Promise((resolve) => {
|
|
1151
|
+
const root = $('modalRoot');
|
|
1152
|
+
root.innerHTML = `
|
|
1153
|
+
<div class="overlay overlay-center" data-overlay>
|
|
1154
|
+
<div class="modal" role="dialog" aria-modal="true">
|
|
1155
|
+
<div class="modal-head">
|
|
1156
|
+
<span class="modal-icon ${variant === 'danger' ? 'is-danger' : ''}">
|
|
1157
|
+
${icon(variant === 'danger' ? 'alert' : 'info', 'ic-xl')}
|
|
1158
|
+
</span>
|
|
1159
|
+
<span class="modal-title">${esc(title)}</span>
|
|
1160
|
+
</div>
|
|
1161
|
+
${body ? `<p class="modal-body">${esc(body)}</p>` : ''}
|
|
1162
|
+
${pathText ? `<div class="modal-path">${esc(pathText)}</div>` : ''}
|
|
1163
|
+
${input ? `
|
|
1164
|
+
<div class="modal-field">
|
|
1165
|
+
<label for="modalInput">${esc(input.label)}</label>
|
|
1166
|
+
<input class="input ${input.mono ? 'is-mono' : ''}" id="modalInput"
|
|
1167
|
+
placeholder="${esc(input.placeholder || '')}"
|
|
1168
|
+
value="${esc(input.value || '')}" autocomplete="off">
|
|
1169
|
+
</div>` : ''}
|
|
1170
|
+
${confirmWord ? `
|
|
1171
|
+
<div class="modal-field">
|
|
1172
|
+
<label for="modalConfirmWord">Tapez « ${esc(confirmWord)} » pour confirmer</label>
|
|
1173
|
+
<input class="input is-mono" id="modalConfirmWord"
|
|
1174
|
+
placeholder="${esc(confirmWord)}" autocomplete="off">
|
|
1175
|
+
</div>` : ''}
|
|
1176
|
+
<div class="modal-actions">
|
|
1177
|
+
<button type="button" class="btn btn-ghost btn-lg" id="modalCancel">${esc(cancelLabel)}</button>
|
|
1178
|
+
<button type="button" class="btn btn-lg ${variant === 'danger' ? 'btn-danger' : 'btn-primary'}" id="modalConfirm">${esc(confirmLabel)}</button>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
`;
|
|
1183
|
+
|
|
1184
|
+
const confirmBtn = $('modalConfirm');
|
|
1185
|
+
const wordInput = $('modalConfirmWord');
|
|
1186
|
+
const valueInput = $('modalInput');
|
|
521
1187
|
|
|
522
|
-
|
|
1188
|
+
const validate = () => {
|
|
1189
|
+
let ok = true;
|
|
1190
|
+
if (confirmWord) ok = wordInput.value.trim() === confirmWord;
|
|
1191
|
+
if (input && input.required !== false) ok = ok && valueInput.value.trim().length > 0;
|
|
1192
|
+
confirmBtn.disabled = !ok;
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
const close = (result) => {
|
|
1196
|
+
document.removeEventListener('keydown', onKey);
|
|
1197
|
+
root.innerHTML = '';
|
|
1198
|
+
resolve(result);
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const onKey = (event) => {
|
|
1202
|
+
if (event.key === 'Escape') close(null);
|
|
1203
|
+
if (event.key === 'Enter' && !confirmBtn.disabled) {
|
|
1204
|
+
event.preventDefault();
|
|
1205
|
+
close(valueInput ? valueInput.value.trim() : true);
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
if (wordInput) wordInput.addEventListener('input', validate);
|
|
1210
|
+
if (valueInput) valueInput.addEventListener('input', validate);
|
|
1211
|
+
validate();
|
|
1212
|
+
|
|
1213
|
+
$('modalCancel').addEventListener('click', () => close(null));
|
|
1214
|
+
confirmBtn.addEventListener('click', () => close(valueInput ? valueInput.value.trim() : true));
|
|
1215
|
+
root.querySelector('[data-overlay]').addEventListener('mousedown', (event) => {
|
|
1216
|
+
if (event.target.hasAttribute('data-overlay')) close(null);
|
|
1217
|
+
});
|
|
1218
|
+
document.addEventListener('keydown', onKey);
|
|
1219
|
+
|
|
1220
|
+
setTimeout(() => (valueInput || wordInput || confirmBtn).focus(), 0);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/* =============================================================== */
|
|
1225
|
+
/* Palette */
|
|
1226
|
+
/* =============================================================== */
|
|
1227
|
+
|
|
1228
|
+
let palette = null;
|
|
1229
|
+
|
|
1230
|
+
function openPalette(options) {
|
|
1231
|
+
closePalette();
|
|
1232
|
+
palette = {
|
|
1233
|
+
placeholder: options.placeholder || 'Rechercher…',
|
|
1234
|
+
groupLabel: options.groupLabel || '',
|
|
1235
|
+
footerHint: options.footerHint || '',
|
|
1236
|
+
emptyLabel: options.emptyLabel || 'Aucun résultat',
|
|
1237
|
+
filters: options.filters || null,
|
|
1238
|
+
onPick: options.onPick,
|
|
1239
|
+
createLabel: options.createLabel || null,
|
|
1240
|
+
validateCreate: options.validateCreate || null,
|
|
1241
|
+
source: options.source,
|
|
1242
|
+
remote: !!options.remote,
|
|
1243
|
+
items: [],
|
|
1244
|
+
active: 0,
|
|
1245
|
+
query: '',
|
|
1246
|
+
loading: false,
|
|
1247
|
+
timer: null
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const root = $('paletteRoot');
|
|
1251
|
+
root.innerHTML = `
|
|
1252
|
+
<div class="overlay overlay-top" data-overlay>
|
|
1253
|
+
<div class="palette" role="dialog" aria-modal="true">
|
|
1254
|
+
<div class="palette-search">
|
|
1255
|
+
${icon('search', 'ic-xl')}
|
|
1256
|
+
<input id="paletteInput" placeholder="${esc(palette.placeholder)}" autocomplete="off" spellcheck="false">
|
|
1257
|
+
<span class="spinner" id="paletteSpinner" hidden></span>
|
|
1258
|
+
<span class="kbd">esc</span>
|
|
1259
|
+
</div>
|
|
1260
|
+
<div class="palette-filters" id="paletteFilters" ${palette.filters ? '' : 'hidden'}></div>
|
|
1261
|
+
<div class="palette-list" id="paletteList"></div>
|
|
1262
|
+
<div class="palette-footer">
|
|
1263
|
+
<span>↑↓ naviguer ↵ sélectionner esc fermer</span>
|
|
1264
|
+
<span class="right">${esc(palette.footerHint)}</span>
|
|
1265
|
+
</div>
|
|
1266
|
+
</div>
|
|
1267
|
+
</div>
|
|
1268
|
+
`;
|
|
1269
|
+
|
|
1270
|
+
const input = $('paletteInput');
|
|
1271
|
+
input.addEventListener('input', onPaletteInput);
|
|
1272
|
+
input.addEventListener('keydown', onPaletteKey);
|
|
1273
|
+
root.querySelector('[data-overlay]').addEventListener('mousedown', (event) => {
|
|
1274
|
+
if (event.target.hasAttribute('data-overlay')) closePalette();
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
renderPaletteFilters();
|
|
1278
|
+
refreshPalette('');
|
|
1279
|
+
setTimeout(() => input.focus(), 0);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function closePalette() {
|
|
1283
|
+
if (palette && palette.timer) clearTimeout(palette.timer);
|
|
1284
|
+
palette = null;
|
|
1285
|
+
$('paletteRoot').innerHTML = '';
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function renderPaletteFilters() {
|
|
1289
|
+
if (!palette || !palette.filters) return;
|
|
1290
|
+
const container = $('paletteFilters');
|
|
1291
|
+
container.innerHTML = palette.filters.map((filter, index) =>
|
|
1292
|
+
`<button type="button" class="chip ${filter.on ? 'is-on' : ''}" data-filter="${index}">${esc(filter.label)}</button>`
|
|
1293
|
+
).join('') + `<span class="palette-count" id="paletteCount"></span>`;
|
|
1294
|
+
|
|
1295
|
+
container.querySelectorAll('[data-filter]').forEach((chip) => {
|
|
1296
|
+
chip.addEventListener('click', async () => {
|
|
1297
|
+
const filter = palette.filters[Number(chip.dataset.filter)];
|
|
1298
|
+
filter.on = !filter.on;
|
|
1299
|
+
renderPaletteFilters();
|
|
1300
|
+
await filter.onToggle(filter.on);
|
|
1301
|
+
refreshPalette(palette.query, true);
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function onPaletteInput(event) {
|
|
1307
|
+
if (!palette) return;
|
|
1308
|
+
palette.query = event.target.value;
|
|
1309
|
+
if (palette.remote) {
|
|
1310
|
+
if (palette.timer) clearTimeout(palette.timer);
|
|
1311
|
+
palette.timer = setTimeout(() => refreshPalette(palette.query, true), 400);
|
|
1312
|
+
renderPaletteList();
|
|
1313
|
+
} else {
|
|
1314
|
+
refreshPalette(palette.query);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function onPaletteKey(event) {
|
|
1319
|
+
if (!palette) return;
|
|
1320
|
+
const count = palette.rendered ? palette.rendered.length : 0;
|
|
1321
|
+
if (event.key === 'Escape') { closePalette(); return; }
|
|
1322
|
+
if (!count) return;
|
|
1323
|
+
|
|
1324
|
+
if (event.key === 'ArrowDown') {
|
|
1325
|
+
event.preventDefault();
|
|
1326
|
+
palette.active = Math.min(palette.active + 1, count - 1);
|
|
1327
|
+
renderPaletteList(true);
|
|
1328
|
+
} else if (event.key === 'ArrowUp') {
|
|
1329
|
+
event.preventDefault();
|
|
1330
|
+
palette.active = Math.max(palette.active - 1, 0);
|
|
1331
|
+
renderPaletteList(true);
|
|
1332
|
+
} else if (event.key === 'Enter') {
|
|
1333
|
+
event.preventDefault();
|
|
1334
|
+
pickPaletteItem(palette.active);
|
|
1335
|
+
}
|
|
523
1336
|
}
|
|
524
1337
|
|
|
525
|
-
async function
|
|
526
|
-
if (!
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1338
|
+
async function refreshPalette(query, force) {
|
|
1339
|
+
if (!palette) return;
|
|
1340
|
+
// La palette peut être fermée pendant l'attente réseau : on garde
|
|
1341
|
+
// la référence ouverte au départ et on abandonne si elle a changé.
|
|
1342
|
+
const owner = palette;
|
|
1343
|
+
|
|
1344
|
+
if (owner.remote) {
|
|
1345
|
+
owner.loading = true;
|
|
1346
|
+
renderPaletteList();
|
|
1347
|
+
const spinner = $('paletteSpinner');
|
|
1348
|
+
if (spinner) spinner.hidden = false;
|
|
1349
|
+
|
|
1350
|
+
let items = [];
|
|
1351
|
+
let failure = null;
|
|
1352
|
+
try {
|
|
1353
|
+
items = await owner.source(query);
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
failure = error;
|
|
539
1356
|
}
|
|
540
|
-
|
|
541
|
-
|
|
1357
|
+
|
|
1358
|
+
if (palette !== owner) return;
|
|
1359
|
+
owner.loading = false;
|
|
1360
|
+
owner.items = items;
|
|
1361
|
+
const spinnerAfter = $('paletteSpinner');
|
|
1362
|
+
if (spinnerAfter) spinnerAfter.hidden = true;
|
|
1363
|
+
if (failure) toast({ type: 'error', title: 'Recherche impossible', detail: failure.message });
|
|
1364
|
+
} else if (force || !owner.items.length) {
|
|
1365
|
+
const items = await owner.source(query);
|
|
1366
|
+
if (palette !== owner) return;
|
|
1367
|
+
owner.items = items;
|
|
542
1368
|
}
|
|
1369
|
+
|
|
1370
|
+
owner.active = 0;
|
|
1371
|
+
renderPaletteList();
|
|
543
1372
|
}
|
|
544
1373
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1374
|
+
function renderPaletteList(scrollOnly) {
|
|
1375
|
+
if (!palette) return;
|
|
1376
|
+
const list = $('paletteList');
|
|
1377
|
+
if (!list) return;
|
|
548
1378
|
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1379
|
+
const query = palette.query.trim();
|
|
1380
|
+
let items = palette.items;
|
|
1381
|
+
if (!palette.remote && query) {
|
|
1382
|
+
const needle = query.toLowerCase();
|
|
1383
|
+
items = items.filter((item) => item.title.toLowerCase().includes(needle));
|
|
553
1384
|
}
|
|
554
1385
|
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
TEST_COMMAND: '',
|
|
566
|
-
READ_ONLY: currentConfig.READ_ONLY || false,
|
|
567
|
-
INCLUDE_PUBLIC: currentConfig.INCLUDE_PUBLIC || false,
|
|
568
|
-
INCLUDE_NOT_OWNED: currentConfig.INCLUDE_NOT_OWNED || false
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
try {
|
|
572
|
-
const response = await fetch(api('api/configs'), {
|
|
573
|
-
method: 'POST',
|
|
574
|
-
headers: { 'Content-Type': 'application/json' },
|
|
575
|
-
body: JSON.stringify({ name: trimmedName, config: newConfig })
|
|
1386
|
+
const rendered = items.slice();
|
|
1387
|
+
const exact = items.some((item) => item.title === query);
|
|
1388
|
+
if (palette.createLabel && query && !exact && (!palette.validateCreate || palette.validateCreate(query))) {
|
|
1389
|
+
rendered.unshift({
|
|
1390
|
+
id: query,
|
|
1391
|
+
title: palette.createLabel.replace('%s', query),
|
|
1392
|
+
subtitle: 'Sera créée au premier commit',
|
|
1393
|
+
iconName: 'plus',
|
|
1394
|
+
isCreate: true,
|
|
1395
|
+
value: query
|
|
576
1396
|
});
|
|
1397
|
+
}
|
|
1398
|
+
palette.rendered = rendered;
|
|
577
1399
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1400
|
+
const count = $('paletteCount');
|
|
1401
|
+
if (count) count.textContent = palette.loading ? 'recherche…' : `${items.length} résultat${items.length > 1 ? 's' : ''}`;
|
|
1402
|
+
|
|
1403
|
+
if (scrollOnly && list.children.length) {
|
|
1404
|
+
list.querySelectorAll('.palette-item').forEach((node, index) => {
|
|
1405
|
+
node.classList.toggle('is-active', index === palette.active);
|
|
1406
|
+
if (index === palette.active) node.scrollIntoView({ block: 'nearest' });
|
|
1407
|
+
});
|
|
1408
|
+
return;
|
|
587
1409
|
}
|
|
588
|
-
}
|
|
589
1410
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1411
|
+
if (palette.loading && !rendered.length) {
|
|
1412
|
+
list.innerHTML = `
|
|
1413
|
+
<div style="padding:12px">
|
|
1414
|
+
<div class="skeleton" style="height:52px;margin-bottom:8px"></div>
|
|
1415
|
+
<div class="skeleton" style="height:52px;margin-bottom:8px"></div>
|
|
1416
|
+
<div class="skeleton" style="height:52px"></div>
|
|
1417
|
+
</div>`;
|
|
594
1418
|
return;
|
|
595
1419
|
}
|
|
596
1420
|
|
|
597
|
-
if (!
|
|
1421
|
+
if (!rendered.length) {
|
|
1422
|
+
list.innerHTML = `<div class="palette-empty">${esc(palette.emptyLabel)}</div>`;
|
|
598
1423
|
return;
|
|
599
1424
|
}
|
|
600
1425
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1426
|
+
list.innerHTML = (palette.groupLabel ? `<div class="palette-group-label">${esc(palette.groupLabel)}</div>` : '')
|
|
1427
|
+
+ rendered.map((item, index) => `
|
|
1428
|
+
<button type="button" class="palette-item ${index === palette.active ? 'is-active' : ''} ${item.current ? 'is-current' : ''} ${item.isCreate ? 'is-create' : ''}" data-index="${index}">
|
|
1429
|
+
<span class="item-icon">${icon(item.iconName || 'book', 'ic-lg')}</span>
|
|
1430
|
+
<span class="item-text">
|
|
1431
|
+
<span class="item-title">${esc(item.title)}</span>
|
|
1432
|
+
${item.subtitle ? `<span class="item-subtitle">${esc(item.subtitle)}</span>` : ''}
|
|
1433
|
+
</span>
|
|
1434
|
+
${item.badge ? `<span class="item-badge">${esc(item.badge)}</span>` : ''}
|
|
1435
|
+
</button>
|
|
1436
|
+
`).join('');
|
|
605
1437
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
1438
|
+
list.querySelectorAll('.palette-item').forEach((node) => {
|
|
1439
|
+
node.addEventListener('click', () => pickPaletteItem(Number(node.dataset.index)));
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function pickPaletteItem(index) {
|
|
1444
|
+
if (!palette || !palette.rendered) return;
|
|
1445
|
+
const item = palette.rendered[index];
|
|
1446
|
+
if (!item) return;
|
|
1447
|
+
const handler = palette.onPick;
|
|
1448
|
+
closePalette();
|
|
1449
|
+
handler(item.value != null ? item.value : item.id, item);
|
|
619
1450
|
}
|
|
620
1451
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
1452
|
+
/* =============================================================== */
|
|
1453
|
+
/* Réseau */
|
|
1454
|
+
/* =============================================================== */
|
|
1455
|
+
|
|
1456
|
+
async function request(endpoint, options) {
|
|
1457
|
+
const response = await fetch(api(endpoint), options);
|
|
1458
|
+
if (!response.ok) {
|
|
1459
|
+
const data = await response.json().catch(() => ({}));
|
|
1460
|
+
throw new Error(data.error || `Erreur ${response.status}`);
|
|
624
1461
|
}
|
|
625
|
-
|
|
1462
|
+
const text = await response.text();
|
|
1463
|
+
return text ? JSON.parse(text) : null;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
async function loadConfigs() {
|
|
1467
|
+
const data = await request('api/configs');
|
|
1468
|
+
state.configNames = data.names || [];
|
|
1469
|
+
state.currentConfigName = data.current || '';
|
|
1470
|
+
}
|
|
626
1471
|
|
|
627
|
-
|
|
628
|
-
|
|
1472
|
+
async function loadConfig() {
|
|
1473
|
+
const data = await request('api/config');
|
|
1474
|
+
const config = {};
|
|
1475
|
+
FIELDS.forEach((field) => {
|
|
1476
|
+
config[field.id] = field.type === 'bool' ? isTrue(data[field.id]) : (data[field.id] || '');
|
|
1477
|
+
});
|
|
1478
|
+
state.config = config;
|
|
1479
|
+
state.saved = Object.assign({}, config);
|
|
1480
|
+
state.extras = {
|
|
1481
|
+
AGENT_NAME: data.AGENT_NAME || '',
|
|
1482
|
+
CLONE_DIR: data.CLONE_DIR || '',
|
|
1483
|
+
REPO_PATH: data.REPO_PATH || ''
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
629
1486
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1487
|
+
function normalizeRepo(entry) {
|
|
1488
|
+
if (typeof entry === 'string') return { full_name: entry };
|
|
1489
|
+
return entry || {};
|
|
1490
|
+
}
|
|
634
1491
|
|
|
1492
|
+
async function fetchRepositories(search) {
|
|
1493
|
+
state.loadingRepos = true;
|
|
635
1494
|
try {
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
} catch (error) {
|
|
643
|
-
console.warn('Error fetching repositories:', error);
|
|
1495
|
+
const query = search ? `?search=${encodeURIComponent(search)}` : '';
|
|
1496
|
+
const data = await request(`api/repositories${query}`);
|
|
1497
|
+
const repos = (data || []).map(normalizeRepo).filter((repo) => repo.full_name);
|
|
1498
|
+
repos.forEach((repo) => { state.repoMeta[repo.full_name] = repo; });
|
|
1499
|
+
state.repos = repos;
|
|
1500
|
+
return repos;
|
|
644
1501
|
} finally {
|
|
645
|
-
|
|
1502
|
+
state.loadingRepos = false;
|
|
646
1503
|
}
|
|
647
1504
|
}
|
|
648
1505
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1506
|
+
async function fetchBranches(repo) {
|
|
1507
|
+
if (!repo) return [];
|
|
1508
|
+
state.loadingBranches[repo] = true;
|
|
1509
|
+
try {
|
|
1510
|
+
const data = await request(`api/branches?repo=${encodeURIComponent(repo)}`);
|
|
1511
|
+
state.branches[repo] = data || [];
|
|
1512
|
+
return state.branches[repo];
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
state.branches[repo] = [];
|
|
1515
|
+
toast({ type: 'error', title: 'Branches indisponibles', detail: error.message });
|
|
1516
|
+
return [];
|
|
1517
|
+
} finally {
|
|
1518
|
+
state.loadingBranches[repo] = false;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
653
1521
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
</div>
|
|
661
|
-
`).join('');
|
|
662
|
-
resultsContainer.classList.remove('hidden');
|
|
663
|
-
activeIndex = -1;
|
|
664
|
-
} else {
|
|
665
|
-
resultsContainer.innerHTML = '<div class="search-result-item">Aucun résultat</div>';
|
|
666
|
-
resultsContainer.classList.remove('hidden');
|
|
1522
|
+
async function checkHealth() {
|
|
1523
|
+
try {
|
|
1524
|
+
await request('proxy/health');
|
|
1525
|
+
state.serverOk = true;
|
|
1526
|
+
} catch (_) {
|
|
1527
|
+
state.serverOk = false;
|
|
667
1528
|
}
|
|
1529
|
+
renderStatus();
|
|
668
1530
|
}
|
|
669
1531
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const hiddenInput = document.getElementById('TARGET_REPO');
|
|
674
|
-
const resultsContainer = document.getElementById('REPO_RESULTS');
|
|
1532
|
+
/* =============================================================== */
|
|
1533
|
+
/* État « modifié » */
|
|
1534
|
+
/* =============================================================== */
|
|
675
1535
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1536
|
+
function changedFields() {
|
|
1537
|
+
return FIELDS.filter((field) => {
|
|
1538
|
+
const current = state.config[field.id];
|
|
1539
|
+
const saved = state.saved[field.id];
|
|
1540
|
+
if (field.type === 'bool') return !!current !== !!saved;
|
|
1541
|
+
return (current || '') !== (saved || '');
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
682
1544
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1545
|
+
function setField(id, value) {
|
|
1546
|
+
state.config[id] = value;
|
|
1547
|
+
refreshDerived();
|
|
1548
|
+
}
|
|
686
1549
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1550
|
+
/* =============================================================== */
|
|
1551
|
+
/* Rendu — parties persistantes */
|
|
1552
|
+
/* =============================================================== */
|
|
690
1553
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1554
|
+
function renderStatus() {
|
|
1555
|
+
const pill = $('statusPill');
|
|
1556
|
+
pill.classList.toggle('is-down', !state.serverOk);
|
|
1557
|
+
$('statusLabel').textContent = state.serverOk ? 'Serveur actif' : 'Serveur injoignable';
|
|
1558
|
+
}
|
|
694
1559
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
renderInputs(currentConfig);
|
|
1560
|
+
function renderChrome() {
|
|
1561
|
+
$('configName').textContent = state.currentConfigName || 'Aucune configuration';
|
|
698
1562
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1563
|
+
const badge = $('agentBadge');
|
|
1564
|
+
if (state.extras.AGENT_NAME) {
|
|
1565
|
+
badge.textContent = state.extras.AGENT_NAME;
|
|
1566
|
+
badge.hidden = false;
|
|
1567
|
+
} else {
|
|
1568
|
+
badge.hidden = true;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function refreshDerived() {
|
|
1573
|
+
const changed = changedFields();
|
|
1574
|
+
const note = $('dirtyNote');
|
|
1575
|
+
if (changed.length) {
|
|
1576
|
+
note.textContent = `${changed.length} modification${changed.length > 1 ? 's' : ''} non enregistrée${changed.length > 1 ? 's' : ''}`;
|
|
1577
|
+
note.classList.remove('is-clean');
|
|
1578
|
+
note.title = changed.map((field) => field.label).join(', ');
|
|
1579
|
+
} else {
|
|
1580
|
+
note.textContent = 'Aucune modification en attente';
|
|
1581
|
+
note.classList.add('is-clean');
|
|
1582
|
+
note.title = '';
|
|
712
1583
|
}
|
|
1584
|
+
$('btnSave').disabled = changed.length === 0 || !state.currentConfigName;
|
|
1585
|
+
$('btnDiscard').disabled = changed.length === 0;
|
|
1586
|
+
|
|
1587
|
+
const repo = state.config.TARGET_REPO;
|
|
1588
|
+
$('navSummaryRepo').textContent = repo || 'Aucun dépôt sélectionné';
|
|
1589
|
+
$('navSummaryOptions').textContent = [
|
|
1590
|
+
state.config.READ_ONLY ? 'Lecture seule' : 'Lecture-écriture',
|
|
1591
|
+
state.config.INCLUDE_PUBLIC ? 'dépôts publics inclus' : 'dépôts privés'
|
|
1592
|
+
].join(' · ');
|
|
1593
|
+
$('navSummaryDeploy').textContent = state.config.LOCAL_DEPLOY
|
|
1594
|
+
? 'Machine locale'
|
|
1595
|
+
: (state.config.SSH_HOST ? `SSH · ${state.config.SSH_HOST}` : 'SSH non configuré');
|
|
1596
|
+
|
|
1597
|
+
$('btnClone').disabled = !state.config.TARGET_REPO || !state.config.TARGET_BRANCH;
|
|
1598
|
+
$('btnClean').disabled = !state.extras.REPO_PATH;
|
|
1599
|
+
|
|
1600
|
+
const preview = $('cmdPreview');
|
|
1601
|
+
if (preview) preview.textContent = buildCommandPreview();
|
|
1602
|
+
|
|
1603
|
+
renderSummaryCard();
|
|
713
1604
|
}
|
|
714
1605
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1606
|
+
function buildCommandPreview() {
|
|
1607
|
+
const command = state.config.TEST_COMMAND || 'npm test';
|
|
1608
|
+
const dir = state.config.SSH_REMOTE_PATH || '<chemin du projet>';
|
|
1609
|
+
if (state.config.LOCAL_DEPLOY) return `Sera exécuté : cd ${dir} && ${command}`;
|
|
1610
|
+
const port = state.config.SSH_PORT || '22';
|
|
1611
|
+
const user = state.config.SSH_USER || '<utilisateur>';
|
|
1612
|
+
const host = state.config.SSH_HOST || '<hôte>';
|
|
1613
|
+
return `Sera exécuté : ssh -p ${port} ${user}@${host} "cd ${dir} && ${command}"`;
|
|
1614
|
+
}
|
|
718
1615
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1616
|
+
function renderSummaryCard() {
|
|
1617
|
+
const container = $('summaryRows');
|
|
1618
|
+
if (!container) return;
|
|
1619
|
+
|
|
1620
|
+
const base = state.config.BASE_BRANCH;
|
|
1621
|
+
const target = state.config.TARGET_BRANCH;
|
|
1622
|
+
const rows = [
|
|
1623
|
+
['Dépôt', state.config.TARGET_REPO || 'Non défini'],
|
|
1624
|
+
['Branches', base || target ? `${base || '—'} → ${target || '—'}` : 'Non définies'],
|
|
1625
|
+
['Mode', state.config.READ_ONLY ? 'Lecture seule' : 'Lecture-écriture'],
|
|
1626
|
+
['Déploiement', state.config.LOCAL_DEPLOY ? 'Machine locale' : (state.config.SSH_HOST ? `SSH · ${state.config.SSH_HOST}` : 'SSH non configuré')]
|
|
1627
|
+
];
|
|
1628
|
+
if (state.extras.CLONE_DIR) rows.push(['Répertoire de clonage', state.extras.CLONE_DIR]);
|
|
1629
|
+
|
|
1630
|
+
container.innerHTML = rows.map(([label, value]) => `
|
|
1631
|
+
<div class="summary-row">
|
|
1632
|
+
<div class="sum-label">${esc(label)}</div>
|
|
1633
|
+
<div class="sum-value">${esc(value)}</div>
|
|
1634
|
+
</div>
|
|
1635
|
+
`).join('');
|
|
731
1636
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1637
|
+
const notice = $('summaryNotice');
|
|
1638
|
+
if (notice) notice.hidden = changedFields().length === 0;
|
|
1639
|
+
}
|
|
735
1640
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1641
|
+
/* =============================================================== */
|
|
1642
|
+
/* Panneau — Dépôt & branches */
|
|
1643
|
+
/* =============================================================== */
|
|
1644
|
+
|
|
1645
|
+
function repoMetaLine(fullName) {
|
|
1646
|
+
const meta = state.repoMeta[fullName];
|
|
1647
|
+
if (!meta || meta.private === undefined) return '';
|
|
1648
|
+
const bits = [];
|
|
1649
|
+
bits.push(`<span class="chip-static">${icon(meta.private ? 'lock' : 'globe')}${meta.private ? 'Privé' : 'Public'}</span>`);
|
|
1650
|
+
if (meta.language) bits.push(`<span class="lang-dot"></span><span>${esc(meta.language)}</span>`);
|
|
1651
|
+
if (meta.updated_at) bits.push(`<span>· ${esc(timeAgo(meta.updated_at))}</span>`);
|
|
1652
|
+
return bits.join('');
|
|
1653
|
+
}
|
|
739
1654
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
</div>
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
</div>
|
|
1655
|
+
function renderRepoPanel() {
|
|
1656
|
+
const repo = state.config.TARGET_REPO;
|
|
1657
|
+
const base = state.config.BASE_BRANCH;
|
|
1658
|
+
const target = state.config.TARGET_BRANCH;
|
|
1659
|
+
const branches = state.branches[repo] || [];
|
|
1660
|
+
const targetIsNew = !!target && branches.length > 0 && !branches.includes(target);
|
|
1661
|
+
const loading = !!state.loadingBranches[repo];
|
|
1662
|
+
|
|
1663
|
+
const repoCard = repo ? `
|
|
1664
|
+
<div class="card">
|
|
1665
|
+
<div class="repo-row">
|
|
1666
|
+
<span class="repo-avatar">${icon('book')}</span>
|
|
1667
|
+
<span class="repo-info">
|
|
1668
|
+
<div class="repo-name">${esc(repo)}</div>
|
|
1669
|
+
<div class="repo-meta">${repoMetaLine(repo)}</div>
|
|
1670
|
+
</span>
|
|
1671
|
+
<button type="button" class="btn btn-ghost btn-lg" id="btnChangeRepo">Changer de dépôt</button>
|
|
758
1672
|
</div>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
<div id="${field.id}_loading" class="field-input flex items-center gap-2 text-dim">
|
|
769
|
-
<div class="spinner"></div>
|
|
770
|
-
Chargement des branches...
|
|
771
|
-
</div>
|
|
772
|
-
</div>
|
|
773
|
-
`;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Recréer les options avec l'option par défaut sélectionnée si value est vide
|
|
777
|
-
let branchOptions = `<option value="" ${!value ? 'selected' : ''}>— Choisir une branche —</option>`;
|
|
778
|
-
branchOptions += branches.map(branch =>
|
|
779
|
-
`<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
|
|
780
|
-
).join('');
|
|
781
|
-
|
|
782
|
-
return `
|
|
783
|
-
<div>
|
|
784
|
-
<label for="${field.id}" class="field-label">${field.label}</label>
|
|
785
|
-
<select id="${field.id}" class="field-select">
|
|
786
|
-
${branchOptions}
|
|
787
|
-
</select>
|
|
1673
|
+
</div>` : `
|
|
1674
|
+
<div class="card">
|
|
1675
|
+
<div class="empty-state">
|
|
1676
|
+
<span class="repo-avatar">${icon('book')}</span>
|
|
1677
|
+
<span class="empty-text">
|
|
1678
|
+
<div class="empty-title">Aucun dépôt sélectionné</div>
|
|
1679
|
+
<div class="empty-help">Choisissez le dépôt GitHub sur lequel le serveur MCP doit travailler.</div>
|
|
1680
|
+
</span>
|
|
1681
|
+
<button type="button" class="btn btn-primary btn-lg" id="btnChangeRepo">Choisir un dépôt</button>
|
|
788
1682
|
</div>
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1683
|
+
</div>`;
|
|
1684
|
+
|
|
1685
|
+
const pickerValue = (value, placeholder) => value
|
|
1686
|
+
? `<span class="name">${esc(value)}</span>`
|
|
1687
|
+
: `<span class="name placeholder">${esc(placeholder)}</span>`;
|
|
1688
|
+
|
|
1689
|
+
$('panel-repo').innerHTML = `
|
|
1690
|
+
<h1 class="page-title">Dépôt & branches</h1>
|
|
1691
|
+
<p class="page-subtitle">Le dépôt sur lequel le serveur MCP opère, et les deux branches qu'il utilise.</p>
|
|
1692
|
+
|
|
1693
|
+
<div class="layout">
|
|
1694
|
+
<div class="col-main">
|
|
1695
|
+
${repoCard}
|
|
1696
|
+
|
|
1697
|
+
<div class="card">
|
|
1698
|
+
<h2 class="card-title">Branches</h2>
|
|
1699
|
+
<p class="card-help">La branche de travail est créée à partir de la branche de base.</p>
|
|
1700
|
+
|
|
1701
|
+
<div class="branch-flow">
|
|
1702
|
+
<button type="button" class="branch-picker" id="pickBase" ${repo && !loading ? '' : 'disabled'}>
|
|
1703
|
+
<span class="pick-label">Branche de base</span>
|
|
1704
|
+
<span class="pick-value">
|
|
1705
|
+
${icon('branch')}
|
|
1706
|
+
${loading ? '<span class="name placeholder">Chargement…</span>' : pickerValue(base, 'Choisir une branche')}
|
|
1707
|
+
<span class="chevrons">${icon('chevrons')}</span>
|
|
1708
|
+
</span>
|
|
1709
|
+
</button>
|
|
1710
|
+
|
|
1711
|
+
<span class="branch-arrow">${icon('arrowRight', 'ic-xl')}</span>
|
|
1712
|
+
|
|
1713
|
+
<button type="button" class="branch-picker is-target" id="pickTarget" ${repo && !loading ? '' : 'disabled'}>
|
|
1714
|
+
<span class="pick-label">Branche de travail</span>
|
|
1715
|
+
<span class="pick-value">
|
|
1716
|
+
${icon('branch')}
|
|
1717
|
+
${loading ? '<span class="name placeholder">Chargement…</span>' : pickerValue(target, 'Choisir ou créer')}
|
|
1718
|
+
${targetIsNew ? '<span class="badge-new">nouvelle</span>' : ''}
|
|
1719
|
+
<span class="chevrons">${icon('chevrons')}</span>
|
|
1720
|
+
</span>
|
|
1721
|
+
</button>
|
|
801
1722
|
</div>
|
|
802
|
-
</div>
|
|
803
|
-
`;
|
|
804
|
-
}
|
|
805
1723
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
return `
|
|
815
|
-
<div>
|
|
816
|
-
<label for="${field.id}" class="field-label">${field.label}</label>
|
|
817
|
-
<div class="space-y-2">
|
|
818
|
-
<select id="${field.id}" class="field-select">
|
|
819
|
-
${selectOptions}
|
|
820
|
-
</select>
|
|
821
|
-
<input type="text" id="${field.id}_custom" placeholder="Entrez le nom de la nouvelle branche" value="${isCustom ? value : ''}" class="field-input ${shouldShowCustom ? '' : 'hidden'}">
|
|
1724
|
+
<p class="flow-hint">${
|
|
1725
|
+
!repo
|
|
1726
|
+
? 'Sélectionnez d\u2019abord un dépôt pour charger ses branches.'
|
|
1727
|
+
: targetIsNew
|
|
1728
|
+
? `« ${esc(target)} » n\u2019existe pas encore sur le dépôt : elle sera créée à partir de ${esc(base || 'la branche de base')} au premier commit.`
|
|
1729
|
+
: 'Les deux branches existent déjà sur le dépôt.'
|
|
1730
|
+
}</p>
|
|
822
1731
|
</div>
|
|
823
1732
|
</div>
|
|
824
|
-
`;
|
|
825
|
-
}
|
|
826
1733
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1734
|
+
<div class="col-side">
|
|
1735
|
+
<div class="card">
|
|
1736
|
+
<div class="side-label">Résumé effectif</div>
|
|
1737
|
+
<div id="summaryRows" style="margin-top:12px"></div>
|
|
1738
|
+
<div class="notice" id="summaryNotice" hidden>
|
|
1739
|
+
${icon('alert')}
|
|
1740
|
+
<span>Redémarrage requis</span>
|
|
1741
|
+
</div>
|
|
1742
|
+
</div>
|
|
1743
|
+
</div>
|
|
833
1744
|
</div>
|
|
834
1745
|
`;
|
|
1746
|
+
|
|
1747
|
+
const changeRepo = $('btnChangeRepo');
|
|
1748
|
+
if (changeRepo) changeRepo.addEventListener('click', openRepoPalette);
|
|
1749
|
+
const pickBase = $('pickBase');
|
|
1750
|
+
if (pickBase) pickBase.addEventListener('click', () => openBranchPalette('BASE_BRANCH'));
|
|
1751
|
+
const pickTarget = $('pickTarget');
|
|
1752
|
+
if (pickTarget) pickTarget.addEventListener('click', () => openBranchPalette('TARGET_BRANCH'));
|
|
835
1753
|
}
|
|
836
1754
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1755
|
+
function openRepoPalette() {
|
|
1756
|
+
openPalette({
|
|
1757
|
+
placeholder: 'Rechercher un dépôt…',
|
|
1758
|
+
groupLabel: 'Vos dépôts',
|
|
1759
|
+
footerHint: 'Recherche côté GitHub',
|
|
1760
|
+
emptyLabel: 'Aucun dépôt ne correspond',
|
|
1761
|
+
remote: true,
|
|
1762
|
+
filters: [
|
|
1763
|
+
{
|
|
1764
|
+
label: 'Dépôts publics',
|
|
1765
|
+
on: !!state.config.INCLUDE_PUBLIC,
|
|
1766
|
+
onToggle: (on) => setIncludeFlag('INCLUDE_PUBLIC', on)
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
label: 'Autres propriétaires',
|
|
1770
|
+
on: !!state.config.INCLUDE_NOT_OWNED,
|
|
1771
|
+
onToggle: (on) => setIncludeFlag('INCLUDE_NOT_OWNED', on)
|
|
1772
|
+
}
|
|
1773
|
+
],
|
|
1774
|
+
source: async (query) => {
|
|
1775
|
+
const repos = await fetchRepositories(query);
|
|
1776
|
+
return repos.map((repo) => ({
|
|
1777
|
+
id: repo.full_name,
|
|
1778
|
+
title: repo.full_name,
|
|
1779
|
+
subtitle: [
|
|
1780
|
+
repo.private === undefined ? null : (repo.private ? 'Privé' : 'Public'),
|
|
1781
|
+
repo.language || null,
|
|
1782
|
+
repo.updated_at ? timeAgo(repo.updated_at) : null
|
|
1783
|
+
].filter(Boolean).join(' · '),
|
|
1784
|
+
badge: repo.full_name === state.config.TARGET_REPO
|
|
1785
|
+
? 'actuel'
|
|
1786
|
+
: (repo.owned === false ? 'non possédé' : null),
|
|
1787
|
+
current: repo.full_name === state.config.TARGET_REPO
|
|
1788
|
+
}));
|
|
1789
|
+
},
|
|
1790
|
+
onPick: selectRepository
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
842
1793
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1794
|
+
async function selectRepository(fullName) {
|
|
1795
|
+
if (fullName === state.config.TARGET_REPO) return;
|
|
1796
|
+
state.config.TARGET_REPO = fullName;
|
|
1797
|
+
state.config.BASE_BRANCH = '';
|
|
1798
|
+
state.config.TARGET_BRANCH = '';
|
|
1799
|
+
renderRepoPanel();
|
|
1800
|
+
refreshDerived();
|
|
1801
|
+
|
|
1802
|
+
await fetchBranches(fullName);
|
|
1803
|
+
const branches = state.branches[fullName] || [];
|
|
1804
|
+
if (branches.includes('main')) state.config.BASE_BRANCH = 'main';
|
|
1805
|
+
else if (branches.includes('master')) state.config.BASE_BRANCH = 'master';
|
|
1806
|
+
else if (branches.length) state.config.BASE_BRANCH = branches[0];
|
|
1807
|
+
|
|
1808
|
+
renderRepoPanel();
|
|
1809
|
+
refreshDerived();
|
|
1810
|
+
}
|
|
858
1811
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1812
|
+
function openBranchPalette(fieldId) {
|
|
1813
|
+
const repo = state.config.TARGET_REPO;
|
|
1814
|
+
const branches = state.branches[repo] || [];
|
|
1815
|
+
const isTarget = fieldId === 'TARGET_BRANCH';
|
|
1816
|
+
|
|
1817
|
+
openPalette({
|
|
1818
|
+
placeholder: isTarget ? 'Rechercher ou nommer une branche…' : 'Rechercher une branche…',
|
|
1819
|
+
groupLabel: 'Branches du dépôt',
|
|
1820
|
+
footerHint: repo,
|
|
1821
|
+
emptyLabel: isTarget ? 'Tapez un nom pour créer la branche' : 'Aucune branche',
|
|
1822
|
+
createLabel: isTarget ? 'Créer la branche «\u00a0%s\u00a0»' : null,
|
|
1823
|
+
validateCreate: (value) => /^[\w.\-\/]+$/.test(value),
|
|
1824
|
+
source: async () => branches.map((branch) => ({
|
|
1825
|
+
id: branch,
|
|
1826
|
+
title: branch,
|
|
1827
|
+
iconName: 'branch',
|
|
1828
|
+
badge: branch === state.config[fieldId] ? 'actuel' : null,
|
|
1829
|
+
current: branch === state.config[fieldId]
|
|
1830
|
+
})),
|
|
1831
|
+
onPick: (value) => {
|
|
1832
|
+
state.config[fieldId] = value;
|
|
1833
|
+
renderRepoPanel();
|
|
1834
|
+
refreshDerived();
|
|
863
1835
|
}
|
|
864
|
-
|
|
865
|
-
renderInputs(currentConfig);
|
|
866
|
-
} catch (error) {
|
|
867
|
-
showStatus(error.message || 'Erreur lors du chargement de la configuration', 'error');
|
|
868
|
-
}
|
|
1836
|
+
});
|
|
869
1837
|
}
|
|
870
1838
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1839
|
+
/* =============================================================== */
|
|
1840
|
+
/* Panneau — Options */
|
|
1841
|
+
/* =============================================================== */
|
|
1842
|
+
|
|
1843
|
+
const OPTION_ROWS = [
|
|
1844
|
+
{
|
|
1845
|
+
id: 'READ_ONLY',
|
|
1846
|
+
title: 'Mode lecture seule',
|
|
1847
|
+
help: 'Le serveur peut lire le dépôt mais ne pousse ni commit ni branche.'
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
id: 'INCLUDE_PUBLIC',
|
|
1851
|
+
title: 'Inclure les dépôts publics',
|
|
1852
|
+
help: 'Sans cette option, seuls vos dépôts privés apparaissent dans la recherche.'
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
id: 'INCLUDE_NOT_OWNED',
|
|
1856
|
+
title: 'Inclure les dépôts d\u2019autres propriétaires',
|
|
1857
|
+
help: 'Ajoute les dépôts où vous êtes collaborateur ou membre d\u2019une organisation.'
|
|
883
1858
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1859
|
+
];
|
|
1860
|
+
|
|
1861
|
+
function renderOptionsPanel() {
|
|
1862
|
+
$('panel-options').innerHTML = `
|
|
1863
|
+
<h1 class="page-title">Options</h1>
|
|
1864
|
+
<p class="page-subtitle">Ce que le serveur MCP a le droit de faire, et ce qu'il vous propose.</p>
|
|
1865
|
+
|
|
1866
|
+
<div class="layout is-single" style="max-width:800px">
|
|
1867
|
+
<div class="col-main">
|
|
1868
|
+
<div class="card">
|
|
1869
|
+
${OPTION_ROWS.map((row) => `
|
|
1870
|
+
<div class="toggle-row">
|
|
1871
|
+
<span class="toggle-text">
|
|
1872
|
+
<div class="toggle-title">${esc(row.title)}</div>
|
|
1873
|
+
<div class="toggle-help">${esc(row.help)}</div>
|
|
1874
|
+
</span>
|
|
1875
|
+
<label class="switch">
|
|
1876
|
+
<input type="checkbox" data-toggle="${row.id}" ${state.config[row.id] ? 'checked' : ''}>
|
|
1877
|
+
<span class="slider"></span>
|
|
1878
|
+
</label>
|
|
1879
|
+
</div>
|
|
1880
|
+
`).join('')}
|
|
1881
|
+
</div>
|
|
889
1882
|
</div>
|
|
890
|
-
`;
|
|
891
|
-
}
|
|
892
|
-
if (field.type === 'select') {
|
|
893
|
-
return renderSelectField(field, config[field.id] || '', config);
|
|
894
|
-
}
|
|
895
|
-
const wrapperClass = sshOnlyFields.includes(field.id) ? 'ssh-only' : '';
|
|
896
|
-
return `
|
|
897
|
-
<div class="${wrapperClass}">
|
|
898
|
-
<label for="${field.id}" class="field-label">${field.label}</label>
|
|
899
|
-
<input type="${field.type}" id="${field.id}" value="${config[field.id] || ''}" placeholder="${field.placeholder}" class="field-input">
|
|
900
1883
|
</div>
|
|
901
1884
|
`;
|
|
1885
|
+
|
|
1886
|
+
$('panel-options').querySelectorAll('[data-toggle]').forEach((input) => {
|
|
1887
|
+
input.addEventListener('change', () => {
|
|
1888
|
+
const id = input.dataset.toggle;
|
|
1889
|
+
if (id === 'INCLUDE_PUBLIC' || id === 'INCLUDE_NOT_OWNED') {
|
|
1890
|
+
setIncludeFlag(id, input.checked);
|
|
1891
|
+
} else {
|
|
1892
|
+
setField(id, input.checked);
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
async function setIncludeFlag(id, value) {
|
|
1899
|
+
state.config[id] = value;
|
|
1900
|
+
refreshDerived();
|
|
1901
|
+
if (state.panel === 'options') {
|
|
1902
|
+
const input = document.querySelector(`[data-toggle="${id}"]`);
|
|
1903
|
+
if (input) input.checked = value;
|
|
1904
|
+
}
|
|
1905
|
+
try {
|
|
1906
|
+
await request('api/config', {
|
|
1907
|
+
method: 'POST',
|
|
1908
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1909
|
+
body: JSON.stringify({ [id]: String(value) })
|
|
1910
|
+
});
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
toast({ type: 'error', title: 'Filtre non appliqué', detail: error.message });
|
|
1913
|
+
}
|
|
902
1914
|
}
|
|
903
1915
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1916
|
+
/* =============================================================== */
|
|
1917
|
+
/* Panneau — Déploiement & test */
|
|
1918
|
+
/* =============================================================== */
|
|
1919
|
+
|
|
1920
|
+
function renderDeployPanel() {
|
|
1921
|
+
const local = !!state.config.LOCAL_DEPLOY;
|
|
1922
|
+
const test = state.sshTest;
|
|
1923
|
+
|
|
1924
|
+
const testResult = test
|
|
1925
|
+
? `<span class="test-result ${test.pending ? 'is-pending' : test.ok ? 'is-ok' : 'is-ko'}">
|
|
1926
|
+
${test.pending ? '<span class="spinner"></span>' : icon(test.ok ? 'check' : 'cross')}
|
|
1927
|
+
<span>${esc(test.message)}</span>
|
|
1928
|
+
</span>`
|
|
912
1929
|
: '';
|
|
913
1930
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1931
|
+
$('panel-deploy').innerHTML = `
|
|
1932
|
+
<h1 class="page-title">Déploiement & test</h1>
|
|
1933
|
+
<p class="page-subtitle">Où le serveur applique les changements, et comment il vérifie qu'ils passent.</p>
|
|
1934
|
+
|
|
1935
|
+
<div class="layout">
|
|
1936
|
+
<div class="col-main">
|
|
1937
|
+
<div class="card">
|
|
1938
|
+
<h2 class="card-title">Cible de déploiement</h2>
|
|
1939
|
+
<div class="segmented">
|
|
1940
|
+
<button type="button" data-target="local" class="${local ? 'is-active' : ''}">
|
|
1941
|
+
${icon('drive')}<span>Même machine</span>
|
|
1942
|
+
</button>
|
|
1943
|
+
<button type="button" data-target="ssh" class="${local ? '' : 'is-active'}">
|
|
1944
|
+
${icon('server')}<span>Serveur distant (SSH)</span>
|
|
1945
|
+
</button>
|
|
1946
|
+
</div>
|
|
1947
|
+
<p class="card-help" style="margin-top:16px">${
|
|
1948
|
+
local
|
|
1949
|
+
? 'Les commandes sont exécutées sur la machine qui héberge le serveur MCP.'
|
|
1950
|
+
: 'Les commandes sont exécutées à distance. Les identifiants de connexion s\u2019affichent ci-dessous.'
|
|
1951
|
+
}</p>
|
|
1952
|
+
</div>
|
|
1953
|
+
|
|
1954
|
+
<div class="card" id="sshCard" ${local ? 'hidden' : ''}>
|
|
1955
|
+
<h2 class="card-title">Connexion SSH</h2>
|
|
1956
|
+
|
|
1957
|
+
<div class="field-grid" style="margin-top:20px">
|
|
1958
|
+
<div class="field" style="flex:2 1 260px">
|
|
1959
|
+
<label class="field-label" for="SSH_HOST">Hôte</label>
|
|
1960
|
+
<input class="input is-mono" id="SSH_HOST" data-text-field="SSH_HOST"
|
|
1961
|
+
value="${esc(state.config.SSH_HOST)}" placeholder="srv.example.com" autocomplete="off">
|
|
1962
|
+
</div>
|
|
1963
|
+
<div class="field" style="flex:1 1 140px">
|
|
1964
|
+
<label class="field-label" for="SSH_USER">Utilisateur</label>
|
|
1965
|
+
<input class="input is-mono" id="SSH_USER" data-text-field="SSH_USER"
|
|
1966
|
+
value="${esc(state.config.SSH_USER)}" placeholder="root" autocomplete="off">
|
|
1967
|
+
</div>
|
|
1968
|
+
<div class="field" style="flex:1 1 120px">
|
|
1969
|
+
<label class="field-label" for="SSH_PORT">Port</label>
|
|
1970
|
+
<input class="input is-mono" id="SSH_PORT" data-text-field="SSH_PORT"
|
|
1971
|
+
value="${esc(state.config.SSH_PORT)}" placeholder="22" autocomplete="off">
|
|
1972
|
+
</div>
|
|
1973
|
+
</div>
|
|
1974
|
+
|
|
1975
|
+
<div class="field" style="margin-top:20px">
|
|
1976
|
+
<label class="field-label" for="SSH_KEY_PATH">Clé privée</label>
|
|
1977
|
+
<input class="input is-mono" id="SSH_KEY_PATH" data-text-field="SSH_KEY_PATH"
|
|
1978
|
+
value="${esc(state.config.SSH_KEY_PATH)}"
|
|
1979
|
+
placeholder="C:\\Users\\moi\\.ssh\\id_ed25519" autocomplete="off">
|
|
1980
|
+
</div>
|
|
918
1981
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1982
|
+
<div class="test-row">
|
|
1983
|
+
<button type="button" class="btn btn-soft btn-lg" id="btnSshTest">
|
|
1984
|
+
${icon('plug')}<span>Tester la connexion</span>
|
|
1985
|
+
</button>
|
|
1986
|
+
${testResult}
|
|
1987
|
+
</div>
|
|
924
1988
|
</div>
|
|
925
|
-
`;
|
|
926
|
-
}
|
|
927
1989
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1990
|
+
<div class="card">
|
|
1991
|
+
<h2 class="card-title">Projet & tests</h2>
|
|
1992
|
+
|
|
1993
|
+
<div class="field" style="margin-top:20px">
|
|
1994
|
+
<label class="field-label" for="SSH_REMOTE_PATH">Chemin du projet sur la cible</label>
|
|
1995
|
+
<input class="input is-mono" id="SSH_REMOTE_PATH" data-text-field="SSH_REMOTE_PATH"
|
|
1996
|
+
value="${esc(state.config.SSH_REMOTE_PATH)}"
|
|
1997
|
+
placeholder="/var/www/mon-projet" autocomplete="off">
|
|
1998
|
+
</div>
|
|
1999
|
+
|
|
2000
|
+
<div class="field" style="margin-top:20px">
|
|
2001
|
+
<label class="field-label" for="TEST_COMMAND">Commande de test</label>
|
|
2002
|
+
<input class="input is-mono" id="TEST_COMMAND" data-text-field="TEST_COMMAND"
|
|
2003
|
+
value="${esc(state.config.TEST_COMMAND)}"
|
|
2004
|
+
placeholder="npm test" autocomplete="off">
|
|
2005
|
+
</div>
|
|
2006
|
+
|
|
2007
|
+
<p class="cmd-preview" id="cmdPreview"></p>
|
|
939
2008
|
</div>
|
|
940
2009
|
</div>
|
|
941
|
-
`;
|
|
942
|
-
}).join('') + cloneDirInfo;
|
|
943
2010
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
2011
|
+
<div class="col-side">
|
|
2012
|
+
<div class="card">
|
|
2013
|
+
<div class="side-label">Journal d'activité</div>
|
|
2014
|
+
<div id="activityRows" style="margin-top:12px"></div>
|
|
2015
|
+
</div>
|
|
2016
|
+
</div>
|
|
2017
|
+
</div>
|
|
2018
|
+
`;
|
|
947
2019
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
2020
|
+
$('panel-deploy').querySelectorAll('[data-target]').forEach((button) => {
|
|
2021
|
+
button.addEventListener('click', () => {
|
|
2022
|
+
const wantLocal = button.dataset.target === 'local';
|
|
2023
|
+
if (wantLocal === !!state.config.LOCAL_DEPLOY) return;
|
|
2024
|
+
state.config.LOCAL_DEPLOY = wantLocal;
|
|
2025
|
+
renderDeployPanel();
|
|
2026
|
+
refreshDerived();
|
|
2027
|
+
});
|
|
2028
|
+
});
|
|
951
2029
|
|
|
952
|
-
|
|
2030
|
+
bindTextFields($('panel-deploy'));
|
|
2031
|
+
|
|
2032
|
+
const testBtn = $('btnSshTest');
|
|
2033
|
+
if (testBtn) testBtn.addEventListener('click', runSshTest);
|
|
2034
|
+
|
|
2035
|
+
renderActivity();
|
|
953
2036
|
}
|
|
954
2037
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
fields.forEach(field => {
|
|
959
|
-
const input = document.getElementById(field.id);
|
|
960
|
-
if (input) {
|
|
961
|
-
if (field.type === 'checkbox' || field.type === 'slider') {
|
|
962
|
-
currentConfig[field.id] = input.checked;
|
|
963
|
-
} else if (field.id === 'TARGET_BRANCH') {
|
|
964
|
-
const customInput = document.getElementById('TARGET_BRANCH_custom');
|
|
965
|
-
currentConfig[field.id] = (input.value === '' && customInput && customInput.value) ? customInput.value : input.value;
|
|
966
|
-
} else {
|
|
967
|
-
currentConfig[field.id] = input.value;
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
renderInputs(currentConfig);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Setup event listeners
|
|
975
|
-
function setupEventListeners() {
|
|
976
|
-
const targetBranchSelect = document.getElementById('TARGET_BRANCH');
|
|
977
|
-
const repoSearch = document.getElementById('REPO_SEARCH');
|
|
978
|
-
const resultsContainer = document.getElementById('REPO_RESULTS');
|
|
979
|
-
|
|
980
|
-
// Sections repliables : bascule sans re-render pour préserver les saisies
|
|
981
|
-
inputsContainer.querySelectorAll('.section-header').forEach(header => {
|
|
982
|
-
header.addEventListener('click', () => {
|
|
983
|
-
const id = header.dataset.section;
|
|
984
|
-
sectionState[id] = !sectionState[id];
|
|
985
|
-
header.classList.toggle('open', sectionState[id]);
|
|
986
|
-
const body = inputsContainer.querySelector(`[data-section-body="${id}"]`);
|
|
987
|
-
if (body) body.classList.toggle('hidden', !sectionState[id]);
|
|
988
|
-
});
|
|
2038
|
+
function bindTextFields(scope) {
|
|
2039
|
+
scope.querySelectorAll('[data-text-field]').forEach((input) => {
|
|
2040
|
+
input.addEventListener('input', () => setField(input.dataset.textField, input.value));
|
|
989
2041
|
});
|
|
2042
|
+
}
|
|
990
2043
|
|
|
991
|
-
|
|
992
|
-
const
|
|
993
|
-
if (
|
|
994
|
-
const applyDeployMode = (isLocal) => {
|
|
995
|
-
inputsContainer.querySelectorAll('.ssh-only').forEach(el => el.classList.toggle('hidden', isLocal));
|
|
996
|
-
};
|
|
997
|
-
applyDeployMode(localDeployToggle.checked);
|
|
998
|
-
localDeployToggle.addEventListener('change', () => applyDeployMode(localDeployToggle.checked));
|
|
999
|
-
}
|
|
2044
|
+
function renderActivity() {
|
|
2045
|
+
const container = $('activityRows');
|
|
2046
|
+
if (!container) return;
|
|
1000
2047
|
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
renderRepoResults();
|
|
1006
|
-
} else {
|
|
1007
|
-
// Si vraiment vide, on tente de charger une fois
|
|
1008
|
-
fetchRepositories();
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
2048
|
+
if (!state.activity.length) {
|
|
2049
|
+
container.innerHTML = `<div class="log-empty">Les actions de cette session s'afficheront ici.</div>`;
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
1011
2052
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
} else if (e.key === 'ArrowUp') {
|
|
1022
|
-
e.preventDefault();
|
|
1023
|
-
activeIndex = Math.max(activeIndex - 1, 0);
|
|
1024
|
-
updateActiveItem(items);
|
|
1025
|
-
} else if (e.key === 'Enter') {
|
|
1026
|
-
e.preventDefault();
|
|
1027
|
-
if (activeIndex >= 0) {
|
|
1028
|
-
selectRepo(selectState.repos[activeIndex]);
|
|
1029
|
-
}
|
|
1030
|
-
} else if (e.key === 'Escape') {
|
|
1031
|
-
resultsContainer.classList.add('hidden');
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
2053
|
+
const iconFor = { ok: 'check', ko: 'cross', info: 'refresh', neutral: 'download' };
|
|
2054
|
+
container.innerHTML = state.activity.map((entry) => `
|
|
2055
|
+
<div class="log-entry is-${entry.tone}">
|
|
2056
|
+
${icon(iconFor[entry.tone] || 'info')}
|
|
2057
|
+
<span class="log-label">${esc(entry.label)}</span>
|
|
2058
|
+
<span class="log-time">${esc(timeAgo(entry.at))}</span>
|
|
2059
|
+
</div>
|
|
2060
|
+
`).join('');
|
|
2061
|
+
}
|
|
1034
2062
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
2063
|
+
async function runSshTest() {
|
|
2064
|
+
state.sshTest = { pending: true, message: 'Connexion en cours…' };
|
|
2065
|
+
renderDeployPanel();
|
|
2066
|
+
try {
|
|
2067
|
+
const result = await request('api/ssh-test', {
|
|
2068
|
+
method: 'POST',
|
|
2069
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2070
|
+
body: JSON.stringify({
|
|
2071
|
+
SSH_HOST: state.config.SSH_HOST,
|
|
2072
|
+
SSH_USER: state.config.SSH_USER,
|
|
2073
|
+
SSH_PORT: state.config.SSH_PORT,
|
|
2074
|
+
SSH_KEY_PATH: state.config.SSH_KEY_PATH,
|
|
2075
|
+
SSH_REMOTE_PATH: state.config.SSH_REMOTE_PATH
|
|
2076
|
+
})
|
|
1041
2077
|
});
|
|
2078
|
+
state.sshTest = { ok: true, message: result.message || 'Connexion établie' };
|
|
2079
|
+
logActivity('ok', 'Connexion SSH vérifiée');
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
state.sshTest = { ok: false, message: error.message };
|
|
2082
|
+
logActivity('ko', `Test SSH échoué : ${error.message}`);
|
|
1042
2083
|
}
|
|
2084
|
+
renderDeployPanel();
|
|
2085
|
+
}
|
|
1043
2086
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (index === activeIndex) {
|
|
1048
|
-
item.scrollIntoView({ block: 'nearest' });
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
2087
|
+
/* =============================================================== */
|
|
2088
|
+
/* Navigation entre panneaux */
|
|
2089
|
+
/* =============================================================== */
|
|
1052
2090
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
}
|
|
2091
|
+
function showPanel(name) {
|
|
2092
|
+
state.panel = name;
|
|
2093
|
+
['repo', 'options', 'deploy'].forEach((key) => {
|
|
2094
|
+
$(`panel-${key}`).hidden = key !== name;
|
|
1057
2095
|
});
|
|
2096
|
+
document.querySelectorAll('.nav-item').forEach((item) => {
|
|
2097
|
+
item.classList.toggle('is-active', item.dataset.panel === name);
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
if (name === 'repo') renderRepoPanel();
|
|
2101
|
+
if (name === 'options') renderOptionsPanel();
|
|
2102
|
+
if (name === 'deploy') renderDeployPanel();
|
|
2103
|
+
refreshDerived();
|
|
2104
|
+
}
|
|
1058
2105
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
});
|
|
2106
|
+
/* =============================================================== */
|
|
2107
|
+
/* Configurations */
|
|
2108
|
+
/* =============================================================== */
|
|
2109
|
+
|
|
2110
|
+
function openConfigPalette() {
|
|
2111
|
+
openPalette({
|
|
2112
|
+
placeholder: 'Rechercher une configuration…',
|
|
2113
|
+
groupLabel: 'Configurations',
|
|
2114
|
+
footerHint: 'Une configuration par projet',
|
|
2115
|
+
emptyLabel: 'Aucune configuration',
|
|
2116
|
+
createLabel: 'Créer la configuration «\u00a0%s\u00a0»',
|
|
2117
|
+
validateCreate: (value) => !state.configNames.includes(value),
|
|
2118
|
+
source: async () => state.configNames.map((name) => ({
|
|
2119
|
+
id: name,
|
|
2120
|
+
title: name,
|
|
2121
|
+
iconName: 'folder',
|
|
2122
|
+
badge: name === state.currentConfigName ? 'actuelle' : null,
|
|
2123
|
+
current: name === state.currentConfigName
|
|
2124
|
+
})),
|
|
2125
|
+
onPick: async (value, item) => {
|
|
2126
|
+
if (item.isCreate) await createConfiguration(value);
|
|
2127
|
+
else if (value !== state.currentConfigName) await activateConfiguration(value);
|
|
1083
2128
|
}
|
|
1084
2129
|
});
|
|
2130
|
+
}
|
|
1085
2131
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
2132
|
+
async function activateConfiguration(name) {
|
|
2133
|
+
try {
|
|
2134
|
+
await request(`api/configs/${encodeURIComponent(name)}/activate`, { method: 'POST' });
|
|
2135
|
+
state.currentConfigName = name;
|
|
2136
|
+
state.branches = {};
|
|
2137
|
+
state.sshTest = null;
|
|
2138
|
+
await reloadEverything();
|
|
2139
|
+
toast({ type: 'success', title: `Configuration «\u00a0${name}\u00a0» activée` });
|
|
2140
|
+
logActivity('info', `Configuration «\u00a0${name}\u00a0» activée`);
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
toast({ type: 'error', title: 'Activation impossible', detail: error.message });
|
|
1099
2143
|
}
|
|
1100
2144
|
}
|
|
1101
2145
|
|
|
1102
|
-
function
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
: '<svg class="w-4 h-4 shrink-0" style="color:var(--danger)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
|
|
1109
|
-
|
|
1110
|
-
toast.className = `toast ${type === 'success' ? 'toast-success' : 'toast-error'}`;
|
|
1111
|
-
toast.innerHTML = `${icon}<span class="flex-1">${message}</span>`;
|
|
2146
|
+
async function createConfiguration(name) {
|
|
2147
|
+
const blank = {};
|
|
2148
|
+
FIELDS.forEach((field) => { blank[field.id] = field.type === 'bool' ? false : ''; });
|
|
2149
|
+
blank.READ_ONLY = state.config.READ_ONLY;
|
|
2150
|
+
blank.INCLUDE_PUBLIC = state.config.INCLUDE_PUBLIC;
|
|
2151
|
+
blank.INCLUDE_NOT_OWNED = state.config.INCLUDE_NOT_OWNED;
|
|
1112
2152
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
2153
|
+
try {
|
|
2154
|
+
await request('api/configs', {
|
|
2155
|
+
method: 'POST',
|
|
2156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2157
|
+
body: JSON.stringify({ name, config: blank })
|
|
2158
|
+
});
|
|
2159
|
+
await loadConfigs();
|
|
2160
|
+
await activateConfiguration(name);
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
toast({ type: 'error', title: 'Création impossible', detail: error.message });
|
|
2163
|
+
}
|
|
1119
2164
|
}
|
|
1120
2165
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
// Check if a configuration is selected
|
|
1126
|
-
if (!currentConfigName) {
|
|
1127
|
-
showStatus('Veuillez sélectionner ou créer une configuration', 'error');
|
|
2166
|
+
async function saveConfiguration() {
|
|
2167
|
+
if (!state.currentConfigName) {
|
|
2168
|
+
toast({ type: 'error', title: 'Aucune configuration sélectionnée', detail: 'Créez-en une depuis la barre du haut.' });
|
|
1128
2169
|
return;
|
|
1129
2170
|
}
|
|
1130
2171
|
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
const input = document.getElementById(field.id);
|
|
1134
|
-
if (!input) return; // Protection contre les éléments manquants
|
|
1135
|
-
if (field.type === 'checkbox' || field.type === 'slider') {
|
|
1136
|
-
config[field.id] = input.checked;
|
|
1137
|
-
} else if (field.id === 'TARGET_BRANCH') {
|
|
1138
|
-
const customInput = document.getElementById(`${field.id}_custom`);
|
|
1139
|
-
config[field.id] = customInput && customInput.value ? customInput.value : input.value;
|
|
1140
|
-
} else {
|
|
1141
|
-
config[field.id] = input.value;
|
|
1142
|
-
}
|
|
1143
|
-
});
|
|
2172
|
+
const payload = {};
|
|
2173
|
+
FIELDS.forEach((field) => { payload[field.id] = state.config[field.id]; });
|
|
1144
2174
|
|
|
1145
2175
|
try {
|
|
1146
|
-
|
|
1147
|
-
const response = await fetch(api('api/configs'), {
|
|
2176
|
+
await request('api/configs', {
|
|
1148
2177
|
method: 'POST',
|
|
1149
2178
|
headers: { 'Content-Type': 'application/json' },
|
|
1150
|
-
body: JSON.stringify({ name: currentConfigName, config })
|
|
2179
|
+
body: JSON.stringify({ name: state.currentConfigName, config: payload })
|
|
2180
|
+
});
|
|
2181
|
+
state.saved = Object.assign({}, state.config);
|
|
2182
|
+
refreshDerived();
|
|
2183
|
+
logActivity('ok', 'Configuration enregistrée');
|
|
2184
|
+
toast({
|
|
2185
|
+
type: 'success',
|
|
2186
|
+
title: 'Configuration enregistrée',
|
|
2187
|
+
detail: 'Redémarrez le serveur pour l\u2019appliquer.',
|
|
2188
|
+
actionLabel: 'Redémarrer',
|
|
2189
|
+
onAction: restartServer
|
|
1151
2190
|
});
|
|
1152
|
-
|
|
1153
|
-
if (response.ok) {
|
|
1154
|
-
showStatus(`Configuration "${currentConfigName}" enregistrée avec succès !`, 'success');
|
|
1155
|
-
} else {
|
|
1156
|
-
const data = await response.json().catch(() => ({ error: 'Erreur inconnue' }));
|
|
1157
|
-
throw new Error(data.error || 'Erreur lors de l\'enregistrement');
|
|
1158
|
-
}
|
|
1159
2191
|
} catch (error) {
|
|
1160
|
-
|
|
2192
|
+
toast({ type: 'error', title: 'Enregistrement impossible', detail: error.message });
|
|
1161
2193
|
}
|
|
1162
|
-
}
|
|
2194
|
+
}
|
|
1163
2195
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
}
|
|
2196
|
+
function discardChanges() {
|
|
2197
|
+
state.config = Object.assign({}, state.saved);
|
|
2198
|
+
showPanel(state.panel);
|
|
2199
|
+
toast({ type: 'info', title: 'Modifications annulées' });
|
|
2200
|
+
}
|
|
1168
2201
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
2202
|
+
/* =============================================================== */
|
|
2203
|
+
/* Actions serveur */
|
|
2204
|
+
/* =============================================================== */
|
|
2205
|
+
|
|
2206
|
+
async function restartServer() {
|
|
2207
|
+
const confirmed = await openModal({
|
|
2208
|
+
title: 'Redémarrer le serveur MCP ?',
|
|
2209
|
+
body: 'La connexion en cours sera coupée le temps du redémarrage, puis la page se rechargera.',
|
|
2210
|
+
confirmLabel: 'Redémarrer'
|
|
2211
|
+
});
|
|
2212
|
+
if (!confirmed) return;
|
|
1173
2213
|
|
|
1174
|
-
|
|
2214
|
+
const label = $('btnRestartLabel');
|
|
2215
|
+
const original = label.textContent;
|
|
2216
|
+
label.textContent = 'Redémarrage…';
|
|
2217
|
+
$('btnRestart').disabled = true;
|
|
1175
2218
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
2219
|
+
try {
|
|
2220
|
+
await request('api/restart', { method: 'POST' });
|
|
2221
|
+
logActivity('info', 'Serveur MCP redémarré');
|
|
2222
|
+
toast({ type: 'success', title: 'Redémarrage lancé', detail: 'La page se recharge dans quelques secondes.' });
|
|
2223
|
+
setTimeout(() => window.location.reload(), 3000);
|
|
1182
2224
|
} catch (error) {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
2225
|
+
toast({ type: 'error', title: 'Redémarrage impossible', detail: error.message });
|
|
2226
|
+
label.textContent = original;
|
|
2227
|
+
$('btnRestart').disabled = false;
|
|
1186
2228
|
}
|
|
1187
|
-
}
|
|
2229
|
+
}
|
|
1188
2230
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
2231
|
+
async function stopServer() {
|
|
2232
|
+
const confirmed = await openModal({
|
|
2233
|
+
variant: 'danger',
|
|
2234
|
+
title: 'Arrêter complètement le serveur MCP ?',
|
|
2235
|
+
body: 'Le serveur s\u2019arrête et cette page ne sera plus accessible. Il faudra le relancer depuis votre client MCP.',
|
|
2236
|
+
confirmLabel: 'Arrêter'
|
|
2237
|
+
});
|
|
2238
|
+
if (!confirmed) return;
|
|
1193
2239
|
|
|
1194
|
-
const originalHTML = stopBtn.innerHTML; // Sauvegarder le HTML original
|
|
1195
2240
|
try {
|
|
1196
|
-
|
|
1197
|
-
|
|
2241
|
+
await request('api/stop', { method: 'POST' });
|
|
2242
|
+
state.serverOk = false;
|
|
2243
|
+
renderStatus();
|
|
2244
|
+
logActivity('ko', 'Serveur MCP arrêté');
|
|
2245
|
+
toast({ type: 'info', title: 'Le serveur s\u2019arrête', detail: 'Cette page ne sera plus disponible.', sticky: true });
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
toast({ type: 'error', title: 'Arrêt impossible', detail: error.message });
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
1198
2250
|
|
|
1199
|
-
|
|
2251
|
+
async function cleanWorkingDir() {
|
|
2252
|
+
const repoName = (state.config.TARGET_REPO || '').split('/').pop() || 'le dépôt';
|
|
2253
|
+
const confirmed = await openModal({
|
|
2254
|
+
variant: 'danger',
|
|
2255
|
+
title: 'Supprimer le dossier de travail local ?',
|
|
2256
|
+
body: 'Le dossier sera supprimé du disque et les modifications non poussées seront perdues. Il faudra recloner le dépôt pour réutiliser les outils Git.',
|
|
2257
|
+
pathText: state.extras.REPO_PATH,
|
|
2258
|
+
confirmWord: repoName,
|
|
2259
|
+
confirmLabel: 'Supprimer'
|
|
2260
|
+
});
|
|
2261
|
+
if (!confirmed) return;
|
|
1200
2262
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
2263
|
+
try {
|
|
2264
|
+
await request('api/delete-working-dir', { method: 'POST' });
|
|
2265
|
+
state.extras.REPO_PATH = '';
|
|
2266
|
+
refreshDerived();
|
|
2267
|
+
logActivity('ko', 'Dossier de travail supprimé');
|
|
2268
|
+
toast({ type: 'success', title: 'Dossier de travail supprimé' });
|
|
1206
2269
|
} catch (error) {
|
|
1207
|
-
|
|
1208
|
-
stopBtn.disabled = false;
|
|
1209
|
-
stopBtn.innerHTML = originalHTML; // Restaurer le HTML original
|
|
2270
|
+
toast({ type: 'error', title: 'Suppression impossible', detail: error.message });
|
|
1210
2271
|
}
|
|
1211
|
-
}
|
|
2272
|
+
}
|
|
1212
2273
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
2274
|
+
async function cloneRepository() {
|
|
2275
|
+
const confirmed = await openModal({
|
|
2276
|
+
title: 'Cloner le dépôt ?',
|
|
2277
|
+
body: 'Le clonage se fait en arrière-plan dans le répertoire de clonage. Enregistrez vos modifications avant de lancer l\u2019opération.',
|
|
2278
|
+
pathText: state.extras.CLONE_DIR || undefined,
|
|
2279
|
+
confirmLabel: 'Cloner'
|
|
2280
|
+
});
|
|
2281
|
+
if (!confirmed) return;
|
|
2282
|
+
|
|
2283
|
+
const label = $('btnCloneLabel');
|
|
2284
|
+
const original = label.textContent;
|
|
2285
|
+
label.textContent = 'Clonage…';
|
|
2286
|
+
$('btnClone').disabled = true;
|
|
1217
2287
|
|
|
1218
|
-
const originalHTML = deleteWorkingDirBtn.innerHTML;
|
|
1219
2288
|
try {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
const response = await fetch(api('api/delete-working-dir'), { method: 'POST' });
|
|
1224
|
-
|
|
1225
|
-
if (response.ok) {
|
|
1226
|
-
showStatus('Dossier de travail supprimé avec succès.', 'success');
|
|
1227
|
-
currentConfig.REPO_PATH = '';
|
|
1228
|
-
deleteWorkingDirBtn.disabled = true;
|
|
1229
|
-
} else {
|
|
1230
|
-
const data = await response.json().catch(() => ({ error: 'Erreur lors de la suppression' }));
|
|
1231
|
-
throw new Error(data.error || 'Erreur lors de la suppression');
|
|
1232
|
-
}
|
|
2289
|
+
await request('api/clone', { method: 'POST' });
|
|
2290
|
+
logActivity('neutral', `Clonage lancé pour ${state.config.TARGET_REPO}`);
|
|
2291
|
+
toast({ type: 'success', title: 'Clonage lancé', detail: 'Le processus tourne en arrière-plan.' });
|
|
1233
2292
|
} catch (error) {
|
|
1234
|
-
|
|
2293
|
+
toast({ type: 'error', title: 'Clonage impossible', detail: error.message });
|
|
1235
2294
|
} finally {
|
|
1236
|
-
|
|
1237
|
-
|
|
2295
|
+
label.textContent = original;
|
|
2296
|
+
refreshDerived();
|
|
1238
2297
|
}
|
|
1239
|
-
}
|
|
2298
|
+
}
|
|
1240
2299
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
2300
|
+
/* =============================================================== */
|
|
2301
|
+
/* Démarrage */
|
|
2302
|
+
/* =============================================================== */
|
|
2303
|
+
|
|
2304
|
+
async function reloadEverything() {
|
|
2305
|
+
await loadConfigs();
|
|
2306
|
+
await loadConfig();
|
|
2307
|
+
renderChrome();
|
|
2308
|
+
|
|
2309
|
+
if (state.config.TARGET_REPO) {
|
|
2310
|
+
fetchBranches(state.config.TARGET_REPO).then(() => {
|
|
2311
|
+
if (state.panel === 'repo') renderRepoPanel();
|
|
2312
|
+
refreshDerived();
|
|
2313
|
+
});
|
|
1244
2314
|
}
|
|
1245
2315
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
2316
|
+
fetchRepositories().catch(() => { /* la palette signalera l'erreur */ });
|
|
2317
|
+
showPanel(state.panel);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
function bindChrome() {
|
|
2321
|
+
$('configSwitcher').addEventListener('click', openConfigPalette);
|
|
2322
|
+
$('btnSave').addEventListener('click', saveConfiguration);
|
|
2323
|
+
$('btnDiscard').addEventListener('click', discardChanges);
|
|
2324
|
+
$('btnClone').addEventListener('click', cloneRepository);
|
|
2325
|
+
$('btnRestart').addEventListener('click', restartServer);
|
|
2326
|
+
$('btnClean').addEventListener('click', cleanWorkingDir);
|
|
2327
|
+
$('btnStop').addEventListener('click', stopServer);
|
|
2328
|
+
|
|
2329
|
+
document.querySelectorAll('.nav-item').forEach((item) => {
|
|
2330
|
+
item.addEventListener('click', () => showPanel(item.dataset.panel));
|
|
2331
|
+
});
|
|
1250
2332
|
|
|
1251
|
-
|
|
2333
|
+
document.addEventListener('keydown', (event) => {
|
|
2334
|
+
const typing = /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName);
|
|
2335
|
+
const modalOpen = $('modalRoot').childElementCount > 0;
|
|
2336
|
+
if (modalOpen) return;
|
|
2337
|
+
|
|
2338
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
|
|
2339
|
+
event.preventDefault();
|
|
2340
|
+
if (!$('btnSave').disabled) saveConfiguration();
|
|
2341
|
+
} else if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
|
|
2342
|
+
event.preventDefault();
|
|
2343
|
+
openRepoPalette();
|
|
2344
|
+
} else if (event.key === '/' && !typing && !palette) {
|
|
2345
|
+
event.preventDefault();
|
|
2346
|
+
openRepoPalette();
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
1252
2349
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
throw new Error(data.error || 'Erreur lors du clonage');
|
|
2350
|
+
window.addEventListener('beforeunload', (event) => {
|
|
2351
|
+
if (changedFields().length) {
|
|
2352
|
+
event.preventDefault();
|
|
2353
|
+
event.returnValue = '';
|
|
1258
2354
|
}
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
async function boot() {
|
|
2359
|
+
bindChrome();
|
|
2360
|
+
renderStatus();
|
|
2361
|
+
try {
|
|
2362
|
+
await reloadEverything();
|
|
1259
2363
|
} catch (error) {
|
|
1260
|
-
|
|
1261
|
-
cloneBtn.disabled = false;
|
|
1262
|
-
} finally {
|
|
1263
|
-
cloneBtn.innerHTML = originalHTML;
|
|
2364
|
+
toast({ type: 'error', title: 'Chargement impossible', detail: error.message, sticky: true });
|
|
1264
2365
|
}
|
|
1265
|
-
|
|
2366
|
+
checkHealth();
|
|
2367
|
+
setInterval(checkHealth, 15000);
|
|
2368
|
+
setInterval(() => { if (state.panel === 'deploy') renderActivity(); }, 30000);
|
|
2369
|
+
}
|
|
1266
2370
|
|
|
1267
|
-
|
|
2371
|
+
boot();
|
|
2372
|
+
})();
|
|
1268
2373
|
</script>
|
|
1269
2374
|
</body>
|
|
1270
2375
|
</html>
|