om4r932 commited on
Commit
cf82bf3
·
1 Parent(s): 25226b8

Split HTML/CSS/JS + SSE for TDocs only

Browse files
Files changed (5) hide show
  1. app.py +25 -10
  2. classes.py +18 -4
  3. index.html +55 -355
  4. static/script.js +242 -0
  5. static/style.css +132 -0
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import requests, re, warnings
2
  from dotenv import load_dotenv
3
  from fastapi import FastAPI, Request, HTTPException
@@ -55,6 +56,7 @@ spec_etsi_indexer = SpecETSIIndexer()
55
 
56
  app = FastAPI()
57
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=["*"], allow_origins=["*"])
 
58
 
59
  @app.get('/')
60
  def main():
@@ -91,9 +93,13 @@ def index_tdocs_wg_progress(req: IndexTDoc):
91
  category, wg_number = get_folder_name(req.wg)
92
  folder = meetings_mapping[category][wg_number]
93
  url = f"https://www.3gpp.org/ftp/{meetings_mapping[category][0]}"
94
-
95
- tdoc_indexer.process_workgroup(folder, url)
96
- return {"status": "OK"}
 
 
 
 
97
 
98
  @app.post("/index_tdocs/meeting")
99
  def index_tdocs_meeting_progress(req: IndexTDoc):
@@ -105,17 +111,26 @@ def index_tdocs_meeting_progress(req: IndexTDoc):
105
  category, wg_number = get_folder_name(req.wg)
106
  folder = meetings_mapping[category][wg_number]
107
  url = f"https://www.3gpp.org/ftp/{meetings_mapping[category][0]}/{folder}"
108
- for i, meet in enumerate(req.meetings):
109
- tdoc_indexer.process_meeting(meet, url)
110
- tdoc_indexer.save_indexer()
111
- return {"status": "OK"}
 
 
 
 
 
112
 
113
 
114
  @app.post("/index_tdocs/all")
115
  def index_all_tdocs_progress():
116
- tdoc_indexer.index_all_tdocs()
117
- return {"status": "OK"}
118
-
 
 
 
 
119
 
120
  @app.post("/index_specs/3gpp")
121
  def index_3gpp_specs_progress():
 
1
+ from fastapi.staticfiles import StaticFiles
2
  import requests, re, warnings
3
  from dotenv import load_dotenv
4
  from fastapi import FastAPI, Request, HTTPException
 
56
 
57
  app = FastAPI()
58
  app.add_middleware(CORSMiddleware, allow_credentials=True, allow_headers=["*"], allow_origins=["*"])
59
+ app.mount("/static", StaticFiles(directory="static"), name="static")
60
 
61
  @app.get('/')
62
  def main():
 
93
  category, wg_number = get_folder_name(req.wg)
94
  folder = meetings_mapping[category][wg_number]
95
  url = f"https://www.3gpp.org/ftp/{meetings_mapping[category][0]}"
96
+ def generate_events():
97
+ yield f"event: info\ndata: {req.wg}\n\n"
98
+ for content in tdoc_indexer.process_workgroup(folder, url):
99
+ yield content
100
+ tdoc_indexer.save_indexer()
101
+ yield "event: end\ndata: Indexation terminé !\n\n"
102
+ return StreamingResponse(generate_events(), media_type="text/event-stream")
103
 
104
  @app.post("/index_tdocs/meeting")
105
  def index_tdocs_meeting_progress(req: IndexTDoc):
 
111
  category, wg_number = get_folder_name(req.wg)
112
  folder = meetings_mapping[category][wg_number]
113
  url = f"https://www.3gpp.org/ftp/{meetings_mapping[category][0]}/{folder}"
114
+ def generate_events():
115
+ yield f"event: get-maximum\ndata: {len(req.meetings)}\n\n"
116
+ for i, meet in enumerate(req.meetings):
117
+ yield f"event: info\ndata: {req.wg}-{meet}\n\n"
118
+ tdoc_indexer.process_meeting(meet, url)
119
+ yield f"event: progress\ndata: {i+1}\n\n"
120
+ tdoc_indexer.save_indexer()
121
+ yield "event: end\ndata: Indexation terminé !\n\n"
122
+ return StreamingResponse(generate_events(), media_type="text/event-stream")
123
 
124
 
125
  @app.post("/index_tdocs/all")
126
  def index_all_tdocs_progress():
127
+ def generate_events():
128
+ for content in tdoc_indexer.index_all_tdocs():
129
+ yield content
130
+ tdoc_indexer.save_indexer()
131
+ yield "event: end\ndata: Indexation terminé !\n\n"
132
+ return StreamingResponse(generate_events(), media_type="text/event-stream")
133
+
134
 
135
  @app.post("/index_specs/3gpp")
136
  def index_3gpp_specs_progress():
classes.py CHANGED
@@ -196,8 +196,13 @@ class TDocIndexer:
196
  futures = [executor.submit(self.process_meeting, meeting, wg_url)
197
  for meeting in meeting_folders if meeting not in ['./', '../']]
198
 
199
- # Attendre que toutes les tâches soient terminées
200
- concurrent.futures.wait(futures)
 
 
 
 
 
201
 
202
  def index_all_tdocs(self):
203
  """Indexer tous les documents ZIP dans la structure FTP 3GPP avec multithreading"""
@@ -220,7 +225,9 @@ class TDocIndexer:
220
  # Traiter chaque groupe de travail séquentiellement
221
  # (mais les réunions à l'intérieur seront traitées en parallèle)
222
  for wg in workgroups:
223
- self.process_workgroup(wg, main_url)
 
 
224
 
225
  docs_count_after = len(self.indexer)
226
  new_docs_count = abs(docs_count_after - docs_count_before)
@@ -249,7 +256,14 @@ class TDocIndexer:
249
  with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
250
  futures = [executor.submit(self.process_meeting, meeting, main_url, workshop=True)
251
  for meeting in meeting_folders if meeting not in ['./', '../']]
252
- concurrent.futures.wait(futures)
 
 
 
 
 
 
 
253
 
254
  docs_count_after = len(self.indexer)
255
  new_docs_count = docs_count_after - docs_count_before
 
196
  futures = [executor.submit(self.process_meeting, meeting, wg_url)
197
  for meeting in meeting_folders if meeting not in ['./', '../']]
198
 
199
+ total = len(futures)
200
+ done_count = 0
201
+ yield f"event: get-maximum\ndata: {total}\n\n"
202
+
203
+ for future in concurrent.futures.as_completed(futures):
204
+ done_count += 1
205
+ yield f"event: progress\ndata: {done_count}\n\n"
206
 
207
  def index_all_tdocs(self):
208
  """Indexer tous les documents ZIP dans la structure FTP 3GPP avec multithreading"""
 
225
  # Traiter chaque groupe de travail séquentiellement
226
  # (mais les réunions à l'intérieur seront traitées en parallèle)
227
  for wg in workgroups:
228
+ yield f"event: info\ndata: {main_tsg}-{wg}\n\n"
229
+ for content in self.process_workgroup(wg, main_url):
230
+ yield content
231
 
232
  docs_count_after = len(self.indexer)
233
  new_docs_count = abs(docs_count_after - docs_count_before)
 
256
  with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
257
  futures = [executor.submit(self.process_meeting, meeting, main_url, workshop=True)
258
  for meeting in meeting_folders if meeting not in ['./', '../']]
259
+ total = len(futures)
260
+ done_count = 0
261
+
262
+ yield f"event: get-maximum\ndata: {total}\n\n"
263
+
264
+ for future in concurrent.futures.as_completed(futures):
265
+ done_count += 1
266
+ yield f"event: progress\ndata: {done_count}\n\n"
267
 
268
  docs_count_after = len(self.indexer)
269
  new_docs_count = docs_count_after - docs_count_before
index.html CHANGED
@@ -1,367 +1,67 @@
1
  <!DOCTYPE html>
2
  <html lang="fr">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>3GPP/ETSI Document Indexer Main Menu</title>
7
- <style>
8
- body {
9
- font-family: "Montserrat", sans-serif;
10
- background: #fafafa;
11
- margin: 24px;
12
- color: #1f2937;
13
- }
14
- h1 {
15
- font-size: 1.8rem;
16
- margin-bottom: 24px;
17
- }
18
- .row {
19
- display: flex;
20
- gap: 24px;
21
- margin-bottom: 24px;
22
- }
23
- .column {
24
- flex: 1;
25
- display: flex;
26
- flex-direction: column;
27
- gap: 12px;
28
- }
29
- button {
30
- background-color: #6c63ff;
31
- color: white;
32
- font-weight: 600;
33
- font-size: 1rem;
34
- padding: 10px 14px;
35
- border: none;
36
- border-radius: 0.6em;
37
- cursor: pointer;
38
- box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
39
- transition: background-color 0.2s ease;
40
- }
41
- button:hover {
42
- background-color: #5753d6;
43
- }
44
- button:disabled {
45
- cursor: default;
46
- background-color: #778191;
47
- }
48
- select {
49
- padding: 10px 14px;
50
- border-radius: 0.6em;
51
- border: none;
52
- box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
53
- font-size: 1rem;
54
- color: #374151;
55
- background: #f3f4f6;
56
- appearance: none;
57
- cursor: pointer;
58
- }
59
- select:focus {
60
- outline: none;
61
- box-shadow: 0 0 0 2px #6c63ff;
62
- background: white;
63
- }
64
- select:hover {
65
- background: #e5e7eb;
66
- }
67
- select:disabled {
68
- cursor: default;
69
- }
70
-
71
- .dropdown-content {
72
- position: absolute; /* ou fixed si tu veux */
73
- z-index: 9999; /* un nombre élevé pour être sûr que c'est au dessus */
74
- background-color: white; /* pour que ce soit bien visible */
75
- border: 1px solid #ccc;
76
- /* autres styles que tu avais déjà */
77
- border-radius: 0.6em;
78
- box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
79
- padding: 10px;
80
- max-height: 55vh;
81
- overflow-y: auto;
82
- }
83
-
84
- #dropbtn {
85
- background: #f3f4f6;
86
- color: #374151;
87
- font-size: 1rem;
88
- font-family: "Montserrat", sans-serif; /* même font que body */
89
- padding: 10px 14px;
90
- border-radius: 0.6em;
91
- font-weight: normal;
92
- border: none;
93
- box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
94
- cursor: pointer;
95
- width: 100%;
96
- text-align: left;
97
- appearance: none; /* supprime les styles natives du bouton */
98
- user-select: none;
99
- transition: background-color 0.2s ease;
100
- display: inline-block;
101
- }
102
-
103
- #dropbtn:hover {
104
- background: #e5e7eb;
105
- }
106
 
107
- #dropbtn:disabled {
108
- cursor: default;
109
- }
110
-
111
- #dropbtn:focus {
112
- outline: none;
113
- box-shadow: 0 0 0 2px #6c63ff;
114
- background: white;
115
- }
116
-
117
- option {
118
- background: white;
119
- }
120
- textarea {
121
- width: 100%;
122
- min-height: 450px;
123
- border-radius: 0.6em;
124
- border: none;
125
- box-shadow: 0 2px 6px rgb(31 41 55 / 12%);
126
- padding: 12px;
127
- font-family: monospace, monospace;
128
- font-size: 0.95rem;
129
- color: #1f2937;
130
- resize: vertical;
131
- background: white;
132
- }
133
- textarea[readonly] {
134
- background: #e5e7eb;
135
- cursor: default;
136
- }
137
- </style>
138
  </head>
139
- <body>
140
 
141
- <h1>📄 3GPP/ETSI Document/Specification Indexer Main Menu</h1>
142
 
143
- <div class="row" id="r1">
144
- <div class="column">
145
- <button id="tdocs-btn">Re-index TDocs</button>
146
- <button id="spec-3gpp-btn">Re-index 3GPP Specifications</button>
147
- </div>
148
- <div class="column">
149
- <select id="tdocs-wg-option" aria-label="Options Working Group TDocs">
150
- <option value="ALL" selected>Index all working groups</option>
151
- <option value="SA0">SP</option>
152
- <option value="SA1">SA1</option>
153
- <option value="SA2">SA2</option>
154
- <option value="SA3">SA3</option>
155
- <option value="SA4">SA4</option>
156
- <option value="SA5">SA5</option>
157
- <option value="SA6">SA6</option>
158
- <option value="CT0">CP</option>
159
- <option value="CT1">CT1</option>
160
- <option value="CT2">CT2</option>
161
- <option value="CT3">CT3</option>
162
- <option value="CT4">CT4</option>
163
- <option value="CT5">CT5</option>
164
- <option value="CT6">CT6</option>
165
- <option value="RAN0">RP</option>
166
- <option value="RAN1">RAN1</option>
167
- <option value="RAN2">RAN2</option>
168
- <option value="RAN3">RAN3</option>
169
- <option value="RAN4">RAN4</option>
170
- <option value="RAN5">RAN5</option>
171
- <option value="RAN6">RAN6</option>
172
- </select>
173
-
174
- </div>
175
- <div class="column">
176
- <div class="dropdown">
177
- <button id="dropbtn" disabled="disabled">Index all meetings</button>
178
- <div id="dropdownContent" class="dropdown-content" style="display:none;">
179
- <label style="display:none;"><input type="checkbox" checked value="ALL">Index all meetings</label>
180
- </div>
 
 
 
 
 
181
  </div>
182
- <button id="spec-etsi-btn">Re-index ETSI Specifications</button>
183
- </div>
184
- </div>
185
-
186
- <textarea id="output" readonly placeholder="Output..." aria-label="Output console"></textarea>
187
-
188
- <script type="module">
189
- const output = document.getElementById('output');
190
- let selectedMeetings = [];
191
- let currentURL = null;
192
-
193
- function toggleDropdown() {
194
- const dropdown = document.getElementById("dropdownContent");
195
- dropdown.style.display = (dropdown.style.display === "none") ? "block" : "none";
196
- }
197
-
198
- document.getElementById('dropbtn').addEventListener('click', ()=>{toggleDropdown()})
199
- document.addEventListener('mousedown', (e)=>{
200
- if(document.getElementById("dropdownContent").style.display == "block" && e.target.className != "dropdown-content" && e.target.tagName != "INPUT" && e.target.tagName != "LABEL"){
201
- document.getElementById("dropdownContent").style.display = "none";
202
- }
203
- })
204
-
205
- function logMessage(msg, reset){
206
- if(reset){
207
- output.value = msg + "\n";
208
- };
209
- output.value += msg + '\n';
210
- output.scrollTop = output.scrollHeight;
211
- }
212
-
213
- document.getElementById('tdocs-wg-option').addEventListener('change', async (e) => {
214
- let wg = e.target.value;
215
- const dropdownContent = document.getElementById('dropdownContent');
216
- const dropbtn = document.getElementById('dropbtn');
217
 
218
- if (wg != "ALL") {
219
- dropdownContent.innerHTML = '<label style="display:none;"><input type="checkbox" checked value="ALL">Index all meetings</label>';
220
- const response = await fetch(`/get_meetings/${wg}`, { method: "GET" });
221
- const responseJson = await response.json();
222
- const meetings = responseJson.meetings;
223
- currentURL = responseJson.url;
224
 
225
- for (const meet of meetings) {
226
- const label = document.createElement('label');
227
- const checkbox = document.createElement('input');
228
- checkbox.type = "checkbox";
229
- checkbox.value = meet;
230
- label.appendChild(checkbox);
231
- label.appendChild(document.createTextNode(meet));
232
- dropdownContent.appendChild(label);
233
- dropdownContent.appendChild(document.createElement('br'));
234
- }
235
- dropbtn.removeAttribute('disabled');
236
-
237
- // après création, ajoute les listeners de gestion sur chaque checkbox
238
- initCheckboxListeners();
239
- // Initialise l'état initial
240
- updateDropbtnLabel();
241
- } else {
242
- dropdownContent.innerHTML = '<label style="display:none;"><input type="checkbox" checked value="ALL">Index all meetings</label>';
243
- dropbtn.setAttribute('disabled', 'true');
244
- dropbtn.textContent = "Index all meetings";
245
- }
246
- });
247
-
248
- function disableButtons(){
249
- document.getElementById("spec-3gpp-btn").setAttribute('disabled', 'disabled')
250
- document.getElementById("spec-etsi-btn").setAttribute('disabled', 'disabled')
251
- document.getElementById("tdocs-btn").setAttribute('disabled', 'disabled')
252
- }
253
-
254
- function enableButtons(){
255
- document.getElementById("spec-3gpp-btn").removeAttribute('disabled')
256
- document.getElementById("spec-etsi-btn").removeAttribute('disabled')
257
- document.getElementById("tdocs-btn").removeAttribute('disabled')
258
- }
259
-
260
- function initCheckboxListeners() {
261
- const dropdownContent = document.getElementById('dropdownContent');
262
- const dropbtn = document.getElementById('dropbtn');
263
-
264
- function updateState() {
265
- const checkboxes = dropdownContent.querySelectorAll('input[type="checkbox"]');
266
- const allCheckbox = dropdownContent.querySelector('input[value="ALL"]');
267
- const checkedBoxes = Array.from(checkboxes).filter(cb => cb.checked && cb !== allCheckbox);
268
-
269
- if (checkedBoxes.length === 0) {
270
- allCheckbox.checked = true;
271
- dropbtn.textContent = "Index all meetings";
272
- selectedMeetings = ["ALL"];
273
- } else {
274
- if (allCheckbox.checked) {
275
- allCheckbox.checked = false; // décocher ALL si autre(s) cochée(s)
276
- }
277
- if (checkedBoxes.length === 1) {
278
- dropbtn.textContent = checkedBoxes[0].value;
279
- } else {
280
- dropbtn.textContent = `${checkedBoxes.length} meetings sélectionnés`;
281
- }
282
- selectedMeetings = checkedBoxes.map(cb => cb.value);
283
- }
284
-
285
- console.log(selectedMeetings);
286
- console.log(currentURL);
287
- }
288
-
289
- const checkboxes = dropdownContent.querySelectorAll('input[type="checkbox"]');
290
- checkboxes.forEach(cb => cb.addEventListener('change', updateState));
291
-
292
- updateState(); // mise à jour initiale
293
- }
294
- function updateDropbtnLabel() {
295
- const dropdownContent = document.getElementById('dropdownContent');
296
- const checkboxes = dropdownContent.querySelectorAll('input[type="checkbox"]');
297
- const allCheckbox = dropdownContent.querySelector('input[value="ALL"]');
298
- const dropbtn = document.getElementById('dropbtn');
299
- const checkedBoxes = Array.from(checkboxes).filter(cb => cb.checked && cb !== allCheckbox);
300
-
301
- if (checkedBoxes.length === 0) {
302
- allCheckbox.checked = true;
303
- dropbtn.textContent = "Index all meetings";
304
- } else if (checkedBoxes.length === 1) {
305
- allCheckbox.checked = false;
306
- dropbtn.textContent = checkedBoxes[0].value;
307
- } else {
308
- allCheckbox.checked = false;
309
- dropbtn.textContent = `${checkedBoxes.length} meetings sélectionnés`;
310
- }
311
- }
312
-
313
-
314
- document.getElementById('tdocs-btn').addEventListener('click', () => {
315
- disableButtons()
316
- logMessage(`Started re-indexing TDocs`);
317
- if(currentURL){
318
- if(!selectedMeetings.includes("ALL")){
319
- fetch("/index_tdocs/meeting", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({wg: document.getElementById("tdocs-wg-option").value, meetings: selectedMeetings})})
320
- .then(resp => resp.text())
321
- .then(data => {
322
- logMessage("Index successful")
323
- enableButtons()
324
- })
325
- } else {
326
- fetch("/index_tdocs/working_group", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({wg: document.getElementById("tdocs-wg-option").value})})
327
- .then(resp => resp.text())
328
- .then(data => {
329
- logMessage("Index successful")
330
- enableButtons()
331
- })
332
- }
333
- } else {
334
- fetch("/index_tdocs/all", {method: "POST", headers: {"Content-Type": "application/json"}})
335
- .then(resp => resp.text())
336
- .then(data => {
337
- logMessage("Index successful")
338
- enableButtons()
339
- })
340
- }
341
- });
342
-
343
- document.getElementById('spec-3gpp-btn').addEventListener('click', () => {
344
- disableButtons()
345
- logMessage(`Started re-indexing 3GPP Specifications`);
346
- fetch("/index_specs/3gpp", {method: "POST", headers: {"Content-Type": "application/json"}})
347
- .then(resp => resp.text())
348
- .then(data => {
349
- logMessage("Index successful")
350
- enableButtons()
351
- })
352
- });
353
-
354
- document.getElementById('spec-etsi-btn').addEventListener('click', () => {
355
- logMessage('Started re-indexing ETSI Specifications');
356
- disableButtons()
357
- fetch("/index_specs/etsi", {method: "POST", headers: {"Content-Type": "application/json"}})
358
- .then(resp => resp.text())
359
- .then(data => {
360
- logMessage("Index successful")
361
- enableButtons()
362
- })
363
- });
364
- </script>
365
 
366
  </body>
 
 
367
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="fr">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>3GPP/ETSI Document Indexer Main Menu</title>
8
+ <link rel="stylesheet" href="/static/style.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  </head>
 
10
 
11
+ <body>
12
 
13
+ <h1>📄 3GPP/ETSI Document/Specification Indexer Main Menu</h1>
14
+
15
+ <div class="row" id="r1">
16
+ <div class="column">
17
+ <button id="tdocs-btn">Re-index TDocs</button>
18
+ <button id="spec-3gpp-btn">Re-index 3GPP Specifications</button>
19
+ </div>
20
+ <div class="column">
21
+ <select id="tdocs-wg-option" aria-label="Options Working Group TDocs">
22
+ <option value="ALL" selected>Index all working groups</option>
23
+ <option value="SA0">SP</option>
24
+ <option value="SA1">SA1</option>
25
+ <option value="SA2">SA2</option>
26
+ <option value="SA3">SA3</option>
27
+ <option value="SA4">SA4</option>
28
+ <option value="SA5">SA5</option>
29
+ <option value="SA6">SA6</option>
30
+ <option value="CT0">CP</option>
31
+ <option value="CT1">CT1</option>
32
+ <option value="CT2">CT2</option>
33
+ <option value="CT3">CT3</option>
34
+ <option value="CT4">CT4</option>
35
+ <option value="CT5">CT5</option>
36
+ <option value="CT6">CT6</option>
37
+ <option value="RAN0">RP</option>
38
+ <option value="RAN1">RAN1</option>
39
+ <option value="RAN2">RAN2</option>
40
+ <option value="RAN3">RAN3</option>
41
+ <option value="RAN4">RAN4</option>
42
+ <option value="RAN5">RAN5</option>
43
+ <option value="RAN6">RAN6</option>
44
+ </select>
45
+
46
+ </div>
47
+ <div class="column">
48
+ <div class="dropdown">
49
+ <button id="dropbtn" disabled="disabled">Index all meetings</button>
50
+ <div id="dropdownContent" class="dropdown-content" style="display:none;">
51
+ <label style="display:none;"><input type="checkbox" checked value="ALL">Index all meetings</label>
52
+ </div>
53
+ </div>
54
+ <button id="spec-etsi-btn">Re-index ETSI Specifications</button>
55
+ </div>
56
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ <center>
59
+ <p id="infoText">No documents indexed</p>
60
+ <progress id="indexProgression" value="0" max="50"></progress>
61
+ </center>
 
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  </body>
65
+ <script src="/static/script.js"></script>
66
+
67
  </html>
static/script.js ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let selectedMeetings = [];
2
+ let currentURL = null;
3
+
4
+ function toggleDropdown() {
5
+ const dropdown = document.getElementById("dropdownContent");
6
+ dropdown.style.display = (dropdown.style.display === "none") ? "block" : "none";
7
+ }
8
+
9
+ document.getElementById('dropbtn').addEventListener('click', () => {
10
+ toggleDropdown()
11
+ })
12
+ document.addEventListener('mousedown', (e) => {
13
+ if (document.getElementById("dropdownContent").style.display == "block" && e.target.className != "dropdown-content" && e.target.tagName != "INPUT" && e.target.tagName != "LABEL") {
14
+ document.getElementById("dropdownContent").style.display = "none";
15
+ }
16
+ })
17
+
18
+ document.getElementById('tdocs-wg-option').addEventListener('change', async (e) => {
19
+ let wg = e.target.value;
20
+ const dropdownContent = document.getElementById('dropdownContent');
21
+ const dropbtn = document.getElementById('dropbtn');
22
+
23
+ if (wg != "ALL") {
24
+ dropdownContent.innerHTML = '<label style="display:none;"><input type="checkbox" checked value="ALL">Index all meetings</label>';
25
+ const response = await fetch(`/get_meetings/${wg}`, {
26
+ method: "GET"
27
+ });
28
+ const responseJson = await response.json();
29
+ const meetings = responseJson.meetings;
30
+ currentURL = responseJson.url;
31
+
32
+ for (const meet of meetings) {
33
+ const label = document.createElement('label');
34
+ const checkbox = document.createElement('input');
35
+ checkbox.type = "checkbox";
36
+ checkbox.value = meet;
37
+ label.appendChild(checkbox);
38
+ label.appendChild(document.createTextNode(meet));
39
+ dropdownContent.appendChild(label);
40
+ dropdownContent.appendChild(document.createElement('br'));
41
+ }
42
+ dropbtn.removeAttribute('disabled');
43
+
44
+ // après création, ajoute les listeners de gestion sur chaque checkbox
45
+ initCheckboxListeners();
46
+ // Initialise l'état initial
47
+ updateDropbtnLabel();
48
+ } else {
49
+ dropdownContent.innerHTML = '<label style="display:none;"><input type="checkbox" checked value="ALL">Index all meetings</label>';
50
+ dropbtn.setAttribute('disabled', 'true');
51
+ dropbtn.textContent = "Index all meetings";
52
+ }
53
+ });
54
+
55
+ function disableButtons() {
56
+ document.getElementById("spec-3gpp-btn").setAttribute('disabled', 'disabled')
57
+ document.getElementById("spec-etsi-btn").setAttribute('disabled', 'disabled')
58
+ document.getElementById("tdocs-btn").setAttribute('disabled', 'disabled')
59
+ }
60
+
61
+ function enableButtons() {
62
+ document.getElementById("spec-3gpp-btn").removeAttribute('disabled')
63
+ document.getElementById("spec-etsi-btn").removeAttribute('disabled')
64
+ document.getElementById("tdocs-btn").removeAttribute('disabled')
65
+ }
66
+
67
+ function initCheckboxListeners() {
68
+ const dropdownContent = document.getElementById('dropdownContent');
69
+ const dropbtn = document.getElementById('dropbtn');
70
+
71
+ function updateState() {
72
+ const checkboxes = dropdownContent.querySelectorAll('input[type="checkbox"]');
73
+ const allCheckbox = dropdownContent.querySelector('input[value="ALL"]');
74
+ const checkedBoxes = Array.from(checkboxes).filter(cb => cb.checked && cb !== allCheckbox);
75
+
76
+ if (checkedBoxes.length === 0) {
77
+ allCheckbox.checked = true;
78
+ dropbtn.textContent = "Index all meetings";
79
+ selectedMeetings = ["ALL"];
80
+ } else {
81
+ if (allCheckbox.checked) {
82
+ allCheckbox.checked = false; // décocher ALL si autre(s) cochée(s)
83
+ }
84
+ if (checkedBoxes.length === 1) {
85
+ dropbtn.textContent = checkedBoxes[0].value;
86
+ } else {
87
+ dropbtn.textContent = `${checkedBoxes.length} meetings sélectionnés`;
88
+ }
89
+ selectedMeetings = checkedBoxes.map(cb => cb.value);
90
+ }
91
+
92
+ console.log(selectedMeetings);
93
+ console.log(currentURL);
94
+ }
95
+
96
+ const checkboxes = dropdownContent.querySelectorAll('input[type="checkbox"]');
97
+ checkboxes.forEach(cb => cb.addEventListener('change', updateState));
98
+
99
+ updateState(); // mise à jour initiale
100
+ }
101
+
102
+ function updateDropbtnLabel() {
103
+ const dropdownContent = document.getElementById('dropdownContent');
104
+ const checkboxes = dropdownContent.querySelectorAll('input[type="checkbox"]');
105
+ const allCheckbox = dropdownContent.querySelector('input[value="ALL"]');
106
+ const dropbtn = document.getElementById('dropbtn');
107
+ const checkedBoxes = Array.from(checkboxes).filter(cb => cb.checked && cb !== allCheckbox);
108
+
109
+ if (checkedBoxes.length === 0) {
110
+ allCheckbox.checked = true;
111
+ dropbtn.textContent = "Index all meetings";
112
+ } else if (checkedBoxes.length === 1) {
113
+ allCheckbox.checked = false;
114
+ dropbtn.textContent = checkedBoxes[0].value;
115
+ } else {
116
+ allCheckbox.checked = false;
117
+ dropbtn.textContent = `${checkedBoxes.length} meetings sélectionnés`;
118
+ }
119
+ }
120
+ const progress = document.getElementById('indexProgression')
121
+
122
+ document.getElementById('tdocs-btn').addEventListener('click', async () => {
123
+ disableButtons()
124
+ let response = null;
125
+ if (currentURL) {
126
+ if (!selectedMeetings.includes("ALL")) {
127
+ response = await fetch("/index_tdocs/meeting", {
128
+ method: "POST",
129
+ body: JSON.stringify({
130
+ wg: document.getElementById("tdocs-wg-option").value,
131
+ meetings: selectedMeetings
132
+ }),
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ "Accept": "text/event-stream",
136
+ },
137
+ });
138
+ } else {
139
+ response = await fetch("/index_tdocs/working_group", {
140
+ method: "POST",
141
+ body: JSON.stringify({
142
+ wg: document.getElementById("tdocs-wg-option").value
143
+ }),
144
+ headers: {
145
+ "Content-Type": "application/json",
146
+ "Accept": "text/event-stream",
147
+ },
148
+ });
149
+ }
150
+ } else {
151
+ response = await fetch("/index_tdocs/all", {
152
+ method: "POST",
153
+ headers: {
154
+ "Content-Type": "application/json",
155
+ "Accept": "text/event-stream",
156
+ },
157
+ });
158
+ }
159
+
160
+ const reader = response.body.getReader();
161
+ const decoder = new TextDecoder("utf-8");
162
+ let buffer = "";
163
+
164
+ while (true) {
165
+ const {
166
+ done,
167
+ value
168
+ } = await reader.read();
169
+ if (done) break;
170
+
171
+ buffer += decoder.decode(value, {
172
+ stream: true
173
+ });
174
+
175
+ let events = buffer.split("\n\n"); // séparer les événements
176
+ buffer = events.pop(); // garder le reste pour le prochain chunk
177
+
178
+ for (const rawEvent of events) {
179
+ const event = {};
180
+ rawEvent.split("\n").forEach((line) => {
181
+ const [key, ...rest] = line.split(":");
182
+ if (key) event[key.trim()] = rest.join(":").trim();
183
+ });
184
+
185
+ // dispatch selon event.event ou par défaut message
186
+ switch (event.event) {
187
+ case "progress":
188
+ console.log("[progress]", event.data);
189
+ progress.value = event.data;
190
+ break;
191
+ case "get-maximum":
192
+ console.log("[new-max]", event.data);
193
+ progress.max = event.data
194
+ break;
195
+ case "info":
196
+ console.log("[info]", event.data);
197
+ document.getElementById('infoText').textContent = event.data
198
+ break;
199
+ case "end":
200
+ console.log("[end]", event.data);
201
+ document.getElementById('infoText').textContent = "Indexation successful"
202
+ break;
203
+ default:
204
+ if (event.data) {
205
+ console.log("[message]", event.data);
206
+ };
207
+ }
208
+ }
209
+ }
210
+
211
+ enableButtons()
212
+ });
213
+
214
+ document.getElementById('spec-3gpp-btn').addEventListener('click', () => {
215
+ disableButtons()
216
+ logMessage(`Started re-indexing 3GPP Specifications`);
217
+ fetch("/index_specs/3gpp", {
218
+ method: "POST",
219
+ headers: {
220
+ "Content-Type": "application/json"
221
+ }
222
+ })
223
+ .then(resp => resp.text())
224
+ .then(data => {
225
+ enableButtons()
226
+ })
227
+ });
228
+
229
+ document.getElementById('spec-etsi-btn').addEventListener('click', () => {
230
+ logMessage('Started re-indexing ETSI Specifications');
231
+ disableButtons()
232
+ fetch("/index_specs/etsi", {
233
+ method: "POST",
234
+ headers: {
235
+ "Content-Type": "application/json"
236
+ }
237
+ })
238
+ .then(resp => resp.text())
239
+ .then(data => {
240
+ enableButtons()
241
+ })
242
+ });
static/style.css ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: "Montserrat", sans-serif;
3
+ background: #fafafa;
4
+ margin: 24px;
5
+ color: #1f2937;
6
+ }
7
+
8
+ h1 {
9
+ font-size: 1.8rem;
10
+ margin-bottom: 24px;
11
+ }
12
+
13
+ .row {
14
+ display: flex;
15
+ gap: 24px;
16
+ margin-bottom: 24px;
17
+ }
18
+
19
+ .column {
20
+ flex: 1;
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 12px;
24
+ }
25
+
26
+ button {
27
+ background-color: #6c63ff;
28
+ color: white;
29
+ font-weight: 600;
30
+ font-size: 1rem;
31
+ padding: 10px 14px;
32
+ border: none;
33
+ border-radius: 0.6em;
34
+ cursor: pointer;
35
+ box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
36
+ transition: background-color 0.2s ease;
37
+ }
38
+
39
+ button:hover {
40
+ background-color: #5753d6;
41
+ }
42
+
43
+ button:disabled {
44
+ cursor: default;
45
+ background-color: #778191;
46
+ }
47
+
48
+ select {
49
+ padding: 10px 14px;
50
+ border-radius: 0.6em;
51
+ border: none;
52
+ box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
53
+ font-size: 1rem;
54
+ color: #374151;
55
+ background: #f3f4f6;
56
+ appearance: none;
57
+ cursor: pointer;
58
+ }
59
+
60
+ select:focus {
61
+ outline: none;
62
+ box-shadow: 0 0 0 2px #6c63ff;
63
+ background: white;
64
+ }
65
+
66
+ select:hover {
67
+ background: #e5e7eb;
68
+ }
69
+
70
+ select:disabled {
71
+ cursor: default;
72
+ }
73
+
74
+ .dropdown-content {
75
+ position: absolute;
76
+ /* ou fixed si tu veux */
77
+ z-index: 9999;
78
+ /* un nombre élevé pour être sûr que c'est au dessus */
79
+ background-color: white;
80
+ /* pour que ce soit bien visible */
81
+ border: 1px solid #ccc;
82
+ /* autres styles que tu avais déjà */
83
+ border-radius: 0.6em;
84
+ box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
85
+ padding: 10px;
86
+ max-height: 55vh;
87
+ overflow-y: auto;
88
+ }
89
+
90
+ #dropbtn {
91
+ background: #f3f4f6;
92
+ color: #374151;
93
+ font-size: 1rem;
94
+ font-family: "Montserrat", sans-serif;
95
+ /* même font que body */
96
+ padding: 10px 14px;
97
+ border-radius: 0.6em;
98
+ font-weight: normal;
99
+ border: none;
100
+ box-shadow: 0 2px 8px rgb(31 41 55 / 8%);
101
+ cursor: pointer;
102
+ width: 100%;
103
+ text-align: left;
104
+ appearance: none;
105
+ /* supprime les styles natives du bouton */
106
+ user-select: none;
107
+ transition: background-color 0.2s ease;
108
+ display: inline-block;
109
+ }
110
+
111
+ #dropbtn:hover {
112
+ background: #e5e7eb;
113
+ }
114
+
115
+ #dropbtn:disabled {
116
+ cursor: default;
117
+ }
118
+
119
+ #dropbtn:focus {
120
+ outline: none;
121
+ box-shadow: 0 0 0 2px #6c63ff;
122
+ background: white;
123
+ }
124
+
125
+ option {
126
+ background: white;
127
+ }
128
+
129
+ progress {
130
+ width: 300px;
131
+ height: 30px;
132
+ }