reviewflow 3.20.0 → 3.22.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 (231) hide show
  1. package/CHANGELOG.md +14 -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 +626 -175
  7. package/dist/dashboard/modules/animations.d.ts +164 -0
  8. package/dist/dashboard/modules/animations.d.ts.map +1 -0
  9. package/dist/dashboard/modules/animations.js +405 -0
  10. package/dist/dashboard/modules/animations.js.map +1 -0
  11. package/dist/dashboard/modules/cardCounters.d.ts +47 -0
  12. package/dist/dashboard/modules/cardCounters.d.ts.map +1 -0
  13. package/dist/dashboard/modules/cardCounters.js +74 -0
  14. package/dist/dashboard/modules/cardCounters.js.map +1 -0
  15. package/dist/dashboard/modules/i18n.d.ts.map +1 -1
  16. package/dist/dashboard/modules/i18n.js +20 -2
  17. package/dist/dashboard/modules/i18n.js.map +1 -1
  18. package/dist/dashboard/modules/managePanel.d.ts +49 -0
  19. package/dist/dashboard/modules/managePanel.d.ts.map +1 -0
  20. package/dist/dashboard/modules/managePanel.js +123 -0
  21. package/dist/dashboard/modules/managePanel.js.map +1 -0
  22. package/dist/dashboard/modules/overview.d.ts +1 -0
  23. package/dist/dashboard/modules/overview.d.ts.map +1 -1
  24. package/dist/dashboard/modules/overview.js +14 -0
  25. package/dist/dashboard/modules/overview.js.map +1 -1
  26. package/dist/dashboard/modules/settingsModal.d.ts +77 -0
  27. package/dist/dashboard/modules/settingsModal.d.ts.map +1 -0
  28. package/dist/dashboard/modules/settingsModal.js +202 -0
  29. package/dist/dashboard/modules/settingsModal.js.map +1 -0
  30. package/dist/dashboard/modules/tabBar.d.ts +4 -0
  31. package/dist/dashboard/modules/tabBar.d.ts.map +1 -1
  32. package/dist/dashboard/modules/tabBar.js +6 -1
  33. package/dist/dashboard/modules/tabBar.js.map +1 -1
  34. package/dist/dashboard/styles.css +1373 -402
  35. package/dist/frameworks/config/configLoader.d.ts +8 -0
  36. package/dist/frameworks/config/configLoader.d.ts.map +1 -1
  37. package/dist/frameworks/config/configLoader.js +18 -0
  38. package/dist/frameworks/config/configLoader.js.map +1 -1
  39. package/dist/main/routes.d.ts.map +1 -1
  40. package/dist/main/routes.js +47 -2
  41. package/dist/main/routes.js.map +1 -1
  42. package/dist/main/server.d.ts.map +1 -1
  43. package/dist/main/server.js +22 -0
  44. package/dist/main/server.js.map +1 -1
  45. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.d.ts +1 -0
  46. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.d.ts.map +1 -1
  47. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.js +26 -5
  48. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.js.map +1 -1
  49. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.d.ts +20 -0
  50. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.d.ts.map +1 -0
  51. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.js +2 -0
  52. package/dist/modules/cli-configuration/entities/projectConfig/projectConfig.gateway.js.map +1 -0
  53. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.d.ts +13 -0
  54. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.d.ts.map +1 -0
  55. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.js +2 -0
  56. package/dist/modules/cli-configuration/entities/repositoryEntry/repositoryEntry.js.map +1 -0
  57. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.d.ts +6 -1
  58. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.d.ts.map +1 -1
  59. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.js +116 -13
  60. package/dist/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.js.map +1 -1
  61. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.d.ts +36 -0
  62. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.d.ts.map +1 -1
  63. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.js +78 -8
  64. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.js.map +1 -1
  65. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.d.ts +7 -0
  66. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.d.ts.map +1 -0
  67. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.js +48 -0
  68. package/dist/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.js.map +1 -0
  69. package/dist/modules/cli-configuration/usecases/cli/addRepositoriesToConfig.usecase.d.ts +1 -6
  70. package/dist/modules/cli-configuration/usecases/cli/addRepositoriesToConfig.usecase.d.ts.map +1 -1
  71. package/dist/modules/cli-configuration/usecases/cli/addRepositoriesToConfig.usecase.js.map +1 -1
  72. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.d.ts +21 -0
  73. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.d.ts.map +1 -0
  74. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.js +27 -0
  75. package/dist/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.js.map +1 -0
  76. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.d.ts +22 -0
  77. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.d.ts.map +1 -0
  78. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.js +27 -0
  79. package/dist/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.js.map +1 -0
  80. package/dist/modules/cli-configuration/usecases/cli/writeInitConfig.usecase.d.ts +1 -6
  81. package/dist/modules/cli-configuration/usecases/cli/writeInitConfig.usecase.d.ts.map +1 -1
  82. package/dist/modules/cli-configuration/usecases/cli/writeInitConfig.usecase.js.map +1 -1
  83. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.d.ts +19 -0
  84. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.d.ts.map +1 -0
  85. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.js +30 -0
  86. package/dist/modules/cli-configuration/usecases/dashboardRepositories/addRepositoryFromDashboard.usecase.js.map +1 -0
  87. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.d.ts +16 -0
  88. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.d.ts.map +1 -0
  89. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.js +27 -0
  90. package/dist/modules/cli-configuration/usecases/dashboardRepositories/removeRepositoryFromDashboard.usecase.js.map +1 -0
  91. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.d.ts +17 -0
  92. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.d.ts.map +1 -0
  93. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.js +28 -0
  94. package/dist/modules/cli-configuration/usecases/dashboardRepositories/updateRepositoryEnabledFromDashboard.usecase.js.map +1 -0
  95. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.d.ts +31 -0
  96. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.d.ts.map +1 -0
  97. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.js +102 -0
  98. package/dist/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.js.map +1 -0
  99. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/github.controller.d.ts.map +1 -1
  100. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/github.controller.js +3 -0
  101. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/github.controller.js.map +1 -1
  102. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.d.ts.map +1 -1
  103. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.js +3 -0
  104. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.js.map +1 -1
  105. package/dist/modules/review-execution/entities/reviewContext/reviewContext.gateway.d.ts +1 -0
  106. package/dist/modules/review-execution/entities/reviewContext/reviewContext.gateway.d.ts.map +1 -1
  107. package/dist/modules/review-execution/entities/reviewContext/reviewContext.schema.d.ts +10 -2
  108. package/dist/modules/review-execution/entities/reviewContext/reviewContext.schema.d.ts.map +1 -1
  109. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.d.ts +11 -0
  110. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.d.ts.map +1 -0
  111. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.js +22 -0
  112. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.js.map +1 -0
  113. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.guard.d.ts +10 -0
  114. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.guard.d.ts.map +1 -1
  115. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.d.ts +31 -1
  116. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.d.ts.map +1 -1
  117. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.js +11 -1
  118. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.js.map +1 -1
  119. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.d.ts +1 -0
  120. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.d.ts.map +1 -1
  121. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.js +27 -1
  122. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.js.map +1 -1
  123. package/dist/modules/review-execution/services/reviewRecovery.service.d.ts +33 -0
  124. package/dist/modules/review-execution/services/reviewRecovery.service.d.ts.map +1 -0
  125. package/dist/modules/review-execution/services/reviewRecovery.service.js +80 -0
  126. package/dist/modules/review-execution/services/reviewRecovery.service.js.map +1 -0
  127. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.d.ts +2 -0
  128. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.d.ts.map +1 -1
  129. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.js +14 -0
  130. package/dist/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.js.map +1 -1
  131. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.d.ts +5 -0
  132. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.d.ts.map +1 -1
  133. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.js +20 -4
  134. package/dist/modules/statistics-insights/interface-adapters/presenters/overview.presenter.js.map +1 -1
  135. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.d.ts +12 -0
  136. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.d.ts.map +1 -0
  137. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.js +304 -0
  138. package/dist/tests/acceptance/177-dashboard-add-project-ui.acceptance.test.js.map +1 -0
  139. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.d.ts +12 -0
  140. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.d.ts.map +1 -0
  141. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.js +131 -0
  142. package/dist/tests/acceptance/178-dashboard-tabs-reposition.acceptance.test.js.map +1 -0
  143. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.d.ts +12 -0
  144. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.d.ts.map +1 -0
  145. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.js +312 -0
  146. package/dist/tests/acceptance/179-dashboard-project-settings-modal.acceptance.test.js.map +1 -0
  147. package/dist/tests/acceptance/91-dashboard-multi-project-overview.acceptance.test.js +5 -0
  148. package/dist/tests/acceptance/91-dashboard-multi-project-overview.acceptance.test.js.map +1 -1
  149. package/dist/tests/stubs/projectConfigGateway.stub.d.ts +15 -0
  150. package/dist/tests/stubs/projectConfigGateway.stub.d.ts.map +1 -0
  151. package/dist/tests/stubs/projectConfigGateway.stub.js +40 -0
  152. package/dist/tests/stubs/projectConfigGateway.stub.js.map +1 -0
  153. package/dist/tests/stubs/reviewContextGateway.stub.d.ts +1 -0
  154. package/dist/tests/stubs/reviewContextGateway.stub.d.ts.map +1 -1
  155. package/dist/tests/stubs/reviewContextGateway.stub.js +3 -0
  156. package/dist/tests/stubs/reviewContextGateway.stub.js.map +1 -1
  157. package/dist/tests/units/config/projectConfig.test.js +43 -0
  158. package/dist/tests/units/config/projectConfig.test.js.map +1 -1
  159. package/dist/tests/units/dashboard/modules/animations.test.d.ts +10 -0
  160. package/dist/tests/units/dashboard/modules/animations.test.d.ts.map +1 -0
  161. package/dist/tests/units/dashboard/modules/animations.test.js +223 -0
  162. package/dist/tests/units/dashboard/modules/animations.test.js.map +1 -0
  163. package/dist/tests/units/dashboard/modules/cardCounters.test.d.ts +2 -0
  164. package/dist/tests/units/dashboard/modules/cardCounters.test.d.ts.map +1 -0
  165. package/dist/tests/units/dashboard/modules/cardCounters.test.js +162 -0
  166. package/dist/tests/units/dashboard/modules/cardCounters.test.js.map +1 -0
  167. package/dist/tests/units/dashboard/modules/managePanel.test.d.ts +2 -0
  168. package/dist/tests/units/dashboard/modules/managePanel.test.d.ts.map +1 -0
  169. package/dist/tests/units/dashboard/modules/managePanel.test.js +112 -0
  170. package/dist/tests/units/dashboard/modules/managePanel.test.js.map +1 -0
  171. package/dist/tests/units/dashboard/modules/overview.test.js +92 -0
  172. package/dist/tests/units/dashboard/modules/overview.test.js.map +1 -1
  173. package/dist/tests/units/dashboard/modules/settingsModal.test.d.ts +2 -0
  174. package/dist/tests/units/dashboard/modules/settingsModal.test.d.ts.map +1 -0
  175. package/dist/tests/units/dashboard/modules/settingsModal.test.js +166 -0
  176. package/dist/tests/units/dashboard/modules/settingsModal.test.js.map +1 -0
  177. package/dist/tests/units/dashboard/modules/tabBar.test.js +40 -11
  178. package/dist/tests/units/dashboard/modules/tabBar.test.js.map +1 -1
  179. package/dist/tests/units/entities/reviewContext/reviewContext.schema.test.js +1 -0
  180. package/dist/tests/units/entities/reviewContext/reviewContext.schema.test.js.map +1 -1
  181. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.d.ts +2 -0
  182. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.d.ts.map +1 -0
  183. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.js +43 -0
  184. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.js.map +1 -0
  185. package/dist/tests/units/entities/reviewContext/reviewContextResult.guard.test.js +7 -3
  186. package/dist/tests/units/entities/reviewContext/reviewContextResult.guard.test.js.map +1 -1
  187. package/dist/tests/units/entities/reviewContext/reviewContextResult.schema.test.js +12 -1
  188. package/dist/tests/units/entities/reviewContext/reviewContextResult.schema.test.js.map +1 -1
  189. package/dist/tests/units/frameworks/config/configLoader.test.js +35 -1
  190. package/dist/tests/units/frameworks/config/configLoader.test.js.map +1 -1
  191. package/dist/tests/units/interface-adapters/controllers/webhook/github.controller.test.js +57 -0
  192. package/dist/tests/units/interface-adapters/controllers/webhook/github.controller.test.js.map +1 -1
  193. package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js +1 -0
  194. package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js.map +1 -1
  195. package/dist/tests/units/interface-adapters/gateways/reviewContext.fileSystem.gateway.test.js +64 -4
  196. package/dist/tests/units/interface-adapters/gateways/reviewContext.fileSystem.gateway.test.js.map +1 -1
  197. package/dist/tests/units/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.test.js +56 -0
  198. package/dist/tests/units/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.test.js.map +1 -1
  199. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.test.js +111 -0
  200. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/projectConfig.routes.test.js.map +1 -1
  201. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.test.js +244 -3
  202. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.test.js.map +1 -1
  203. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.d.ts +2 -0
  204. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.d.ts.map +1 -0
  205. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.js +72 -0
  206. package/dist/tests/units/modules/cli-configuration/interface-adapters/gateways/projectConfig.fileSystem.gateway.test.js.map +1 -0
  207. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.d.ts +2 -0
  208. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.d.ts.map +1 -0
  209. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.js +76 -0
  210. package/dist/tests/units/modules/cli-configuration/usecases/cli/removeRepositoryFromConfig.usecase.test.js.map +1 -0
  211. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.d.ts +2 -0
  212. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.d.ts.map +1 -0
  213. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.js +84 -0
  214. package/dist/tests/units/modules/cli-configuration/usecases/cli/toggleRepositoryEnabled.usecase.test.js.map +1 -0
  215. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.d.ts +2 -0
  216. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.d.ts.map +1 -0
  217. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.js +141 -0
  218. package/dist/tests/units/modules/cli-configuration/usecases/projectConfig/updateProjectConfig.usecase.test.js.map +1 -0
  219. package/dist/tests/units/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.test.js +44 -0
  220. package/dist/tests/units/modules/statistics-insights/interface-adapters/controllers/http/overview.routes.test.js.map +1 -1
  221. package/dist/tests/units/modules/statistics-insights/interface-adapters/presenters/overview.presenter.test.js +42 -0
  222. package/dist/tests/units/modules/statistics-insights/interface-adapters/presenters/overview.presenter.test.js.map +1 -1
  223. package/dist/tests/units/services/reviewRecovery.service.test.d.ts +2 -0
  224. package/dist/tests/units/services/reviewRecovery.service.test.d.ts.map +1 -0
  225. package/dist/tests/units/services/reviewRecovery.service.test.js +49 -0
  226. package/dist/tests/units/services/reviewRecovery.service.test.js.map +1 -0
  227. package/dist/tests/units/services/runReviewRecovery.service.test.d.ts +2 -0
  228. package/dist/tests/units/services/runReviewRecovery.service.test.d.ts.map +1 -0
  229. package/dist/tests/units/services/runReviewRecovery.service.test.js +199 -0
  230. package/dist/tests/units/services/runReviewRecovery.service.test.js.map +1 -0
  231. package/package.json +1 -1
@@ -4,13 +4,25 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Reviewflow Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
7
10
  <link rel="stylesheet" href="styles.css">
8
11
  <script src="https://unpkg.com/lucide@latest"></script>
9
12
  </head>
10
13
  <body>
11
14
  <div class="container">
12
15
  <header>
13
- <div class="logo"><i data-lucide="bot"></i></div>
16
+ <div class="logo" aria-label="Reviewflow">
17
+ <svg viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
18
+ <rect class="logo-frame" x="2" y="2" width="40" height="40" rx="10"/>
19
+ <line class="logo-line" x1="10" y1="14" x2="26" y2="14"/>
20
+ <line class="logo-line logo-line--accent" x1="10" y1="22" x2="20" y2="22"/>
21
+ <line class="logo-line" x1="10" y1="30" x2="24" y2="30"/>
22
+ <path class="logo-check" d="M28 26 L31 29 L36 22"/>
23
+ <circle class="logo-pulse" cx="20" cy="22" r="1.7"/>
24
+ </svg>
25
+ </div>
14
26
  <h1>Reviewflow</h1>
15
27
  <div class="header-actions">
16
28
  <div class="version-update-wrapper">
@@ -30,6 +42,43 @@
30
42
  </div>
31
43
  </header>
32
44
 
45
+ <div class="project-bar" role="region" aria-label="Project navigation">
46
+ <button type="button" id="manage-projects-toggle" class="manage-projects-toggle" aria-expanded="false" aria-controls="manage-panel">
47
+ <span class="manage-projects-toggle-label">// MANAGE PROJECTS</span>
48
+ <i data-lucide="chevron-down" class="manage-projects-toggle-icon"></i>
49
+ </button>
50
+ <section id="manage-panel" class="manage-panel" aria-label="Manage projects" data-open="false"></section>
51
+ <nav id="dashboard-tabs" class="dashboard-tab-bar-wrapper" aria-label="Project tabs"></nav>
52
+ <div class="context-chips" aria-label="Toolchain status">
53
+ <div class="toolchain-chip">
54
+ <span class="toolchain-chip-label" id="i18n-card-claude-cli"></span>
55
+ <div id="claude-status" class="card-claude checking">
56
+ <span class="status" id="i18n-claude-checking"></span>
57
+ <span class="version"></span>
58
+ </div>
59
+ </div>
60
+ <div class="toolchain-chip" id="git-cli-card">
61
+ <span class="toolchain-chip-label" id="git-cli-label"></span>
62
+ <div id="git-cli-status" class="card-claude checking">
63
+ <span class="status" id="i18n-git-load-project"></span>
64
+ <span class="version"></span>
65
+ </div>
66
+ </div>
67
+ <div class="toolchain-chip toolchain-chip-model">
68
+ <span class="toolchain-chip-label" id="i18n-card-model"></span>
69
+ <select id="model-select" class="model-select" onchange="changeModel(this.value)">
70
+ <option value="opus" id="i18n-model-opus"></option>
71
+ <option value="sonnet" id="i18n-model-sonnet"></option>
72
+ </select>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div id="cards-scope-marker" class="cards-scope-marker" data-scope-kind="overview">
78
+ <span class="cards-scope-prefix">// SCOPE</span>
79
+ <span class="cards-scope-label">TOUS LES PROJETS</span>
80
+ </div>
81
+
33
82
  <div class="cards">
34
83
  <div class="card card-priority">
35
84
  <div class="card-label" id="i18n-card-running"></div>
@@ -43,45 +92,21 @@
43
92
  <div class="card-label" id="i18n-card-completed"></div>
44
93
  <div id="completed-count" class="card-value">-</div>
45
94
  </div>
46
- <div class="card">
47
- <div class="card-label" id="i18n-card-claude-cli"></div>
48
- <div id="claude-status" class="card-claude checking">
49
- <span class="status" id="i18n-claude-checking"></span>
50
- <span class="version"></span>
51
- </div>
52
- </div>
53
- <div class="card" id="git-cli-card">
54
- <div class="card-label" id="git-cli-label"></div>
55
- <div id="git-cli-status" class="card-claude checking">
56
- <span class="status" id="i18n-git-load-project"></span>
57
- <span class="version"></span>
58
- </div>
59
- </div>
60
- <div class="card">
61
- <div class="card-label" id="i18n-card-model"></div>
62
- <div class="card-model">
63
- <select id="model-select" class="model-select" onchange="changeModel(this.value)">
64
- <option value="opus" id="i18n-model-opus"></option>
65
- <option value="sonnet" id="i18n-model-sonnet"></option>
66
- </select>
67
- </div>
68
- </div>
69
95
  </div>
70
96
 
71
97
  <div class="dashboard-layout">
72
98
  <aside class="dashboard-sidebar" aria-label="Project tools">
73
- <div class="sidebar-language">
74
- <label class="sidebar-language-label" for="language-select" id="i18n-card-language"></label>
75
- <select id="language-select" class="model-select" onchange="changeLanguage(this.value)">
76
- <option value="en">English</option>
77
- <option value="fr">Français</option>
78
- </select>
79
- </div>
99
+ <button type="button" id="open-settings-modal-btn" class="sidebar-settings-button" hidden>
100
+ <span class="sidebar-settings-button__prefix">// SETTINGS</span>
101
+ </button>
80
102
 
81
- <nav id="dashboard-tabs" class="dashboard-tab-bar-wrapper" aria-label="Project tabs"></nav>
82
103
  <span id="config-status" class="config-status hidden"></span>
83
104
 
84
- <div class="focus-strip">
105
+ <section id="worktree-section" aria-label="Worktree pool"></section>
106
+ </aside>
107
+
108
+ <main class="dashboard-main">
109
+ <div class="focus-strip attention-strip" aria-label="Attention indicators">
85
110
  <div class="focus-chip focus-now">
86
111
  <div class="focus-copy">
87
112
  <span class="focus-label" id="i18n-strip-now"></span>
@@ -106,10 +131,6 @@
106
131
  <button id="focus-strip-toggle" class="focus-toggle-btn" onclick="toggleFocusStripMode()"></button>
107
132
  </div>
108
133
 
109
- <section id="worktree-section" aria-label="Worktree pool"></section>
110
- </aside>
111
-
112
- <main class="dashboard-main">
113
134
  <section id="overview-section" class="overview-section" aria-label="Multi-project overview"></section>
114
135
 
115
136
  <div id="data-loading-state" class="data-loading hidden" role="status" aria-live="polite">
@@ -137,7 +158,11 @@
137
158
  <span id="pending-reviews-count" class="badge-count hidden">0</span>
138
159
  </div>
139
160
  <div id="pending-reviews" class="section-content">
140
- <div class="empty-state" id="i18n-empty-pending-reviews"></div>
161
+ <div class="heartbeat-empty-state" id="pending-reviews-empty-state">
162
+ <span class="heartbeat-label">// STANDBY</span>
163
+ <span class="heartbeat-message" id="i18n-empty-pending-reviews">No reviews waiting for confirmation</span>
164
+ <div class="heartbeat-line-container" id="heartbeat-line" aria-hidden="true"></div>
165
+ </div>
141
166
  </div>
142
167
  </div>
143
168
 
@@ -304,16 +329,41 @@
304
329
  <div id="dev-sheet-content" class="sheet-content"></div>
305
330
  </div>
306
331
 
332
+ <dialog id="settings-modal" class="settings-modal" aria-labelledby="settings-modal-title"></dialog>
333
+
334
+ <!-- UI language select — kept in DOM for JS wiring; visual control lives in settings modal -->
335
+ <select id="language-select" style="display:none" aria-hidden="true" onchange="changeLanguage(this.value)">
336
+ <option value="en">English</option>
337
+ <option value="fr">Français</option>
338
+ </select>
339
+
307
340
  <script type="module">
308
341
  import { t, setLanguage, getLanguage } from './modules/i18n.js';
309
342
  import { formatTime, formatDuration, formatPhase, formatLogTime } from './modules/formatting.js';
343
+ import {
344
+ reducedMotion,
345
+ animateMount,
346
+ animateCounter as animateCounterValue,
347
+ slideTabUnderline,
348
+ heartbeat,
349
+ pulseLive,
350
+ springIn,
351
+ liftCard,
352
+ unliftCard,
353
+ pulseStatusDot,
354
+ breatheLogo,
355
+ crossFadeTab,
356
+ toggleHeight,
357
+ } from './modules/animations.js';
310
358
  import { escapeHtml, markdownToHtml, sanitizeHttpUrl } from './modules/html.js';
311
359
  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';
360
+ import { MAX_RECONNECT_ATTEMPTS, RECONNECT_DELAY, STORAGE_KEY_CURRENT, STORAGE_KEY_FOCUS_STRIP_MODE, QUALITY_TARGET_SCORE } from './modules/constants.js';
313
361
  import { buildTabBarModel, renderTabBarHtml, readActiveTab, writeActiveTab } from './modules/tabBar.js';
362
+ import { buildManagePanelModel, renderManagePanelHtml, buildOptimisticAddedRow, validateLocalPathInput } from './modules/managePanel.js';
314
363
  import { renderOverviewHtml } from './modules/overview.js';
315
364
  import { getDesktopNotificationPayload, shouldNotifyDesktop } from './modules/desktopNotifications.js';
316
365
  import { getLoadingPresentation, getQuietRefreshSectionIdentifiers } from './modules/loading.js';
366
+ import { computeCardCounters, extractGithubSlug } from './modules/cardCounters.js';
317
367
  import { collectReviewNotifications, createReviewNotificationState } from './modules/notifications.js';
318
368
  import { resolveReviewAssigneeDisplay } from './modules/assignee.js';
319
369
  import { buildQueueLanesModel } from './modules/queueLanes.js';
@@ -345,6 +395,12 @@
345
395
  fetchBudgetStatus,
346
396
  submitBudget,
347
397
  } from './modules/budgetSettings.js';
398
+ import {
399
+ buildSettingsViewModel,
400
+ renderSettingsModalHtml,
401
+ validateExternalLink,
402
+ extractFormPayload,
403
+ } from './modules/settingsModal.js';
348
404
 
349
405
  const API_URL = window.location.origin;
350
406
  const WS_URL = `ws://${window.location.host}/ws`;
@@ -767,18 +823,23 @@
767
823
  const reviews = currentData.activeReviews.filter(r => r.jobType !== 'followup');
768
824
  const followups = currentData.activeReviews.filter(r => r.jobType === 'followup');
769
825
 
826
+ renderCardCounters();
827
+ const blocked = currentData.pendingFix.length;
770
828
  const running = currentData.activeReviews.filter(r => r.status === 'running').length;
771
829
  const queued = currentData.activeReviews.filter(r => r.status === 'queued').length;
772
- const blocked = currentData.pendingFix.length;
773
830
  const nowCount = running + blocked;
774
831
  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
832
  document.getElementById('focus-now-count').textContent = String(nowCount);
779
833
  document.getElementById('focus-next-count').textContent = String(nextCount);
780
834
  document.getElementById('focus-blocked-count').textContent = String(blocked);
781
835
 
836
+ const chipNow = document.querySelector('.focus-chip.focus-now');
837
+ const chipNext = document.querySelector('.focus-chip.focus-next');
838
+ const chipBlocked = document.querySelector('.focus-chip.focus-blocked');
839
+ if (chipNow) chipNow.dataset.active = nowCount > 0 ? 'true' : 'false';
840
+ if (chipNext) chipNext.dataset.active = nextCount > 0 ? 'true' : 'false';
841
+ if (chipBlocked) chipBlocked.dataset.active = blocked > 0 ? 'critical' : 'false';
842
+
782
843
  const activeReviewsSection = document.getElementById('active-reviews-section');
783
844
  const activeReviewsEl = document.getElementById('active-reviews');
784
845
  const activeReviewsCount = document.getElementById('active-reviews-count');
@@ -2273,71 +2334,6 @@
2273
2334
  let currentProjectConfig = null;
2274
2335
  let currentProjectPath = null;
2275
2336
 
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
2337
  async function loadProjectConfigFromPath(projectPath) {
2342
2338
  const status = document.getElementById('config-status');
2343
2339
  const info = document.getElementById('config-info');
@@ -2353,14 +2349,8 @@
2353
2349
  currentProjectConfig = data.config;
2354
2350
  currentProjectPath = projectPath;
2355
2351
 
2356
- addProjectToHistory(projectPath);
2357
2352
  localStorage.setItem(STORAGE_KEY_CURRENT, projectPath);
2358
2353
 
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
2354
  const shortName = projectPath.split('/').slice(-2).join('/');
2365
2355
  showConfigStatus(`<i data-lucide="check-circle"></i> ${escapeHtml(shortName)}`, 'success');
2366
2356
  refreshIcons();
@@ -2413,43 +2403,6 @@
2413
2403
  refreshIcons();
2414
2404
  }
2415
2405
 
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
2406
  // SPEC-91 — Dashboard Multi-Project Overview
2454
2407
  let availableRepositories = [];
2455
2408
  let activeTabId = 'overview';
@@ -2458,14 +2411,48 @@
2458
2411
  try {
2459
2412
  const response = await fetch(`${API_URL}/api/repositories`);
2460
2413
  const data = await response.json();
2461
- availableRepositories = Array.isArray(data.repositories)
2462
- ? data.repositories.filter((repository) => repository.enabled)
2463
- : [];
2414
+ availableRepositories = Array.isArray(data.repositories) ? data.repositories : [];
2464
2415
  } catch {
2465
2416
  availableRepositories = [];
2466
2417
  }
2467
2418
  }
2468
2419
 
2420
+ function syncAvailableRepositoriesFromResponse(payload) {
2421
+ availableRepositories = Array.isArray(payload?.repositories) ? payload.repositories : [];
2422
+ }
2423
+
2424
+ function renderCardCounters() {
2425
+ let scope;
2426
+ if (activeTabId === 'overview') {
2427
+ scope = { kind: 'overview' };
2428
+ } else {
2429
+ const repository = availableRepositories.find((r) => r.localPath === activeTabId);
2430
+ const projectName = repository?.name ?? activeTabId.split('/').filter(Boolean).pop() ?? activeTabId;
2431
+ const aliases = [];
2432
+ const slug = extractGithubSlug(repository?.remoteUrl);
2433
+ if (slug) aliases.push(slug);
2434
+ if (repository?.name) aliases.push(repository.name);
2435
+ scope = { kind: 'project', localPath: activeTabId, projectName, aliases };
2436
+ }
2437
+ const counters = computeCardCounters({
2438
+ activeReviews: currentData.activeReviews,
2439
+ reviewFiles: currentData.reviewFiles,
2440
+ scope,
2441
+ });
2442
+ const runningEl = document.getElementById('running-count');
2443
+ const queuedEl = document.getElementById('queued-count');
2444
+ const completedEl = document.getElementById('completed-count');
2445
+ if (runningEl) runningEl.textContent = counters.running;
2446
+ if (queuedEl) queuedEl.textContent = counters.queued;
2447
+ if (completedEl) completedEl.textContent = counters.completed;
2448
+ const markerEl = document.getElementById('cards-scope-marker');
2449
+ if (markerEl) {
2450
+ markerEl.dataset.scopeKind = counters.markerKind;
2451
+ const labelEl = markerEl.querySelector('.cards-scope-label');
2452
+ if (labelEl) labelEl.textContent = counters.markerLabel;
2453
+ }
2454
+ }
2455
+
2469
2456
  function renderDashboardTabs() {
2470
2457
  const container = document.getElementById('dashboard-tabs');
2471
2458
  if (!container) return;
@@ -2481,6 +2468,7 @@
2481
2468
  handleTabClick(tabId);
2482
2469
  });
2483
2470
  });
2471
+ setupTabUnderline();
2484
2472
  }
2485
2473
 
2486
2474
  function handleTabClick(tabId) {
@@ -2498,6 +2486,8 @@
2498
2486
  const overviewSection = document.getElementById('overview-section');
2499
2487
  if (overviewSection) overviewSection.classList.remove('hidden');
2500
2488
  renderDashboardTabs();
2489
+ renderCardCounters();
2490
+ syncSettingsButtonVisibility();
2501
2491
  refreshOverviewSection();
2502
2492
  }
2503
2493
 
@@ -2508,9 +2498,125 @@
2508
2498
  const overviewSection = document.getElementById('overview-section');
2509
2499
  if (overviewSection) overviewSection.classList.add('hidden');
2510
2500
  renderDashboardTabs();
2501
+ renderCardCounters();
2502
+ syncSettingsButtonVisibility();
2511
2503
  loadProjectConfigFromPath(projectPath);
2512
2504
  }
2513
2505
 
2506
+ function syncSettingsButtonVisibility() {
2507
+ const button = document.getElementById('open-settings-modal-btn');
2508
+ if (!button) return;
2509
+ if (activeTabId === 'overview') {
2510
+ button.hidden = true;
2511
+ } else {
2512
+ button.hidden = false;
2513
+ }
2514
+ }
2515
+
2516
+ function resolveActiveProjectName() {
2517
+ if (activeTabId === 'overview') return '—';
2518
+ const repository = availableRepositories.find((r) => r.localPath === activeTabId);
2519
+ if (repository?.name) return repository.name;
2520
+ return activeTabId.split('/').filter(Boolean).pop() ?? activeTabId;
2521
+ }
2522
+
2523
+ async function openSettingsModal() {
2524
+ if (activeTabId === 'overview') return;
2525
+ const dialog = document.getElementById('settings-modal');
2526
+ if (!dialog || typeof dialog.showModal !== 'function') return;
2527
+ try {
2528
+ const response = await fetch(
2529
+ `${API_URL}/api/project-config?path=${encodeURIComponent(activeTabId)}`,
2530
+ );
2531
+ const payload = await response.json();
2532
+ if (!payload?.success) {
2533
+ 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>`;
2534
+ dialog.showModal();
2535
+ return;
2536
+ }
2537
+ const viewModel = buildSettingsViewModel({
2538
+ config: payload.config,
2539
+ projectName: resolveActiveProjectName(),
2540
+ });
2541
+ dialog.innerHTML = renderSettingsModalHtml(viewModel);
2542
+ bindSettingsModalForm(dialog);
2543
+ const uiLangSelect = dialog.querySelector('#settings-modal-ui-language');
2544
+ if (uiLangSelect) {
2545
+ const hiddenSelect = document.getElementById('language-select');
2546
+ uiLangSelect.value = hiddenSelect ? hiddenSelect.value : getLanguage();
2547
+ }
2548
+ dialog.showModal();
2549
+ } catch (error) {
2550
+ showToast(t('toast.error') || 'Erreur', 'error');
2551
+ }
2552
+ }
2553
+
2554
+ function closeSettingsModal() {
2555
+ const dialog = document.getElementById('settings-modal');
2556
+ if (dialog && dialog.open) dialog.close();
2557
+ }
2558
+
2559
+ function bindSettingsModalForm(dialog) {
2560
+ const form = dialog.querySelector('form.settings-modal__form');
2561
+ const cancelBtn = dialog.querySelector('.settings-modal__cancel');
2562
+ const errorEl = dialog.querySelector('.settings-modal__error');
2563
+ if (cancelBtn) {
2564
+ cancelBtn.addEventListener('click', (event) => {
2565
+ event.preventDefault();
2566
+ closeSettingsModal();
2567
+ });
2568
+ }
2569
+ if (!form) return;
2570
+ form.addEventListener('submit', async (event) => {
2571
+ event.preventDefault();
2572
+ if (errorEl) errorEl.textContent = '';
2573
+ const formData = new FormData(form);
2574
+ const payload = extractFormPayload(formData);
2575
+ const linkValidation = validateExternalLink(payload.externalLink ?? '');
2576
+ if (!linkValidation.ok) {
2577
+ if (errorEl) errorEl.textContent = linkValidation.message;
2578
+ return;
2579
+ }
2580
+ try {
2581
+ const response = await fetch(
2582
+ `${API_URL}/api/project-config?path=${encodeURIComponent(activeTabId)}`,
2583
+ {
2584
+ method: 'PATCH',
2585
+ headers: { 'Content-Type': 'application/json' },
2586
+ body: JSON.stringify(payload),
2587
+ },
2588
+ );
2589
+ if (!response.ok) {
2590
+ const errorPayload = await response.json().catch(() => ({ error: 'Échec de la sauvegarde' }));
2591
+ if (errorEl) errorEl.textContent = errorPayload?.error || 'Échec de la sauvegarde';
2592
+ return;
2593
+ }
2594
+ closeSettingsModal();
2595
+ refreshOverviewSection();
2596
+ } catch {
2597
+ if (errorEl) errorEl.textContent = 'Échec de la sauvegarde';
2598
+ }
2599
+ });
2600
+ }
2601
+
2602
+ function bindSettingsModalDismissals() {
2603
+ const dialog = document.getElementById('settings-modal');
2604
+ if (!dialog) return;
2605
+ dialog.addEventListener('click', (event) => {
2606
+ if (event.target === dialog) {
2607
+ closeSettingsModal();
2608
+ }
2609
+ });
2610
+ }
2611
+
2612
+ function bindSettingsModalTrigger() {
2613
+ const button = document.getElementById('open-settings-modal-btn');
2614
+ if (!button) return;
2615
+ button.addEventListener('click', () => {
2616
+ openSettingsModal();
2617
+ });
2618
+ }
2619
+
2514
2620
  async function refreshOverviewSection() {
2515
2621
  const container = document.getElementById('overview-section');
2516
2622
  if (!container) return;
@@ -2525,6 +2631,9 @@
2525
2631
  if (projectPath) activateProjectTab(projectPath);
2526
2632
  });
2527
2633
  });
2634
+ container.querySelectorAll('.project-card__external').forEach((anchor) => {
2635
+ anchor.addEventListener('click', (event) => event.stopPropagation());
2636
+ });
2528
2637
  } catch {
2529
2638
  // Silent: WS reconnection or next refresh will retry
2530
2639
  }
@@ -2532,7 +2641,10 @@
2532
2641
 
2533
2642
  async function initOverviewAndTabs() {
2534
2643
  await fetchAvailableRepositories();
2535
- await syncServerRepositories();
2644
+ renderManagePanel();
2645
+ bindManagePanelToggle();
2646
+ bindSettingsModalTrigger();
2647
+ bindSettingsModalDismissals();
2536
2648
  const persistedTab = readActiveTab();
2537
2649
  const repositoryPaths = availableRepositories.map((repository) => repository.localPath);
2538
2650
  if (persistedTab && persistedTab !== 'overview' && repositoryPaths.includes(persistedTab)) {
@@ -2542,6 +2654,202 @@
2542
2654
  }
2543
2655
  }
2544
2656
 
2657
+ let isManagePanelOpen = false;
2658
+
2659
+ function renderManagePanel() {
2660
+ const panel = document.getElementById('manage-panel');
2661
+ if (!panel) return;
2662
+ const model = buildManagePanelModel({ repositories: availableRepositories, isOpen: isManagePanelOpen });
2663
+ panel.innerHTML = renderManagePanelHtml(model);
2664
+ panel.dataset.open = isManagePanelOpen ? 'true' : 'false';
2665
+ bindManagePanelHandlers();
2666
+ }
2667
+
2668
+ function bindManagePanelToggle() {
2669
+ const toggle = document.getElementById('manage-projects-toggle');
2670
+ if (!toggle || toggle.dataset.bound === 'true') return;
2671
+ toggle.dataset.bound = 'true';
2672
+ toggle.addEventListener('click', () => {
2673
+ isManagePanelOpen = !isManagePanelOpen;
2674
+ toggle.setAttribute('aria-expanded', isManagePanelOpen ? 'true' : 'false');
2675
+ renderManagePanel();
2676
+ const panel = document.getElementById('manage-panel');
2677
+ if (panel) {
2678
+ loadAnimeApi().then((anime) => {
2679
+ if (anime) toggleHeight(panel, isManagePanelOpen, { animeApi: anime });
2680
+ }).catch(() => {});
2681
+ }
2682
+ });
2683
+ }
2684
+
2685
+ function bindManagePanelHandlers() {
2686
+ const panel = document.getElementById('manage-panel');
2687
+ if (!panel) return;
2688
+ const form = panel.querySelector('form.add-form');
2689
+ if (form) {
2690
+ form.addEventListener('submit', handleAddProjectSubmit);
2691
+ const input = form.querySelector('input.add-form-input');
2692
+ if (input) {
2693
+ input.addEventListener('keydown', (event) => {
2694
+ if (event.key === 'Escape') {
2695
+ input.value = '';
2696
+ clearManagePanelError();
2697
+ }
2698
+ });
2699
+ }
2700
+ }
2701
+ panel.querySelectorAll('.manage-row-delete').forEach((button) => {
2702
+ button.addEventListener('click', () => {
2703
+ const row = button.closest('.manage-row');
2704
+ const localPath = row?.dataset.localPath;
2705
+ if (localPath) handleDeleteProject(localPath, row);
2706
+ });
2707
+ });
2708
+ panel.querySelectorAll('.manage-row-toggle').forEach((button) => {
2709
+ button.addEventListener('click', () => {
2710
+ const row = button.closest('.manage-row');
2711
+ const localPath = row?.dataset.localPath;
2712
+ const currentlyEnabled = row?.dataset.enabled === 'true';
2713
+ if (localPath) handleToggleProject(localPath, !currentlyEnabled);
2714
+ });
2715
+ });
2716
+ }
2717
+
2718
+ function setManagePanelError(message) {
2719
+ const panel = document.getElementById('manage-panel');
2720
+ const target = panel?.querySelector('[data-role="error-message"]');
2721
+ if (!target) return;
2722
+ target.textContent = message;
2723
+ target.hidden = false;
2724
+ }
2725
+
2726
+ function clearManagePanelError() {
2727
+ const panel = document.getElementById('manage-panel');
2728
+ const target = panel?.querySelector('[data-role="error-message"]');
2729
+ if (!target) return;
2730
+ target.textContent = '';
2731
+ target.hidden = true;
2732
+ }
2733
+
2734
+ function flashAddFormError() {
2735
+ const form = document.querySelector('#manage-panel form.add-form');
2736
+ if (!form) return;
2737
+ form.classList.remove('is-error');
2738
+ requestAnimationFrame(() => form.classList.add('is-error'));
2739
+ setTimeout(() => form.classList.remove('is-error'), 320);
2740
+ }
2741
+
2742
+ function flashAddFormSuccess() {
2743
+ const input = document.querySelector('#manage-panel input.add-form-input');
2744
+ if (!input) return;
2745
+ input.classList.remove('is-success');
2746
+ requestAnimationFrame(() => input.classList.add('is-success'));
2747
+ setTimeout(() => input.classList.remove('is-success'), 1500);
2748
+ }
2749
+
2750
+ function flashTabEntering(localPath) {
2751
+ const tab = document.querySelector(`.dashboard-tab[data-tab-id="${CSS.escape(localPath)}"]`);
2752
+ if (!tab) return;
2753
+ tab.classList.add('is-entering');
2754
+ setTimeout(() => tab.classList.remove('is-entering'), 1500);
2755
+ }
2756
+
2757
+ async function handleAddProjectSubmit(event) {
2758
+ event.preventDefault();
2759
+ const form = event.currentTarget;
2760
+ const input = form.querySelector('input.add-form-input');
2761
+ const submit = form.querySelector('button.add-form-submit');
2762
+ const rawValue = input ? input.value : '';
2763
+ const validation = validateLocalPathInput(rawValue);
2764
+ clearManagePanelError();
2765
+ if (!validation.ok) {
2766
+ const message = validation.reason === 'empty' ? 'Chemin du projet requis' : 'Le chemin doit être absolu';
2767
+ setManagePanelError(message);
2768
+ flashAddFormError();
2769
+ return;
2770
+ }
2771
+ const trimmed = rawValue.trim();
2772
+ if (submit) submit.classList.add('is-busy');
2773
+ try {
2774
+ const response = await fetch(`${API_URL}/api/repositories`, {
2775
+ method: 'POST',
2776
+ headers: { 'Content-Type': 'application/json' },
2777
+ body: JSON.stringify({ localPath: trimmed }),
2778
+ });
2779
+ const body = await response.json().catch(() => ({}));
2780
+ if (!response.ok) {
2781
+ setManagePanelError(typeof body?.error === 'string' ? body.error : 'Erreur inconnue');
2782
+ flashAddFormError();
2783
+ return;
2784
+ }
2785
+ syncAvailableRepositoriesFromResponse(body);
2786
+ if (input) input.value = '';
2787
+ flashAddFormSuccess();
2788
+ renderManagePanel();
2789
+ renderDashboardTabs();
2790
+ flashTabEntering(trimmed);
2791
+ } catch {
2792
+ setManagePanelError('Erreur réseau');
2793
+ flashAddFormError();
2794
+ } finally {
2795
+ if (submit) submit.classList.remove('is-busy');
2796
+ }
2797
+ }
2798
+
2799
+ async function handleDeleteProject(localPath, rowElement) {
2800
+ try {
2801
+ const response = await fetch(`${API_URL}/api/repositories?localPath=${encodeURIComponent(localPath)}`, {
2802
+ method: 'DELETE',
2803
+ });
2804
+ const body = await response.json().catch(() => ({}));
2805
+ if (!response.ok) {
2806
+ setManagePanelError(typeof body?.error === 'string' ? body.error : 'Erreur inconnue');
2807
+ return;
2808
+ }
2809
+ syncAvailableRepositoriesFromResponse(body);
2810
+ if (rowElement) {
2811
+ rowElement.classList.add('is-leaving');
2812
+ setTimeout(() => {
2813
+ renderManagePanel();
2814
+ }, 250);
2815
+ } else {
2816
+ renderManagePanel();
2817
+ }
2818
+ const tab = document.querySelector(`.dashboard-tab[data-tab-id="${CSS.escape(localPath)}"]`);
2819
+ if (tab) {
2820
+ tab.classList.add('is-leaving');
2821
+ setTimeout(() => renderDashboardTabs(), 250);
2822
+ } else {
2823
+ renderDashboardTabs();
2824
+ }
2825
+ if (activeTabId === localPath) {
2826
+ activateOverviewTab();
2827
+ }
2828
+ } catch {
2829
+ setManagePanelError('Erreur réseau');
2830
+ }
2831
+ }
2832
+
2833
+ async function handleToggleProject(localPath, enabled) {
2834
+ try {
2835
+ const response = await fetch(`${API_URL}/api/repositories?localPath=${encodeURIComponent(localPath)}`, {
2836
+ method: 'PATCH',
2837
+ headers: { 'Content-Type': 'application/json' },
2838
+ body: JSON.stringify({ enabled }),
2839
+ });
2840
+ const body = await response.json().catch(() => ({}));
2841
+ if (!response.ok) {
2842
+ setManagePanelError(typeof body?.error === 'string' ? body.error : 'Erreur inconnue');
2843
+ return;
2844
+ }
2845
+ syncAvailableRepositoriesFromResponse(body);
2846
+ renderManagePanel();
2847
+ renderDashboardTabs();
2848
+ } catch {
2849
+ setManagePanelError('Erreur réseau');
2850
+ }
2851
+ }
2852
+
2545
2853
  function getMrLabel(platform = activePlatform) {
2546
2854
  return platform === 'github' ? 'PR' : 'MR';
2547
2855
  }
@@ -2681,19 +2989,6 @@
2681
2989
  const modelSonnet = document.getElementById('i18n-model-sonnet');
2682
2990
  if (modelSonnet) modelSonnet.textContent = t('model.sonnet');
2683
2991
 
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
2992
  // Login sections
2698
2993
  const claudeLoginTitle = document.getElementById('i18n-claude-login-title');
2699
2994
  if (claudeLoginTitle) claudeLoginTitle.textContent = t('login.claude.title');
@@ -2792,9 +3087,6 @@
2792
3087
 
2793
3088
  const modalConfirm = document.getElementById('cancel-modal-confirm');
2794
3089
  if (modalConfirm) modalConfirm.textContent = t('modal.confirm');
2795
-
2796
- // Update project select placeholder (re-render the select)
2797
- updateProjectSelect();
2798
3090
  }
2799
3091
 
2800
3092
  async function checkForUpdates() {
@@ -2952,9 +3244,6 @@
2952
3244
  window.toggleStats = toggleStats;
2953
3245
  window.changeModel = changeModel;
2954
3246
  window.changeLanguage = changeLanguage;
2955
- window.onProjectSelect = onProjectSelect;
2956
- window.loadProjectConfig = loadProjectConfig;
2957
- window.removeCurrentProject = removeCurrentProject;
2958
3247
  window.toggleReviewAccordion = toggleReviewAccordion;
2959
3248
  window.toggleReviewDescription = toggleReviewDescription;
2960
3249
  window.deleteReviewFile = deleteReviewFile;
@@ -3153,7 +3442,169 @@
3153
3442
  }
3154
3443
 
3155
3444
  refreshWorktreeSection({ animate: true });
3156
- setInterval(() => refreshWorktreeSection({ animate: false }), 30000);
3445
+ setInterval(() => refreshWorktreeSection({ animate: false }), 5000);
3446
+
3447
+ // === Operator's Console boot animations ===
3448
+
3449
+ let heartbeatStop = null;
3450
+
3451
+ async function bootAnimations() {
3452
+ const anime = await loadAnimeApi();
3453
+ if (!anime) return;
3454
+
3455
+ // Logo breath — daemon-alive signal
3456
+ const logo = document.querySelector('.logo');
3457
+ if (logo) breatheLogo(logo, { animeApi: anime });
3458
+
3459
+ // Online status dot pulse
3460
+ const statusDot = document.querySelector('.status-dot');
3461
+ if (statusDot) pulseStatusDot(statusDot, { animeApi: anime });
3462
+
3463
+ // Metric cards stagger mount
3464
+ const cards = document.querySelectorAll('.cards > .card');
3465
+ if (cards.length > 0) animateMount(cards, { animeApi: anime });
3466
+
3467
+ // Sidebar slide-in
3468
+ const sidebar = document.querySelector('.dashboard-sidebar');
3469
+ if (sidebar && !reducedMotion()) {
3470
+ anime.animate(sidebar, {
3471
+ opacity: [{ from: 0, to: 1 }],
3472
+ translateX: [{ from: -8, to: 0 }],
3473
+ duration: 280,
3474
+ easing: 'easeOutCubic',
3475
+ });
3476
+ }
3477
+
3478
+ // Attention strip mount (focus-strip promoted to main)
3479
+ const focusChips = document.querySelectorAll('.focus-chip');
3480
+ if (focusChips.length > 0) animateMount(focusChips, { animeApi: anime, staggerMs: 40, yOffset: 8 });
3481
+
3482
+ // Heartbeat on pending-reviews empty state
3483
+ startHeartbeat(anime);
3484
+
3485
+ // Card hover lift — wire to existing cards
3486
+ wireCardHovers(anime);
3487
+
3488
+ // Settings modal spring-in
3489
+ wireSettingsModal(anime);
3490
+ }
3491
+
3492
+ function startHeartbeat(anime) {
3493
+ const container = document.getElementById('heartbeat-line');
3494
+ if (!container) return;
3495
+ if (heartbeatStop) heartbeatStop.stop();
3496
+ heartbeatStop = heartbeat(container, { animeApi: anime });
3497
+ }
3498
+
3499
+ function wireCardHovers(anime) {
3500
+ const cards = document.querySelectorAll('.cards > .card');
3501
+ for (const card of cards) {
3502
+ card.addEventListener('mouseenter', () => liftCard(card, { animeApi: anime }));
3503
+ card.addEventListener('mouseleave', () => unliftCard(card, { animeApi: anime }));
3504
+ }
3505
+ }
3506
+
3507
+ function wireSettingsModal(anime) {
3508
+ const modal = document.getElementById('settings-modal');
3509
+ if (!modal) return;
3510
+ modal.addEventListener('animationsjs-open', () => springIn(modal, { animeApi: anime }));
3511
+ }
3512
+
3513
+ // Tab underline glide — set up after tab bar renders
3514
+ let tabUnderlineEl = null;
3515
+
3516
+ function setupTabUnderline() {
3517
+ const tabBar = document.querySelector('.dashboard-tab-bar');
3518
+ if (!tabBar) return;
3519
+
3520
+ if (!tabUnderlineEl) {
3521
+ tabUnderlineEl = document.createElement('div');
3522
+ tabUnderlineEl.className = 'tab-underline-indicator';
3523
+ tabBar.style.position = 'relative';
3524
+ tabBar.appendChild(tabUnderlineEl);
3525
+ }
3526
+
3527
+ const activeTab = tabBar.querySelector('.is-active');
3528
+ if (activeTab) {
3529
+ loadAnimeApi().then((anime) => {
3530
+ if (anime && tabUnderlineEl) slideTabUnderline(tabUnderlineEl, activeTab, { animeApi: anime });
3531
+ }).catch(() => {});
3532
+ }
3533
+ }
3534
+
3535
+ // Counter animation — wrap renderCardCounters and focus strip to animate changes
3536
+ const originalCounterIds = ['running-count', 'queued-count', 'completed-count', 'focus-now-count', 'focus-next-count', 'focus-blocked-count'];
3537
+ const previousCounterValues = {};
3538
+
3539
+ function animateCounterChanges(anime) {
3540
+ for (const id of originalCounterIds) {
3541
+ const el = document.getElementById(id);
3542
+ if (!el) continue;
3543
+ if (el.dataset.animating === 'true') continue;
3544
+ const currentText = el.textContent;
3545
+ const currentVal = parseInt(currentText, 10);
3546
+ const prevVal = previousCounterValues[id];
3547
+ if (!Number.isNaN(currentVal) && prevVal !== undefined && prevVal !== currentVal) {
3548
+ el.dataset.animating = 'true';
3549
+ animateCounterValue(el, prevVal, currentVal, {
3550
+ animeApi: anime,
3551
+ onComplete: () => {
3552
+ el.dataset.animating = 'false';
3553
+ previousCounterValues[id] = currentVal;
3554
+ },
3555
+ });
3556
+ } else if (!Number.isNaN(currentVal)) {
3557
+ previousCounterValues[id] = currentVal;
3558
+ }
3559
+ }
3560
+ }
3561
+
3562
+ // Observe counter element changes
3563
+ async function observeCounters() {
3564
+ const anime = await loadAnimeApi();
3565
+ if (!anime) return;
3566
+
3567
+ // Snapshot initial values
3568
+ for (const id of originalCounterIds) {
3569
+ const el = document.getElementById(id);
3570
+ if (el) {
3571
+ const val = parseInt(el.textContent, 10);
3572
+ if (!Number.isNaN(val)) previousCounterValues[id] = val;
3573
+ }
3574
+ }
3575
+
3576
+ const observer = new MutationObserver(() => animateCounterChanges(anime));
3577
+ for (const id of originalCounterIds) {
3578
+ const el = document.getElementById(id);
3579
+ if (el) observer.observe(el, { childList: true, characterData: true, subtree: true });
3580
+ }
3581
+ }
3582
+
3583
+ // Visibility change — pause/resume heartbeat to save battery
3584
+ document.addEventListener('visibilitychange', () => {
3585
+ loadAnimeApi().then((anime) => {
3586
+ if (!anime) return;
3587
+ if (document.hidden) {
3588
+ if (heartbeatStop) heartbeatStop.stop();
3589
+ } else {
3590
+ startHeartbeat(anime);
3591
+ }
3592
+ }).catch(() => {});
3593
+ });
3594
+
3595
+ // Boot animations & observers exactly once, regardless of readyState timing
3596
+ let __animationsBooted = false;
3597
+ function bootOnce() {
3598
+ if (__animationsBooted) return;
3599
+ __animationsBooted = true;
3600
+ bootAnimations();
3601
+ observeCounters();
3602
+ }
3603
+ if (document.readyState !== 'loading') {
3604
+ bootOnce();
3605
+ } else {
3606
+ window.addEventListener('DOMContentLoaded', bootOnce);
3607
+ }
3157
3608
  </script>
3158
3609
  </body>
3159
3610
  </html>