reviewflow 3.20.0 → 3.21.0

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.
Files changed (155) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/config/projectConfig.d.ts +7 -0
  3. package/dist/config/projectConfig.d.ts.map +1 -1
  4. package/dist/config/projectConfig.js +18 -0
  5. package/dist/config/projectConfig.js.map +1 -1
  6. package/dist/dashboard/index.html +374 -137
  7. package/dist/dashboard/modules/cardCounters.d.ts +32 -0
  8. package/dist/dashboard/modules/cardCounters.d.ts.map +1 -0
  9. package/dist/dashboard/modules/cardCounters.js +40 -0
  10. package/dist/dashboard/modules/cardCounters.js.map +1 -0
  11. package/dist/dashboard/modules/managePanel.d.ts +49 -0
  12. package/dist/dashboard/modules/managePanel.d.ts.map +1 -0
  13. package/dist/dashboard/modules/managePanel.js +123 -0
  14. package/dist/dashboard/modules/managePanel.js.map +1 -0
  15. package/dist/dashboard/modules/overview.d.ts +1 -0
  16. package/dist/dashboard/modules/overview.d.ts.map +1 -1
  17. package/dist/dashboard/modules/overview.js +14 -0
  18. package/dist/dashboard/modules/overview.js.map +1 -1
  19. package/dist/dashboard/modules/settingsModal.d.ts +77 -0
  20. package/dist/dashboard/modules/settingsModal.d.ts.map +1 -0
  21. package/dist/dashboard/modules/settingsModal.js +182 -0
  22. package/dist/dashboard/modules/settingsModal.js.map +1 -0
  23. package/dist/dashboard/modules/tabBar.d.ts +4 -0
  24. package/dist/dashboard/modules/tabBar.d.ts.map +1 -1
  25. package/dist/dashboard/modules/tabBar.js +6 -1
  26. package/dist/dashboard/modules/tabBar.js.map +1 -1
  27. package/dist/dashboard/styles.css +612 -0
  28. package/dist/frameworks/config/configLoader.d.ts +8 -0
  29. package/dist/frameworks/config/configLoader.d.ts.map +1 -1
  30. package/dist/frameworks/config/configLoader.js +18 -0
  31. package/dist/frameworks/config/configLoader.js.map +1 -1
  32. package/dist/main/routes.d.ts.map +1 -1
  33. package/dist/main/routes.js +47 -2
  34. package/dist/main/routes.js.map +1 -1
  35. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.d.ts +20 -0
  36. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.d.ts.map +1 -0
  37. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.js +2 -0
  38. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.js.map +1 -0
  39. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.d.ts +13 -0
  40. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.d.ts.map +1 -0
  41. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.js +2 -0
  42. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.js.map +1 -0
  43. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.d.ts +6 -1
  44. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.d.ts.map +1 -1
  45. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.js +116 -13
  46. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.js.map +1 -1
  47. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.d.ts +36 -0
  48. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.d.ts.map +1 -1
  49. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.js +77 -8
  50. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.js.map +1 -1
  51. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.d.ts +7 -0
  52. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.d.ts.map +1 -0
  53. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.js +48 -0
  54. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.js.map +1 -0
  55. package/dist/modules/cli-configuration/usecases/cli/addRepositoriesToConfig.usecase.d.ts +1 -6
  56. package/dist/modules/cli-configuration/usecases/cli/addRepositoriesToConfig.usecase.d.ts.map +1 -1
  57. package/dist/modules/cli-configuration/usecases/cli/addRepositoriesToConfig.usecase.js.map +1 -1
  58. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.d.ts +21 -0
  59. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.d.ts.map +1 -0
  60. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.js +27 -0
  61. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.js.map +1 -0
  62. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.d.ts +22 -0
  63. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.d.ts.map +1 -0
  64. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.js +27 -0
  65. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.js.map +1 -0
  66. package/dist/modules/cli-configuration/usecases/cli/writeInitConfig.usecase.d.ts +1 -6
  67. package/dist/modules/cli-configuration/usecases/cli/writeInitConfig.usecase.d.ts.map +1 -1
  68. package/dist/modules/cli-configuration/usecases/cli/writeInitConfig.usecase.js.map +1 -1
  69. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.d.ts +19 -0
  70. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.d.ts.map +1 -0
  71. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.js +30 -0
  72. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.js.map +1 -0
  73. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.d.ts +16 -0
  74. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.d.ts.map +1 -0
  75. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.js +27 -0
  76. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.js.map +1 -0
  77. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.d.ts +17 -0
  78. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.d.ts.map +1 -0
  79. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.js +28 -0
  80. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.js.map +1 -0
  81. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.d.ts +31 -0
  82. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.d.ts.map +1 -0
  83. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.js +102 -0
  84. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.js.map +1 -0
  85. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.d.ts +2 -0
  86. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.d.ts.map +1 -1
  87. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.js +14 -0
  88. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.js.map +1 -1
  89. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.d.ts +5 -0
  90. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.d.ts.map +1 -1
  91. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.js +20 -4
  92. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.js.map +1 -1
  93. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.d.ts +12 -0
  94. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.d.ts.map +1 -0
  95. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.js +304 -0
  96. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.js.map +1 -0
  97. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.d.ts +12 -0
  98. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.d.ts.map +1 -0
  99. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.js +131 -0
  100. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.js.map +1 -0
  101. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.d.ts +12 -0
  102. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.d.ts.map +1 -0
  103. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.js +312 -0
  104. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.js.map +1 -0
  105. package/dist/tests/acceptance/91-dashboard-multi-project-overview.acceptance.test.js +3 -0
  106. package/dist/tests/acceptance/91-dashboard-multi-project-overview.acceptance.test.js.map +1 -1
  107. package/dist/tests/stubs/projectConfigGateway.stub.d.ts +15 -0
  108. package/dist/tests/stubs/projectConfigGateway.stub.d.ts.map +1 -0
  109. package/dist/tests/stubs/projectConfigGateway.stub.js +40 -0
  110. package/dist/tests/stubs/projectConfigGateway.stub.js.map +1 -0
  111. package/dist/tests/units/config/projectConfig.test.js +43 -0
  112. package/dist/tests/units/config/projectConfig.test.js.map +1 -1
  113. package/dist/tests/units/dashboard/modules/cardCounters.test.d.ts +2 -0
  114. package/dist/tests/units/dashboard/modules/cardCounters.test.d.ts.map +1 -0
  115. package/dist/tests/units/dashboard/modules/cardCounters.test.js +106 -0
  116. package/dist/tests/units/dashboard/modules/cardCounters.test.js.map +1 -0
  117. package/dist/tests/units/dashboard/modules/managePanel.test.d.ts +2 -0
  118. package/dist/tests/units/dashboard/modules/managePanel.test.d.ts.map +1 -0
  119. package/dist/tests/units/dashboard/modules/managePanel.test.js +112 -0
  120. package/dist/tests/units/dashboard/modules/managePanel.test.js.map +1 -0
  121. package/dist/tests/units/dashboard/modules/overview.test.js +92 -0
  122. package/dist/tests/units/dashboard/modules/overview.test.js.map +1 -1
  123. package/dist/tests/units/dashboard/modules/settingsModal.test.d.ts +2 -0
  124. package/dist/tests/units/dashboard/modules/settingsModal.test.d.ts.map +1 -0
  125. package/dist/tests/units/dashboard/modules/settingsModal.test.js +166 -0
  126. package/dist/tests/units/dashboard/modules/settingsModal.test.js.map +1 -0
  127. package/dist/tests/units/dashboard/modules/tabBar.test.js +40 -11
  128. package/dist/tests/units/dashboard/modules/tabBar.test.js.map +1 -1
  129. package/dist/tests/units/frameworks/config/configLoader.test.js +35 -1
  130. package/dist/tests/units/frameworks/config/configLoader.test.js.map +1 -1
  131. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.test.js +111 -0
  132. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.test.js.map +1 -1
  133. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.test.js +243 -2
  134. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.test.js.map +1 -1
  135. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.d.ts +2 -0
  136. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.d.ts.map +1 -0
  137. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.js +72 -0
  138. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.js.map +1 -0
  139. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.d.ts +2 -0
  140. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.d.ts.map +1 -0
  141. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.js +76 -0
  142. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.js.map +1 -0
  143. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.d.ts +2 -0
  144. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.d.ts.map +1 -0
  145. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.js +84 -0
  146. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.js.map +1 -0
  147. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.d.ts +2 -0
  148. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.d.ts.map +1 -0
  149. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.js +141 -0
  150. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.js.map +1 -0
  151. package/dist/tests/units/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.test.js +44 -0
  152. package/dist/tests/units/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.test.js.map +1 -1
  153. package/dist/tests/units/modules/statistics-insights/interface-adapters/presenters/overview.presenter.test.js +42 -0
  154. package/dist/tests/units/modules/statistics-insights/interface-adapters/presenters/overview.presenter.test.js.map +1 -1
  155. package/package.json +1 -1
@@ -30,6 +30,20 @@
30
30
  </div>
31
31
  </header>
32
32
 
33
+ <div class="project-bar" role="region" aria-label="Project navigation">
34
+ <button type="button" id="manage-projects-toggle" class="manage-projects-toggle" aria-expanded="false" aria-controls="manage-panel">
35
+ <span class="manage-projects-toggle-label">// MANAGE PROJECTS</span>
36
+ <i data-lucide="chevron-down" class="manage-projects-toggle-icon"></i>
37
+ </button>
38
+ <section id="manage-panel" class="manage-panel" aria-label="Manage projects" data-open="false"></section>
39
+ <nav id="dashboard-tabs" class="dashboard-tab-bar-wrapper" aria-label="Project tabs"></nav>
40
+ </div>
41
+
42
+ <div id="cards-scope-marker" class="cards-scope-marker" data-scope-kind="overview">
43
+ <span class="cards-scope-prefix">// SCOPE</span>
44
+ <span class="cards-scope-label">TOUS LES PROJETS</span>
45
+ </div>
46
+
33
47
  <div class="cards">
34
48
  <div class="card card-priority">
35
49
  <div class="card-label" id="i18n-card-running"></div>
@@ -78,7 +92,6 @@
78
92
  </select>
79
93
  </div>
80
94
 
81
- <nav id="dashboard-tabs" class="dashboard-tab-bar-wrapper" aria-label="Project tabs"></nav>
82
95
  <span id="config-status" class="config-status hidden"></span>
83
96
 
84
97
  <div class="focus-strip">
@@ -107,6 +120,10 @@
107
120
  </div>
108
121
 
109
122
  <section id="worktree-section" aria-label="Worktree pool"></section>
123
+
124
+ <button type="button" id="open-settings-modal-btn" class="sidebar-settings-button" hidden>
125
+ <span class="sidebar-settings-button__prefix">// SETTINGS</span>
126
+ </button>
110
127
  </aside>
111
128
 
112
129
  <main class="dashboard-main">
@@ -304,16 +321,20 @@
304
321
  <div id="dev-sheet-content" class="sheet-content"></div>
305
322
  </div>
306
323
 
324
+ <dialog id="settings-modal" class="settings-modal" aria-labelledby="settings-modal-title"></dialog>
325
+
307
326
  <script type="module">
308
327
  import { t, setLanguage, getLanguage } from './modules/i18n.js';
309
328
  import { formatTime, formatDuration, formatPhase, formatLogTime } from './modules/formatting.js';
310
329
  import { escapeHtml, markdownToHtml, sanitizeHttpUrl } from './modules/html.js';
311
330
  import { getAgentIcon, icon, refreshIcons } from './modules/icons.js';
312
- import { MAX_RECONNECT_ATTEMPTS, RECONNECT_DELAY, STORAGE_KEY_PROJECTS, STORAGE_KEY_CURRENT, STORAGE_KEY_FOCUS_STRIP_MODE, QUALITY_TARGET_SCORE } from './modules/constants.js';
331
+ import { MAX_RECONNECT_ATTEMPTS, RECONNECT_DELAY, STORAGE_KEY_CURRENT, STORAGE_KEY_FOCUS_STRIP_MODE, QUALITY_TARGET_SCORE } from './modules/constants.js';
313
332
  import { buildTabBarModel, renderTabBarHtml, readActiveTab, writeActiveTab } from './modules/tabBar.js';
333
+ import { buildManagePanelModel, renderManagePanelHtml, buildOptimisticAddedRow, validateLocalPathInput } from './modules/managePanel.js';
314
334
  import { renderOverviewHtml } from './modules/overview.js';
315
335
  import { getDesktopNotificationPayload, shouldNotifyDesktop } from './modules/desktopNotifications.js';
316
336
  import { getLoadingPresentation, getQuietRefreshSectionIdentifiers } from './modules/loading.js';
337
+ import { computeCardCounters } from './modules/cardCounters.js';
317
338
  import { collectReviewNotifications, createReviewNotificationState } from './modules/notifications.js';
318
339
  import { resolveReviewAssigneeDisplay } from './modules/assignee.js';
319
340
  import { buildQueueLanesModel } from './modules/queueLanes.js';
@@ -345,6 +366,12 @@
345
366
  fetchBudgetStatus,
346
367
  submitBudget,
347
368
  } from './modules/budgetSettings.js';
369
+ import {
370
+ buildSettingsViewModel,
371
+ renderSettingsModalHtml,
372
+ validateExternalLink,
373
+ extractFormPayload,
374
+ } from './modules/settingsModal.js';
348
375
 
349
376
  const API_URL = window.location.origin;
350
377
  const WS_URL = `ws://${window.location.host}/ws`;
@@ -767,14 +794,12 @@
767
794
  const reviews = currentData.activeReviews.filter(r => r.jobType !== 'followup');
768
795
  const followups = currentData.activeReviews.filter(r => r.jobType === 'followup');
769
796
 
797
+ renderCardCounters();
798
+ const blocked = currentData.pendingFix.length;
770
799
  const running = currentData.activeReviews.filter(r => r.status === 'running').length;
771
800
  const queued = currentData.activeReviews.filter(r => r.status === 'queued').length;
772
- const blocked = currentData.pendingFix.length;
773
801
  const nowCount = running + blocked;
774
802
  const nextCount = queued + currentData.pendingApproval.length;
775
- document.getElementById('running-count').textContent = running;
776
- document.getElementById('queued-count').textContent = queued;
777
- document.getElementById('completed-count').textContent = currentData.reviewFiles.length;
778
803
  document.getElementById('focus-now-count').textContent = String(nowCount);
779
804
  document.getElementById('focus-next-count').textContent = String(nextCount);
780
805
  document.getElementById('focus-blocked-count').textContent = String(blocked);
@@ -2273,71 +2298,6 @@
2273
2298
  let currentProjectConfig = null;
2274
2299
  let currentProjectPath = null;
2275
2300
 
2276
- function getStoredProjects() {
2277
- try {
2278
- return JSON.parse(localStorage.getItem(STORAGE_KEY_PROJECTS) || '[]');
2279
- } catch {
2280
- return [];
2281
- }
2282
- }
2283
-
2284
- function saveProjects(projects) {
2285
- localStorage.setItem(STORAGE_KEY_PROJECTS, JSON.stringify(projects));
2286
- }
2287
-
2288
- function addProjectToHistory(path) {
2289
- const projects = getStoredProjects();
2290
- const filtered = projects.filter(p => p !== path);
2291
- filtered.unshift(path);
2292
- saveProjects(filtered.slice(0, 10));
2293
- updateProjectSelect();
2294
- }
2295
-
2296
- function removeProjectFromHistory(path) {
2297
- const projects = getStoredProjects().filter(p => p !== path);
2298
- saveProjects(projects);
2299
- updateProjectSelect();
2300
- }
2301
-
2302
- function updateProjectSelect() {
2303
- const select = document.getElementById('project-select');
2304
- if (!select) return;
2305
- const projects = getStoredProjects();
2306
- const current = localStorage.getItem(STORAGE_KEY_CURRENT) || '';
2307
-
2308
- select.innerHTML = `<option value="">${t('project.selectPlaceholder')}</option>`;
2309
- for (const path of projects) {
2310
- const shortName = path.split('/').slice(-2).join('/');
2311
- const option = document.createElement('option');
2312
- option.value = path;
2313
- option.textContent = shortName;
2314
- option.title = path;
2315
- if (path === current) option.selected = true;
2316
- select.appendChild(option);
2317
- }
2318
- }
2319
-
2320
- function onProjectSelect(path) {
2321
- if (path) {
2322
- const input = document.getElementById('project-path-input');
2323
- if (input) input.value = '';
2324
- loadProjectConfigFromPath(path);
2325
- }
2326
- }
2327
-
2328
- async function loadProjectConfig() {
2329
- const input = document.getElementById('project-path-input');
2330
- const select = document.getElementById('project-select');
2331
- const projectPath = (input?.value.trim() ?? '') || (select?.value ?? '');
2332
-
2333
- if (!projectPath) {
2334
- showConfigStatus(t('error.selectOrEnterPath'), 'error');
2335
- return;
2336
- }
2337
-
2338
- await loadProjectConfigFromPath(projectPath);
2339
- }
2340
-
2341
2301
  async function loadProjectConfigFromPath(projectPath) {
2342
2302
  const status = document.getElementById('config-status');
2343
2303
  const info = document.getElementById('config-info');
@@ -2353,14 +2313,8 @@
2353
2313
  currentProjectConfig = data.config;
2354
2314
  currentProjectPath = projectPath;
2355
2315
 
2356
- addProjectToHistory(projectPath);
2357
2316
  localStorage.setItem(STORAGE_KEY_CURRENT, projectPath);
2358
2317
 
2359
- const legacySelect = document.getElementById('project-select');
2360
- if (legacySelect) legacySelect.value = projectPath;
2361
- const legacyInput = document.getElementById('project-path-input');
2362
- if (legacyInput) legacyInput.value = '';
2363
-
2364
2318
  const shortName = projectPath.split('/').slice(-2).join('/');
2365
2319
  showConfigStatus(`<i data-lucide="check-circle"></i> ${escapeHtml(shortName)}`, 'success');
2366
2320
  refreshIcons();
@@ -2413,43 +2367,6 @@
2413
2367
  refreshIcons();
2414
2368
  }
2415
2369
 
2416
- function removeCurrentProject() {
2417
- const select = document.getElementById('project-select');
2418
- const path = select?.value ?? currentProjectPath ?? '';
2419
- if (!path) {
2420
- showConfigStatus(t('project.noProjectSelected'), 'error');
2421
- return;
2422
- }
2423
- const shortName = path.split('/').slice(-2).join('/');
2424
- if (confirm(t('confirm.removeProject', { name: shortName }))) {
2425
- removeProjectFromHistory(path);
2426
- localStorage.removeItem(STORAGE_KEY_CURRENT);
2427
- currentProjectPath = null;
2428
- currentProjectConfig = null;
2429
- document.getElementById('config-info')?.classList.add('hidden');
2430
- showConfigStatus(t('project.removed'), 'success');
2431
- }
2432
- }
2433
-
2434
- async function syncServerRepositories() {
2435
- try {
2436
- const response = await fetch(`${API_URL}/api/repositories`);
2437
- const data = await response.json();
2438
- if (!data.repositories) return;
2439
-
2440
- const stored = getStoredProjects();
2441
- for (const repository of data.repositories) {
2442
- if (repository.enabled && !stored.includes(repository.localPath)) {
2443
- stored.push(repository.localPath);
2444
- }
2445
- }
2446
- saveProjects(stored);
2447
- updateProjectSelect();
2448
- } catch {
2449
- // Server unreachable — use localStorage only
2450
- }
2451
- }
2452
-
2453
2370
  // SPEC-91 — Dashboard Multi-Project Overview
2454
2371
  let availableRepositories = [];
2455
2372
  let activeTabId = 'overview';
@@ -2458,14 +2375,44 @@
2458
2375
  try {
2459
2376
  const response = await fetch(`${API_URL}/api/repositories`);
2460
2377
  const data = await response.json();
2461
- availableRepositories = Array.isArray(data.repositories)
2462
- ? data.repositories.filter((repository) => repository.enabled)
2463
- : [];
2378
+ availableRepositories = Array.isArray(data.repositories) ? data.repositories : [];
2464
2379
  } catch {
2465
2380
  availableRepositories = [];
2466
2381
  }
2467
2382
  }
2468
2383
 
2384
+ function syncAvailableRepositoriesFromResponse(payload) {
2385
+ availableRepositories = Array.isArray(payload?.repositories) ? payload.repositories : [];
2386
+ }
2387
+
2388
+ function renderCardCounters() {
2389
+ let scope;
2390
+ if (activeTabId === 'overview') {
2391
+ scope = { kind: 'overview' };
2392
+ } else {
2393
+ const repository = availableRepositories.find((r) => r.localPath === activeTabId);
2394
+ const projectName = repository?.name ?? activeTabId.split('/').filter(Boolean).pop() ?? activeTabId;
2395
+ scope = { kind: 'project', localPath: activeTabId, projectName };
2396
+ }
2397
+ const counters = computeCardCounters({
2398
+ activeReviews: currentData.activeReviews,
2399
+ reviewFiles: currentData.reviewFiles,
2400
+ scope,
2401
+ });
2402
+ const runningEl = document.getElementById('running-count');
2403
+ const queuedEl = document.getElementById('queued-count');
2404
+ const completedEl = document.getElementById('completed-count');
2405
+ if (runningEl) runningEl.textContent = counters.running;
2406
+ if (queuedEl) queuedEl.textContent = counters.queued;
2407
+ if (completedEl) completedEl.textContent = counters.completed;
2408
+ const markerEl = document.getElementById('cards-scope-marker');
2409
+ if (markerEl) {
2410
+ markerEl.dataset.scopeKind = counters.markerKind;
2411
+ const labelEl = markerEl.querySelector('.cards-scope-label');
2412
+ if (labelEl) labelEl.textContent = counters.markerLabel;
2413
+ }
2414
+ }
2415
+
2469
2416
  function renderDashboardTabs() {
2470
2417
  const container = document.getElementById('dashboard-tabs');
2471
2418
  if (!container) return;
@@ -2498,6 +2445,8 @@
2498
2445
  const overviewSection = document.getElementById('overview-section');
2499
2446
  if (overviewSection) overviewSection.classList.remove('hidden');
2500
2447
  renderDashboardTabs();
2448
+ renderCardCounters();
2449
+ syncSettingsButtonVisibility();
2501
2450
  refreshOverviewSection();
2502
2451
  }
2503
2452
 
@@ -2508,9 +2457,120 @@
2508
2457
  const overviewSection = document.getElementById('overview-section');
2509
2458
  if (overviewSection) overviewSection.classList.add('hidden');
2510
2459
  renderDashboardTabs();
2460
+ renderCardCounters();
2461
+ syncSettingsButtonVisibility();
2511
2462
  loadProjectConfigFromPath(projectPath);
2512
2463
  }
2513
2464
 
2465
+ function syncSettingsButtonVisibility() {
2466
+ const button = document.getElementById('open-settings-modal-btn');
2467
+ if (!button) return;
2468
+ if (activeTabId === 'overview') {
2469
+ button.hidden = true;
2470
+ } else {
2471
+ button.hidden = false;
2472
+ }
2473
+ }
2474
+
2475
+ function resolveActiveProjectName() {
2476
+ if (activeTabId === 'overview') return '—';
2477
+ const repository = availableRepositories.find((r) => r.localPath === activeTabId);
2478
+ if (repository?.name) return repository.name;
2479
+ return activeTabId.split('/').filter(Boolean).pop() ?? activeTabId;
2480
+ }
2481
+
2482
+ async function openSettingsModal() {
2483
+ if (activeTabId === 'overview') return;
2484
+ const dialog = document.getElementById('settings-modal');
2485
+ if (!dialog || typeof dialog.showModal !== 'function') return;
2486
+ try {
2487
+ const response = await fetch(
2488
+ `${API_URL}/api/project-config?path=${encodeURIComponent(activeTabId)}`,
2489
+ );
2490
+ const payload = await response.json();
2491
+ if (!payload?.success) {
2492
+ dialog.innerHTML = `<form method="dialog" class="settings-modal__form"><p class="settings-modal__error">${escapeHtml(payload?.error || 'Configuration projet illisible')}</p><div class="settings-modal__actions"><button type="submit" class="settings-modal__cancel">Fermer</button></div></form>`;
2493
+ dialog.showModal();
2494
+ return;
2495
+ }
2496
+ const viewModel = buildSettingsViewModel({
2497
+ config: payload.config,
2498
+ projectName: resolveActiveProjectName(),
2499
+ });
2500
+ dialog.innerHTML = renderSettingsModalHtml(viewModel);
2501
+ bindSettingsModalForm(dialog);
2502
+ dialog.showModal();
2503
+ } catch (error) {
2504
+ showToast(t('toast.error') || 'Erreur', 'error');
2505
+ }
2506
+ }
2507
+
2508
+ function closeSettingsModal() {
2509
+ const dialog = document.getElementById('settings-modal');
2510
+ if (dialog && dialog.open) dialog.close();
2511
+ }
2512
+
2513
+ function bindSettingsModalForm(dialog) {
2514
+ const form = dialog.querySelector('form.settings-modal__form');
2515
+ const cancelBtn = dialog.querySelector('.settings-modal__cancel');
2516
+ const errorEl = dialog.querySelector('.settings-modal__error');
2517
+ if (cancelBtn) {
2518
+ cancelBtn.addEventListener('click', (event) => {
2519
+ event.preventDefault();
2520
+ closeSettingsModal();
2521
+ });
2522
+ }
2523
+ if (!form) return;
2524
+ form.addEventListener('submit', async (event) => {
2525
+ event.preventDefault();
2526
+ if (errorEl) errorEl.textContent = '';
2527
+ const formData = new FormData(form);
2528
+ const payload = extractFormPayload(formData);
2529
+ const linkValidation = validateExternalLink(payload.externalLink ?? '');
2530
+ if (!linkValidation.ok) {
2531
+ if (errorEl) errorEl.textContent = linkValidation.message;
2532
+ return;
2533
+ }
2534
+ try {
2535
+ const response = await fetch(
2536
+ `${API_URL}/api/project-config?path=${encodeURIComponent(activeTabId)}`,
2537
+ {
2538
+ method: 'PATCH',
2539
+ headers: { 'Content-Type': 'application/json' },
2540
+ body: JSON.stringify(payload),
2541
+ },
2542
+ );
2543
+ if (!response.ok) {
2544
+ const errorPayload = await response.json().catch(() => ({ error: 'Échec de la sauvegarde' }));
2545
+ if (errorEl) errorEl.textContent = errorPayload?.error || 'Échec de la sauvegarde';
2546
+ return;
2547
+ }
2548
+ closeSettingsModal();
2549
+ refreshOverviewSection();
2550
+ } catch {
2551
+ if (errorEl) errorEl.textContent = 'Échec de la sauvegarde';
2552
+ }
2553
+ });
2554
+ }
2555
+
2556
+ function bindSettingsModalDismissals() {
2557
+ const dialog = document.getElementById('settings-modal');
2558
+ if (!dialog) return;
2559
+ dialog.addEventListener('click', (event) => {
2560
+ if (event.target === dialog) {
2561
+ closeSettingsModal();
2562
+ }
2563
+ });
2564
+ }
2565
+
2566
+ function bindSettingsModalTrigger() {
2567
+ const button = document.getElementById('open-settings-modal-btn');
2568
+ if (!button) return;
2569
+ button.addEventListener('click', () => {
2570
+ openSettingsModal();
2571
+ });
2572
+ }
2573
+
2514
2574
  async function refreshOverviewSection() {
2515
2575
  const container = document.getElementById('overview-section');
2516
2576
  if (!container) return;
@@ -2525,6 +2585,9 @@
2525
2585
  if (projectPath) activateProjectTab(projectPath);
2526
2586
  });
2527
2587
  });
2588
+ container.querySelectorAll('.project-card__external').forEach((anchor) => {
2589
+ anchor.addEventListener('click', (event) => event.stopPropagation());
2590
+ });
2528
2591
  } catch {
2529
2592
  // Silent: WS reconnection or next refresh will retry
2530
2593
  }
@@ -2532,7 +2595,10 @@
2532
2595
 
2533
2596
  async function initOverviewAndTabs() {
2534
2597
  await fetchAvailableRepositories();
2535
- await syncServerRepositories();
2598
+ renderManagePanel();
2599
+ bindManagePanelToggle();
2600
+ bindSettingsModalTrigger();
2601
+ bindSettingsModalDismissals();
2536
2602
  const persistedTab = readActiveTab();
2537
2603
  const repositoryPaths = availableRepositories.map((repository) => repository.localPath);
2538
2604
  if (persistedTab && persistedTab !== 'overview' && repositoryPaths.includes(persistedTab)) {
@@ -2542,6 +2608,196 @@
2542
2608
  }
2543
2609
  }
2544
2610
 
2611
+ let isManagePanelOpen = false;
2612
+
2613
+ function renderManagePanel() {
2614
+ const panel = document.getElementById('manage-panel');
2615
+ if (!panel) return;
2616
+ const model = buildManagePanelModel({ repositories: availableRepositories, isOpen: isManagePanelOpen });
2617
+ panel.innerHTML = renderManagePanelHtml(model);
2618
+ panel.dataset.open = isManagePanelOpen ? 'true' : 'false';
2619
+ bindManagePanelHandlers();
2620
+ }
2621
+
2622
+ function bindManagePanelToggle() {
2623
+ const toggle = document.getElementById('manage-projects-toggle');
2624
+ if (!toggle || toggle.dataset.bound === 'true') return;
2625
+ toggle.dataset.bound = 'true';
2626
+ toggle.addEventListener('click', () => {
2627
+ isManagePanelOpen = !isManagePanelOpen;
2628
+ toggle.setAttribute('aria-expanded', isManagePanelOpen ? 'true' : 'false');
2629
+ renderManagePanel();
2630
+ });
2631
+ }
2632
+
2633
+ function bindManagePanelHandlers() {
2634
+ const panel = document.getElementById('manage-panel');
2635
+ if (!panel) return;
2636
+ const form = panel.querySelector('form.add-form');
2637
+ if (form) {
2638
+ form.addEventListener('submit', handleAddProjectSubmit);
2639
+ const input = form.querySelector('input.add-form-input');
2640
+ if (input) {
2641
+ input.addEventListener('keydown', (event) => {
2642
+ if (event.key === 'Escape') {
2643
+ input.value = '';
2644
+ clearManagePanelError();
2645
+ }
2646
+ });
2647
+ }
2648
+ }
2649
+ panel.querySelectorAll('.manage-row-delete').forEach((button) => {
2650
+ button.addEventListener('click', () => {
2651
+ const row = button.closest('.manage-row');
2652
+ const localPath = row?.dataset.localPath;
2653
+ if (localPath) handleDeleteProject(localPath, row);
2654
+ });
2655
+ });
2656
+ panel.querySelectorAll('.manage-row-toggle').forEach((button) => {
2657
+ button.addEventListener('click', () => {
2658
+ const row = button.closest('.manage-row');
2659
+ const localPath = row?.dataset.localPath;
2660
+ const currentlyEnabled = row?.dataset.enabled === 'true';
2661
+ if (localPath) handleToggleProject(localPath, !currentlyEnabled);
2662
+ });
2663
+ });
2664
+ }
2665
+
2666
+ function setManagePanelError(message) {
2667
+ const panel = document.getElementById('manage-panel');
2668
+ const target = panel?.querySelector('[data-role="error-message"]');
2669
+ if (!target) return;
2670
+ target.textContent = message;
2671
+ target.hidden = false;
2672
+ }
2673
+
2674
+ function clearManagePanelError() {
2675
+ const panel = document.getElementById('manage-panel');
2676
+ const target = panel?.querySelector('[data-role="error-message"]');
2677
+ if (!target) return;
2678
+ target.textContent = '';
2679
+ target.hidden = true;
2680
+ }
2681
+
2682
+ function flashAddFormError() {
2683
+ const form = document.querySelector('#manage-panel form.add-form');
2684
+ if (!form) return;
2685
+ form.classList.remove('is-error');
2686
+ requestAnimationFrame(() => form.classList.add('is-error'));
2687
+ setTimeout(() => form.classList.remove('is-error'), 320);
2688
+ }
2689
+
2690
+ function flashAddFormSuccess() {
2691
+ const input = document.querySelector('#manage-panel input.add-form-input');
2692
+ if (!input) return;
2693
+ input.classList.remove('is-success');
2694
+ requestAnimationFrame(() => input.classList.add('is-success'));
2695
+ setTimeout(() => input.classList.remove('is-success'), 1500);
2696
+ }
2697
+
2698
+ function flashTabEntering(localPath) {
2699
+ const tab = document.querySelector(`.dashboard-tab[data-tab-id="${CSS.escape(localPath)}"]`);
2700
+ if (!tab) return;
2701
+ tab.classList.add('is-entering');
2702
+ setTimeout(() => tab.classList.remove('is-entering'), 1500);
2703
+ }
2704
+
2705
+ async function handleAddProjectSubmit(event) {
2706
+ event.preventDefault();
2707
+ const form = event.currentTarget;
2708
+ const input = form.querySelector('input.add-form-input');
2709
+ const submit = form.querySelector('button.add-form-submit');
2710
+ const rawValue = input ? input.value : '';
2711
+ const validation = validateLocalPathInput(rawValue);
2712
+ clearManagePanelError();
2713
+ if (!validation.ok) {
2714
+ const message = validation.reason === 'empty' ? 'Chemin du projet requis' : 'Le chemin doit être absolu';
2715
+ setManagePanelError(message);
2716
+ flashAddFormError();
2717
+ return;
2718
+ }
2719
+ const trimmed = rawValue.trim();
2720
+ if (submit) submit.classList.add('is-busy');
2721
+ try {
2722
+ const response = await fetch(`${API_URL}/api/repositories`, {
2723
+ method: 'POST',
2724
+ headers: { 'Content-Type': 'application/json' },
2725
+ body: JSON.stringify({ localPath: trimmed }),
2726
+ });
2727
+ const body = await response.json().catch(() => ({}));
2728
+ if (!response.ok) {
2729
+ setManagePanelError(typeof body?.error === 'string' ? body.error : 'Erreur inconnue');
2730
+ flashAddFormError();
2731
+ return;
2732
+ }
2733
+ syncAvailableRepositoriesFromResponse(body);
2734
+ if (input) input.value = '';
2735
+ flashAddFormSuccess();
2736
+ renderManagePanel();
2737
+ renderDashboardTabs();
2738
+ flashTabEntering(trimmed);
2739
+ } catch {
2740
+ setManagePanelError('Erreur réseau');
2741
+ flashAddFormError();
2742
+ } finally {
2743
+ if (submit) submit.classList.remove('is-busy');
2744
+ }
2745
+ }
2746
+
2747
+ async function handleDeleteProject(localPath, rowElement) {
2748
+ try {
2749
+ const response = await fetch(`${API_URL}/api/repositories?localPath=${encodeURIComponent(localPath)}`, {
2750
+ method: 'DELETE',
2751
+ });
2752
+ const body = await response.json().catch(() => ({}));
2753
+ if (!response.ok) {
2754
+ setManagePanelError(typeof body?.error === 'string' ? body.error : 'Erreur inconnue');
2755
+ return;
2756
+ }
2757
+ syncAvailableRepositoriesFromResponse(body);
2758
+ if (rowElement) {
2759
+ rowElement.classList.add('is-leaving');
2760
+ setTimeout(() => {
2761
+ renderManagePanel();
2762
+ }, 250);
2763
+ } else {
2764
+ renderManagePanel();
2765
+ }
2766
+ const tab = document.querySelector(`.dashboard-tab[data-tab-id="${CSS.escape(localPath)}"]`);
2767
+ if (tab) {
2768
+ tab.classList.add('is-leaving');
2769
+ setTimeout(() => renderDashboardTabs(), 250);
2770
+ } else {
2771
+ renderDashboardTabs();
2772
+ }
2773
+ if (activeTabId === localPath) {
2774
+ activateOverviewTab();
2775
+ }
2776
+ } catch {
2777
+ setManagePanelError('Erreur réseau');
2778
+ }
2779
+ }
2780
+
2781
+ async function handleToggleProject(localPath, enabled) {
2782
+ try {
2783
+ const response = await fetch(`${API_URL}/api/repositories?localPath=${encodeURIComponent(localPath)}`, {
2784
+ method: 'PATCH',
2785
+ headers: { 'Content-Type': 'application/json' },
2786
+ body: JSON.stringify({ enabled }),
2787
+ });
2788
+ const body = await response.json().catch(() => ({}));
2789
+ if (!response.ok) {
2790
+ setManagePanelError(typeof body?.error === 'string' ? body.error : 'Erreur inconnue');
2791
+ return;
2792
+ }
2793
+ syncAvailableRepositoriesFromResponse(body);
2794
+ renderManagePanel();
2795
+ renderDashboardTabs();
2796
+ } catch {
2797
+ setManagePanelError('Erreur réseau');
2798
+ }
2799
+ }
2800
+
2545
2801
  function getMrLabel(platform = activePlatform) {
2546
2802
  return platform === 'github' ? 'PR' : 'MR';
2547
2803
  }
@@ -2681,19 +2937,6 @@
2681
2937
  const modelSonnet = document.getElementById('i18n-model-sonnet');
2682
2938
  if (modelSonnet) modelSonnet.textContent = t('model.sonnet');
2683
2939
 
2684
- // Project loader
2685
- const projectPlaceholder = document.getElementById('i18n-project-placeholder');
2686
- if (projectPlaceholder) projectPlaceholder.textContent = t('project.selectPlaceholder');
2687
-
2688
- const projectPathInput = document.getElementById('project-path-input');
2689
- if (projectPathInput) projectPathInput.placeholder = t('project.inputPlaceholder');
2690
-
2691
- const projectLoad = document.getElementById('i18n-project-load');
2692
- if (projectLoad) projectLoad.textContent = t('project.load');
2693
-
2694
- const removeProjectBtn = document.getElementById('remove-project-btn');
2695
- if (removeProjectBtn) removeProjectBtn.title = t('project.removeTooltip');
2696
-
2697
2940
  // Login sections
2698
2941
  const claudeLoginTitle = document.getElementById('i18n-claude-login-title');
2699
2942
  if (claudeLoginTitle) claudeLoginTitle.textContent = t('login.claude.title');
@@ -2792,9 +3035,6 @@
2792
3035
 
2793
3036
  const modalConfirm = document.getElementById('cancel-modal-confirm');
2794
3037
  if (modalConfirm) modalConfirm.textContent = t('modal.confirm');
2795
-
2796
- // Update project select placeholder (re-render the select)
2797
- updateProjectSelect();
2798
3038
  }
2799
3039
 
2800
3040
  async function checkForUpdates() {
@@ -2952,9 +3192,6 @@
2952
3192
  window.toggleStats = toggleStats;
2953
3193
  window.changeModel = changeModel;
2954
3194
  window.changeLanguage = changeLanguage;
2955
- window.onProjectSelect = onProjectSelect;
2956
- window.loadProjectConfig = loadProjectConfig;
2957
- window.removeCurrentProject = removeCurrentProject;
2958
3195
  window.toggleReviewAccordion = toggleReviewAccordion;
2959
3196
  window.toggleReviewDescription = toggleReviewDescription;
2960
3197
  window.deleteReviewFile = deleteReviewFile;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @typedef {{ kind: 'overview' } | { kind: 'project', localPath: string, projectName: string }} CardScope
3
+ */
4
+ /**
5
+ * @param {object} input
6
+ * @param {Array<{ project: string, status: string }>} input.activeReviews
7
+ * @param {Array<unknown>} input.reviewFiles
8
+ * @param {CardScope} input.scope
9
+ * @returns {{ running: number, queued: number, completed: number, markerLabel: string, markerKind: 'overview'|'project' }}
10
+ */
11
+ export function computeCardCounters(input: {
12
+ activeReviews: Array<{
13
+ project: string;
14
+ status: string;
15
+ }>;
16
+ reviewFiles: Array<unknown>;
17
+ scope: CardScope;
18
+ }): {
19
+ running: number;
20
+ queued: number;
21
+ completed: number;
22
+ markerLabel: string;
23
+ markerKind: "overview" | "project";
24
+ };
25
+ export type CardScope = {
26
+ kind: "overview";
27
+ } | {
28
+ kind: "project";
29
+ localPath: string;
30
+ projectName: string;
31
+ };
32
+ //# sourceMappingURL=cardCounters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardCounters.d.ts","sourceRoot":"","sources":["../../../src/dashboard/modules/cardCounters.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;GAMG;AACH,2CALG;IAA0D,aAAa,EAA/D,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACpB,WAAW,EAAjC,KAAK,CAAC,OAAO,CAAC;IACG,KAAK,EAAtB,SAAS;CACjB,GAAU;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,GAAC,SAAS,CAAA;CAAE,CA8BzH;wBAtCY;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE"}