reviewflow 3.3.1 → 3.5.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.
- package/CHANGELOG.md +37 -0
- package/dist/cli/formatters/initSummary.d.ts +1 -1
- package/dist/cli/formatters/initSummary.d.ts.map +1 -1
- package/dist/cli/formatters/initSummary.js +2 -0
- package/dist/cli/formatters/initSummary.js.map +1 -1
- package/dist/cli/parseCliArgs.d.ts +6 -1
- package/dist/cli/parseCliArgs.d.ts.map +1 -1
- package/dist/cli/parseCliArgs.js +11 -1
- package/dist/cli/parseCliArgs.js.map +1 -1
- package/dist/config/projectConfig.d.ts +6 -0
- package/dist/config/projectConfig.d.ts.map +1 -1
- package/dist/config/projectConfig.js +13 -0
- package/dist/config/projectConfig.js.map +1 -1
- package/dist/entities/language/language.schema.d.ts +7 -0
- package/dist/entities/language/language.schema.d.ts.map +1 -0
- package/dist/entities/language/language.schema.js +3 -0
- package/dist/entities/language/language.schema.js.map +1 -0
- package/dist/entities/mcpSettings/mcpSettings.guard.d.ts +12 -0
- package/dist/entities/mcpSettings/mcpSettings.guard.d.ts.map +1 -0
- package/dist/entities/mcpSettings/mcpSettings.guard.js +15 -0
- package/dist/entities/mcpSettings/mcpSettings.guard.js.map +1 -0
- package/dist/entities/mcpSettings/mcpSettings.schema.d.ts +13 -0
- package/dist/entities/mcpSettings/mcpSettings.schema.d.ts.map +1 -0
- package/dist/entities/mcpSettings/mcpSettings.schema.js +9 -0
- package/dist/entities/mcpSettings/mcpSettings.schema.js.map +1 -0
- package/dist/frameworks/claude/claudeInvoker.d.ts +1 -1
- package/dist/frameworks/claude/claudeInvoker.d.ts.map +1 -1
- package/dist/frameworks/claude/claudeInvoker.js +6 -3
- package/dist/frameworks/claude/claudeInvoker.js.map +1 -1
- package/dist/frameworks/claude/languageDirective.d.ts +3 -0
- package/dist/frameworks/claude/languageDirective.d.ts.map +1 -0
- package/dist/frameworks/claude/languageDirective.js +9 -0
- package/dist/frameworks/claude/languageDirective.js.map +1 -0
- package/dist/frameworks/queue/pQueueAdapter.d.ts +2 -0
- package/dist/frameworks/queue/pQueueAdapter.d.ts.map +1 -1
- package/dist/frameworks/queue/pQueueAdapter.js +1 -1
- package/dist/frameworks/queue/pQueueAdapter.js.map +1 -1
- package/dist/frameworks/settings/runtimeSettings.d.ts +4 -0
- package/dist/frameworks/settings/runtimeSettings.d.ts.map +1 -1
- package/dist/frameworks/settings/runtimeSettings.js +8 -1
- package/dist/frameworks/settings/runtimeSettings.js.map +1 -1
- package/dist/interface-adapters/controllers/http/settings.routes.d.ts.map +1 -1
- package/dist/interface-adapters/controllers/http/settings.routes.js +12 -1
- package/dist/interface-adapters/controllers/http/settings.routes.js.map +1 -1
- package/dist/interface-adapters/controllers/webhook/github.controller.d.ts.map +1 -1
- package/dist/interface-adapters/controllers/webhook/github.controller.js +2 -1
- package/dist/interface-adapters/controllers/webhook/github.controller.js.map +1 -1
- package/dist/interface-adapters/controllers/webhook/gitlab.controller.d.ts.map +1 -1
- package/dist/interface-adapters/controllers/webhook/gitlab.controller.js +2 -1
- package/dist/interface-adapters/controllers/webhook/gitlab.controller.js.map +1 -1
- package/dist/interface-adapters/views/dashboard/index.html +974 -393
- package/dist/interface-adapters/views/dashboard/modules/assignee.d.ts +7 -0
- package/dist/interface-adapters/views/dashboard/modules/assignee.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/assignee.js +47 -0
- package/dist/interface-adapters/views/dashboard/modules/assignee.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/constants.d.ts +7 -0
- package/dist/interface-adapters/views/dashboard/modules/constants.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/constants.js +6 -0
- package/dist/interface-adapters/views/dashboard/modules/constants.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.d.ts +23 -0
- package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.js +37 -0
- package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/formatting.d.ts +23 -0
- package/dist/interface-adapters/views/dashboard/modules/formatting.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/formatting.js +57 -0
- package/dist/interface-adapters/views/dashboard/modules/formatting.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/html.d.ts +16 -0
- package/dist/interface-adapters/views/dashboard/modules/html.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/html.js +68 -0
- package/dist/interface-adapters/views/dashboard/modules/html.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/i18n.d.ts +11 -0
- package/dist/interface-adapters/views/dashboard/modules/i18n.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/i18n.js +500 -0
- package/dist/interface-adapters/views/dashboard/modules/i18n.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/icons.d.ts +13 -0
- package/dist/interface-adapters/views/dashboard/modules/icons.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/icons.js +29 -0
- package/dist/interface-adapters/views/dashboard/modules/icons.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/loading.d.ts +30 -0
- package/dist/interface-adapters/views/dashboard/modules/loading.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/loading.js +48 -0
- package/dist/interface-adapters/views/dashboard/modules/loading.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/notifications.d.ts +39 -0
- package/dist/interface-adapters/views/dashboard/modules/notifications.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/notifications.js +131 -0
- package/dist/interface-adapters/views/dashboard/modules/notifications.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/priority.d.ts +10 -0
- package/dist/interface-adapters/views/dashboard/modules/priority.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/priority.js +86 -0
- package/dist/interface-adapters/views/dashboard/modules/priority.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/quality.d.ts +30 -0
- package/dist/interface-adapters/views/dashboard/modules/quality.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/quality.js +83 -0
- package/dist/interface-adapters/views/dashboard/modules/quality.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/queueLanes.d.ts +22 -0
- package/dist/interface-adapters/views/dashboard/modules/queueLanes.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/queueLanes.js +27 -0
- package/dist/interface-adapters/views/dashboard/modules/queueLanes.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.d.ts +54 -0
- package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.d.ts.map +1 -0
- package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.js +120 -0
- package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.js.map +1 -0
- package/dist/interface-adapters/views/dashboard/styles.css +1031 -93
- package/dist/main/cli.d.ts +41 -1
- package/dist/main/cli.d.ts.map +1 -1
- package/dist/main/cli.js +228 -88
- package/dist/main/cli.js.map +1 -1
- package/dist/tests/factories/reviewJob.factory.d.ts.map +1 -1
- package/dist/tests/factories/reviewJob.factory.js +1 -0
- package/dist/tests/factories/reviewJob.factory.js.map +1 -1
- package/dist/tests/units/cli/parseCliArgs.test.js +14 -0
- package/dist/tests/units/cli/parseCliArgs.test.js.map +1 -1
- package/dist/tests/units/config/projectConfig.test.d.ts +2 -0
- package/dist/tests/units/config/projectConfig.test.d.ts.map +1 -0
- package/dist/tests/units/config/projectConfig.test.js +69 -0
- package/dist/tests/units/config/projectConfig.test.js.map +1 -0
- package/dist/tests/units/entities/language/language.schema.test.d.ts +2 -0
- package/dist/tests/units/entities/language/language.schema.test.d.ts.map +1 -0
- package/dist/tests/units/entities/language/language.schema.test.js +17 -0
- package/dist/tests/units/entities/language/language.schema.test.js.map +1 -0
- package/dist/tests/units/entities/mcpSettings/mcpSettings.guard.test.d.ts +2 -0
- package/dist/tests/units/entities/mcpSettings/mcpSettings.guard.test.d.ts.map +1 -0
- package/dist/tests/units/entities/mcpSettings/mcpSettings.guard.test.js +52 -0
- package/dist/tests/units/entities/mcpSettings/mcpSettings.guard.test.js.map +1 -0
- package/dist/tests/units/frameworks/claude/languageDirective.test.d.ts +2 -0
- package/dist/tests/units/frameworks/claude/languageDirective.test.d.ts.map +1 -0
- package/dist/tests/units/frameworks/claude/languageDirective.test.js +13 -0
- package/dist/tests/units/frameworks/claude/languageDirective.test.js.map +1 -0
- package/dist/tests/units/frameworks/settings/runtimeSettings.test.d.ts +2 -0
- package/dist/tests/units/frameworks/settings/runtimeSettings.test.d.ts.map +1 -0
- package/dist/tests/units/frameworks/settings/runtimeSettings.test.js +20 -0
- package/dist/tests/units/frameworks/settings/runtimeSettings.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js +1 -0
- package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js.map +1 -1
- package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.js +35 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.js +17 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.js +54 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.js +95 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.js +55 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.js +98 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.js +28 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.js +51 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.js +43 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.js +78 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.js +87 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.js +29 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.d.ts +2 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.d.ts.map +1 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.js +60 -0
- package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.js.map +1 -0
- package/dist/tests/units/main/executeDiscover.test.d.ts +2 -0
- package/dist/tests/units/main/executeDiscover.test.d.ts.map +1 -0
- package/dist/tests/units/main/executeDiscover.test.js +106 -0
- package/dist/tests/units/main/executeDiscover.test.js.map +1 -0
- package/dist/tests/units/main/executeInit.test.d.ts +2 -0
- package/dist/tests/units/main/executeInit.test.d.ts.map +1 -0
- package/dist/tests/units/main/executeInit.test.js +290 -0
- package/dist/tests/units/main/executeInit.test.js.map +1 -0
- package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.d.ts +2 -0
- package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.d.ts.map +1 -0
- package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.js +127 -0
- package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.js.map +1 -0
- package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.d.ts +2 -0
- package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.d.ts.map +1 -0
- package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.js +57 -0
- package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.js.map +1 -0
- package/dist/tests/units/usecases/cli/configureMcp.usecase.test.js +67 -9
- package/dist/tests/units/usecases/cli/configureMcp.usecase.test.js.map +1 -1
- package/dist/usecases/cli/addRepositoriesToConfig.usecase.d.ts +27 -0
- package/dist/usecases/cli/addRepositoriesToConfig.usecase.d.ts.map +1 -0
- package/dist/usecases/cli/addRepositoriesToConfig.usecase.js +34 -0
- package/dist/usecases/cli/addRepositoriesToConfig.usecase.js.map +1 -0
- package/dist/usecases/cli/checkInitPrerequisites.d.ts +16 -0
- package/dist/usecases/cli/checkInitPrerequisites.d.ts.map +1 -0
- package/dist/usecases/cli/checkInitPrerequisites.js +23 -0
- package/dist/usecases/cli/checkInitPrerequisites.js.map +1 -0
- package/dist/usecases/cli/configureMcp.usecase.d.ts +2 -2
- package/dist/usecases/cli/configureMcp.usecase.d.ts.map +1 -1
- package/dist/usecases/cli/configureMcp.usecase.js +16 -3
- package/dist/usecases/cli/configureMcp.usecase.js.map +1 -1
- package/dist/usecases/cli/validateConfig.usecase.d.ts +1 -1
- package/dist/usecases/cli/validateConfig.usecase.d.ts.map +1 -1
- package/dist/usecases/triggerReview.usecase.d.ts +2 -0
- package/dist/usecases/triggerReview.usecase.d.ts.map +1 -1
- package/dist/usecases/triggerReview.usecase.js +1 -0
- package/dist/usecases/triggerReview.usecase.js.map +1 -1
- package/package.json +1 -1
- package/templates/SETUP.md +23 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
@@ -14,67 +14,105 @@
|
|
|
14
14
|
<h1>Reviewflow</h1>
|
|
15
15
|
<div class="header-actions">
|
|
16
16
|
<button id="check-claude-btn" class="btn btn-primary" onclick="checkClaudeStatus()">
|
|
17
|
-
<i data-lucide="search"></i>
|
|
17
|
+
<i data-lucide="search"></i> <span id="i18n-check-claude"></span>
|
|
18
18
|
</button>
|
|
19
19
|
<button id="toggle-logs-btn" class="btn btn-secondary" onclick="toggleLogs()">
|
|
20
|
-
<i data-lucide="scroll-text"></i>
|
|
20
|
+
<i data-lucide="scroll-text"></i> <span id="i18n-logs-btn"></span>
|
|
21
21
|
</button>
|
|
22
22
|
<div id="server-status" class="status-indicator connecting">
|
|
23
23
|
<span class="status-dot"></span>
|
|
24
|
-
<span
|
|
24
|
+
<span id="i18n-server-status"></span>
|
|
25
25
|
</div>
|
|
26
26
|
</div>
|
|
27
27
|
</header>
|
|
28
28
|
|
|
29
29
|
<div class="cards">
|
|
30
|
-
<div class="card">
|
|
31
|
-
<div class="card-label"
|
|
30
|
+
<div class="card card-priority">
|
|
31
|
+
<div class="card-label" id="i18n-card-running"></div>
|
|
32
32
|
<div id="running-count" class="card-value running">-</div>
|
|
33
33
|
</div>
|
|
34
|
-
<div class="card">
|
|
35
|
-
<div class="card-label"
|
|
34
|
+
<div class="card card-priority">
|
|
35
|
+
<div class="card-label" id="i18n-card-queued"></div>
|
|
36
36
|
<div id="queued-count" class="card-value queued">-</div>
|
|
37
37
|
</div>
|
|
38
38
|
<div class="card">
|
|
39
|
-
<div class="card-label"
|
|
39
|
+
<div class="card-label" id="i18n-card-completed"></div>
|
|
40
40
|
<div id="completed-count" class="card-value">-</div>
|
|
41
41
|
</div>
|
|
42
42
|
<div class="card">
|
|
43
|
-
<div class="card-label"
|
|
43
|
+
<div class="card-label" id="i18n-card-claude-cli"></div>
|
|
44
44
|
<div id="claude-status" class="card-claude checking">
|
|
45
|
-
<span class="status"
|
|
45
|
+
<span class="status" id="i18n-claude-checking"></span>
|
|
46
46
|
<span class="version"></span>
|
|
47
47
|
</div>
|
|
48
48
|
</div>
|
|
49
49
|
<div class="card" id="git-cli-card">
|
|
50
|
-
<div class="card-label" id="git-cli-label"
|
|
50
|
+
<div class="card-label" id="git-cli-label"></div>
|
|
51
51
|
<div id="git-cli-status" class="card-claude checking">
|
|
52
|
-
<span class="status"
|
|
52
|
+
<span class="status" id="i18n-git-load-project"></span>
|
|
53
53
|
<span class="version"></span>
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
56
|
<div class="card">
|
|
57
|
-
<div class="card-label"
|
|
57
|
+
<div class="card-label" id="i18n-card-model"></div>
|
|
58
58
|
<div class="card-model">
|
|
59
59
|
<select id="model-select" class="model-select" onchange="changeModel(this.value)">
|
|
60
|
-
<option value="opus"
|
|
61
|
-
<option value="sonnet"
|
|
60
|
+
<option value="opus" id="i18n-model-opus"></option>
|
|
61
|
+
<option value="sonnet" id="i18n-model-sonnet"></option>
|
|
62
|
+
</select>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="card">
|
|
66
|
+
<div class="card-label" id="i18n-card-language"></div>
|
|
67
|
+
<div class="card-model">
|
|
68
|
+
<select id="language-select" class="model-select" onchange="changeLanguage(this.value)">
|
|
69
|
+
<option value="en">English</option>
|
|
70
|
+
<option value="fr">Français</option>
|
|
62
71
|
</select>
|
|
63
72
|
</div>
|
|
64
73
|
</div>
|
|
65
74
|
</div>
|
|
66
75
|
|
|
76
|
+
<div class="focus-strip">
|
|
77
|
+
<div class="focus-chip focus-now">
|
|
78
|
+
<div class="focus-copy">
|
|
79
|
+
<span class="focus-label" id="i18n-strip-now"></span>
|
|
80
|
+
<span class="focus-meta" id="i18n-strip-now-meta"></span>
|
|
81
|
+
</div>
|
|
82
|
+
<span class="focus-value" id="focus-now-count">-</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="focus-chip focus-next">
|
|
85
|
+
<div class="focus-copy">
|
|
86
|
+
<span class="focus-label" id="i18n-strip-next"></span>
|
|
87
|
+
<span class="focus-meta" id="i18n-strip-next-meta"></span>
|
|
88
|
+
</div>
|
|
89
|
+
<span class="focus-value" id="focus-next-count">-</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="focus-chip focus-blocked">
|
|
92
|
+
<div class="focus-copy">
|
|
93
|
+
<span class="focus-label" id="i18n-strip-blocked"></span>
|
|
94
|
+
<span class="focus-meta" id="i18n-strip-blocked-meta"></span>
|
|
95
|
+
</div>
|
|
96
|
+
<span class="focus-value" id="focus-blocked-count">-</span>
|
|
97
|
+
</div>
|
|
98
|
+
<button id="focus-strip-toggle" class="focus-toggle-btn" onclick="toggleFocusStripMode()"></button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div id="data-loading-state" class="data-loading hidden" role="status" aria-live="polite">
|
|
102
|
+
<i data-lucide="loader-circle"></i>
|
|
103
|
+
<span id="i18n-loading-data"></span>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
67
106
|
<div class="project-loader">
|
|
68
107
|
<select id="project-select" class="project-input" style="min-width: 350px;" onchange="onProjectSelect(this.value)">
|
|
69
|
-
<option value=""
|
|
108
|
+
<option value="" id="i18n-project-placeholder"></option>
|
|
70
109
|
</select>
|
|
71
110
|
<input type="text" id="project-path-input" class="project-input" style="min-width: 250px;"
|
|
72
|
-
placeholder="Ou entrer un nouveau chemin..."
|
|
73
111
|
value="">
|
|
74
112
|
<button class="btn btn-primary" onclick="loadProjectConfig()">
|
|
75
|
-
<i data-lucide="folder-open"></i>
|
|
113
|
+
<i data-lucide="folder-open"></i> <span id="i18n-project-load"></span>
|
|
76
114
|
</button>
|
|
77
|
-
<button class="btn btn-secondary" onclick="removeCurrentProject()"
|
|
115
|
+
<button id="remove-project-btn" class="btn btn-secondary" onclick="removeCurrentProject()">
|
|
78
116
|
<i data-lucide="trash-2"></i>
|
|
79
117
|
</button>
|
|
80
118
|
<span id="config-status" class="config-status hidden"></span>
|
|
@@ -82,108 +120,127 @@
|
|
|
82
120
|
<div id="config-info" class="config-info hidden"></div>
|
|
83
121
|
|
|
84
122
|
<div id="claude-login-section" class="login-instructions hidden">
|
|
85
|
-
<strong><i data-lucide="alert-triangle"></i>
|
|
86
|
-
<p style="margin-top: 0.5rem;"
|
|
123
|
+
<strong><i data-lucide="alert-triangle"></i> <span id="i18n-claude-login-title"></span></strong>
|
|
124
|
+
<p style="margin-top: 0.5rem;" id="i18n-claude-login-instruction"></p>
|
|
87
125
|
<p style="margin-top: 0.5rem;"><code>claude login</code></p>
|
|
88
|
-
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: #a1a1aa;"
|
|
126
|
+
<p style="margin-top: 0.5rem; font-size: 0.875rem; color: #a1a1aa;" id="i18n-claude-login-reload"></p>
|
|
89
127
|
</div>
|
|
90
128
|
|
|
91
129
|
<div id="git-login-section" class="login-instructions hidden">
|
|
92
|
-
<strong id="git-login-title"><i data-lucide="alert-triangle"></i>
|
|
130
|
+
<strong id="git-login-title"><i data-lucide="alert-triangle"></i> <span id="i18n-git-login-title"></span></strong>
|
|
93
131
|
<div style="margin-top: 0.75rem;" id="git-login-instructions"></div>
|
|
94
132
|
</div>
|
|
95
133
|
|
|
96
134
|
<div id="logs-section" class="section hidden">
|
|
97
135
|
<div class="section-header">
|
|
98
|
-
<i data-lucide="scroll-text"></i>
|
|
99
|
-
<span id="error-count" class="badge-count hidden"
|
|
136
|
+
<i data-lucide="scroll-text"></i> <span id="i18n-section-logs"></span>
|
|
137
|
+
<span id="error-count" class="badge-count hidden"></span>
|
|
100
138
|
</div>
|
|
101
139
|
<div id="logs-content" class="section-content logs">
|
|
102
|
-
<div class="empty-state"
|
|
140
|
+
<div class="empty-state" id="i18n-empty-logs"></div>
|
|
103
141
|
</div>
|
|
104
142
|
</div>
|
|
105
143
|
|
|
106
144
|
<div id="stats-section" class="section hidden">
|
|
107
|
-
<div class="section-header clickable" onclick="toggleStats()">
|
|
108
|
-
<i data-lucide="bar-chart-3"></i>
|
|
145
|
+
<div class="section-header clickable" onclick="toggleStats()" role="button" tabindex="0" onkeydown="activateOnKeydown(event)">
|
|
146
|
+
<i data-lucide="bar-chart-3"></i> <span id="i18n-section-stats"></span>
|
|
109
147
|
<span id="stats-toggle" class="toggle-icon collapsed"><i data-lucide="chevron-down"></i></span>
|
|
110
148
|
</div>
|
|
111
149
|
<div id="project-stats" class="section-content stats-grid hidden">
|
|
112
|
-
<div class="empty-state"
|
|
150
|
+
<div class="empty-state" id="i18n-empty-stats"></div>
|
|
113
151
|
</div>
|
|
114
152
|
</div>
|
|
115
153
|
|
|
116
154
|
<div class="section" id="active-reviews-section">
|
|
117
155
|
<div class="section-header">
|
|
118
|
-
<i data-lucide="file-search"></i>
|
|
156
|
+
<i data-lucide="file-search"></i> <span id="i18n-section-active-reviews"></span>
|
|
119
157
|
<span id="active-reviews-count" class="badge-count hidden">0</span>
|
|
120
158
|
</div>
|
|
121
159
|
<div id="active-reviews" class="section-content">
|
|
122
|
-
<div class="empty-state"
|
|
160
|
+
<div class="empty-state" id="i18n-empty-active-reviews"></div>
|
|
123
161
|
</div>
|
|
124
162
|
</div>
|
|
125
163
|
|
|
126
164
|
<div class="section hidden" id="active-followups-section">
|
|
127
|
-
<div class="section-header">
|
|
128
|
-
<i data-lucide="refresh-cw"></i>
|
|
165
|
+
<div class="section-header clickable" onclick="toggleSection('active-followups-section')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)">
|
|
166
|
+
<i data-lucide="refresh-cw"></i> <span id="i18n-section-active-followups"></span>
|
|
129
167
|
<span id="active-followups-count" class="badge-count hidden">0</span>
|
|
168
|
+
<span class="section-toggle collapsed"><i data-lucide="chevron-down"></i></span>
|
|
130
169
|
</div>
|
|
131
170
|
<div id="active-followups" class="section-content">
|
|
132
|
-
<div class="empty-state"
|
|
171
|
+
<div class="empty-state" id="i18n-empty-active-followups"></div>
|
|
133
172
|
</div>
|
|
134
173
|
</div>
|
|
135
174
|
|
|
136
175
|
<div class="section hidden" id="pending-fix-section">
|
|
137
176
|
<div class="section-header">
|
|
138
|
-
<i data-lucide="wrench"></i>
|
|
177
|
+
<i data-lucide="wrench"></i> <span id="i18n-section-pending-fix"></span>
|
|
139
178
|
<span id="pending-fix-count" class="badge-count hidden">0</span>
|
|
140
|
-
<button id="sync-threads-btn" class="btn-icon btn-sync"
|
|
179
|
+
<button id="sync-threads-btn" class="btn-icon btn-sync" onclick="syncGitLabThreads()">
|
|
141
180
|
<i data-lucide="refresh-cw"></i>
|
|
142
181
|
</button>
|
|
143
182
|
</div>
|
|
144
183
|
<div id="pending-fix-reviews" class="section-content">
|
|
145
|
-
<div class="empty-state"
|
|
184
|
+
<div class="empty-state" id="i18n-empty-pending-fix"></div>
|
|
146
185
|
</div>
|
|
147
186
|
</div>
|
|
148
187
|
|
|
149
188
|
<div class="section hidden" id="pending-approval-section">
|
|
150
|
-
<div class="section-header">
|
|
151
|
-
<i data-lucide="circle-check"></i>
|
|
189
|
+
<div class="section-header clickable" onclick="toggleSection('pending-approval-section')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)">
|
|
190
|
+
<i data-lucide="circle-check"></i> <span id="i18n-section-pending-approval"></span>
|
|
152
191
|
<span id="pending-approval-count" class="badge-count hidden">0</span>
|
|
192
|
+
<span class="section-toggle collapsed"><i data-lucide="chevron-down"></i></span>
|
|
153
193
|
</div>
|
|
154
194
|
<div id="pending-approval-reviews" class="section-content">
|
|
155
|
-
<div class="empty-state"
|
|
195
|
+
<div class="empty-state" id="i18n-empty-pending-approval"></div>
|
|
156
196
|
</div>
|
|
157
197
|
</div>
|
|
158
198
|
|
|
159
|
-
<div class="section">
|
|
160
|
-
<div class="section-header">
|
|
161
|
-
<i data-lucide="file-check"></i>
|
|
199
|
+
<div class="section" id="completed-reviews-section">
|
|
200
|
+
<div class="section-header clickable" onclick="toggleSection('completed-reviews-section')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)">
|
|
201
|
+
<i data-lucide="file-check"></i> <span id="i18n-section-completed-reviews"></span>
|
|
202
|
+
<span class="section-toggle collapsed"><i data-lucide="chevron-down"></i></span>
|
|
162
203
|
</div>
|
|
163
204
|
<div id="recent-reviews" class="section-content">
|
|
164
|
-
<div class="empty-state"
|
|
205
|
+
<div class="empty-state" id="i18n-empty-loading"></div>
|
|
165
206
|
</div>
|
|
166
207
|
</div>
|
|
167
208
|
|
|
168
209
|
<div class="refresh-info">
|
|
169
|
-
<span id="connection-mode"
|
|
210
|
+
<span id="connection-mode"></span> • <span id="i18n-connection-fallback"></span>
|
|
211
|
+
<span class="refresh-separator"> • </span>
|
|
212
|
+
<span id="session-metrics"></span>
|
|
170
213
|
</div>
|
|
171
214
|
</div>
|
|
172
215
|
|
|
173
216
|
<div id="cancel-modal" class="modal-overlay hidden" onclick="closeCancelModal(event)">
|
|
174
217
|
<div class="modal-content" onclick="event.stopPropagation()">
|
|
175
218
|
<div class="modal-title" id="cancel-modal-title"></div>
|
|
176
|
-
<div class="modal-message"
|
|
219
|
+
<div class="modal-message" id="i18n-modal-message"></div>
|
|
177
220
|
<div class="modal-actions">
|
|
178
|
-
<button class="btn-modal-back" onclick="closeCancelModal()"
|
|
179
|
-
<button class="btn-modal-confirm" id="cancel-modal-confirm" onclick="confirmCancelReview()"
|
|
221
|
+
<button class="btn-modal-back" onclick="closeCancelModal()" id="i18n-modal-back"></button>
|
|
222
|
+
<button class="btn-modal-confirm" id="cancel-modal-confirm" onclick="confirmCancelReview()"></button>
|
|
180
223
|
</div>
|
|
181
224
|
</div>
|
|
182
225
|
</div>
|
|
183
226
|
|
|
184
227
|
<div id="toast-container" class="toast-container"></div>
|
|
185
228
|
|
|
186
|
-
<script>
|
|
229
|
+
<script type="module">
|
|
230
|
+
import { t, setLanguage, getLanguage } from './modules/i18n.js';
|
|
231
|
+
import { formatTime, formatDuration, formatPhase, formatLogTime } from './modules/formatting.js';
|
|
232
|
+
import { escapeHtml, markdownToHtml, sanitizeHttpUrl } from './modules/html.js';
|
|
233
|
+
import { getAgentIcon, icon, refreshIcons } from './modules/icons.js';
|
|
234
|
+
import { MAX_RECONNECT_ATTEMPTS, RECONNECT_DELAY, STORAGE_KEY_PROJECTS, STORAGE_KEY_CURRENT, STORAGE_KEY_FOCUS_STRIP_MODE, QUALITY_TARGET_SCORE } from './modules/constants.js';
|
|
235
|
+
import { getDesktopNotificationPayload, shouldNotifyDesktop } from './modules/desktopNotifications.js';
|
|
236
|
+
import { getLoadingPresentation, getQuietRefreshSectionIdentifiers } from './modules/loading.js';
|
|
237
|
+
import { collectReviewNotifications, createReviewNotificationState } from './modules/notifications.js';
|
|
238
|
+
import { resolveReviewAssigneeDisplay } from './modules/assignee.js';
|
|
239
|
+
import { buildQueueLanesModel } from './modules/queueLanes.js';
|
|
240
|
+
import { rankPendingFixForNowLane } from './modules/priority.js';
|
|
241
|
+
import { getQualityProgress, getQualityTrend } from './modules/quality.js';
|
|
242
|
+
import { createSessionMetricsState, trackSessionAction, updatePriorityItemTracking, getSessionMetricsSnapshot } from './modules/sessionMetrics.js';
|
|
243
|
+
|
|
187
244
|
const API_URL = window.location.origin;
|
|
188
245
|
const WS_URL = `ws://${window.location.host}/ws`;
|
|
189
246
|
|
|
@@ -191,66 +248,280 @@
|
|
|
191
248
|
let wsConnected = false;
|
|
192
249
|
let reconnectAttempts = 0;
|
|
193
250
|
let logsVisible = false;
|
|
194
|
-
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
195
|
-
const RECONNECT_DELAY = 3000;
|
|
196
251
|
|
|
197
252
|
let currentData = { activeReviews: [], recentReviews: [], logs: [], reviewFiles: [], pendingFix: [], pendingApproval: [] };
|
|
198
|
-
let loadedReviews = {};
|
|
199
|
-
let statsCollapsed = true;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
253
|
+
let loadedReviews = {};
|
|
254
|
+
let statsCollapsed = true;
|
|
255
|
+
let focusStripCompact = false;
|
|
256
|
+
const loadingState = { status: 0, reviewFiles: 0, stats: 0, mrTracking: 0 };
|
|
257
|
+
let hasLoadedStatusOnce = false;
|
|
258
|
+
const secondarySections = ['active-followups-section', 'pending-approval-section', 'completed-reviews-section'];
|
|
259
|
+
const sectionExpandedState = Object.fromEntries(secondarySections.map((sectionIdentifier) => [sectionIdentifier, false]));
|
|
260
|
+
const quietRefreshSections = [
|
|
261
|
+
'active-reviews-section',
|
|
262
|
+
'active-followups-section',
|
|
263
|
+
'completed-reviews-section',
|
|
264
|
+
'pending-fix-section',
|
|
265
|
+
];
|
|
266
|
+
const loadingLabelBySource = {
|
|
267
|
+
status: 'loading.status',
|
|
268
|
+
reviewFiles: 'loading.reviewFiles',
|
|
269
|
+
stats: 'loading.stats',
|
|
270
|
+
mrTracking: 'loading.mrTracking',
|
|
271
|
+
};
|
|
272
|
+
let loadingShowTimeout = null;
|
|
273
|
+
let loadingHideTimeout = null;
|
|
274
|
+
let loadingVisibleSince = null;
|
|
275
|
+
const loadingDelayMs = 260;
|
|
276
|
+
const loadingMinimumVisibleMs = 420;
|
|
277
|
+
|
|
278
|
+
let sessionMetrics = createSessionMetricsState(Date.now());
|
|
279
|
+
let reviewNotificationState = createReviewNotificationState();
|
|
280
|
+
|
|
281
|
+
function applyFocusStripMode() {
|
|
282
|
+
const strip = document.querySelector('.focus-strip');
|
|
283
|
+
if (strip) {
|
|
284
|
+
strip.classList.toggle('compact', focusStripCompact);
|
|
285
|
+
}
|
|
286
|
+
document.body.classList.toggle('compact-density', focusStripCompact);
|
|
287
|
+
const toggleBtn = document.getElementById('focus-strip-toggle');
|
|
288
|
+
if (toggleBtn) {
|
|
289
|
+
toggleBtn.textContent = focusStripCompact ? t('strip.modeDetailed') : t('strip.modeCompact');
|
|
290
|
+
toggleBtn.setAttribute('aria-pressed', focusStripCompact ? 'true' : 'false');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function loadFocusStripMode() {
|
|
295
|
+
const mode = localStorage.getItem(STORAGE_KEY_FOCUS_STRIP_MODE);
|
|
296
|
+
focusStripCompact = mode === 'compact';
|
|
297
|
+
applyFocusStripMode();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function toggleFocusStripMode() {
|
|
301
|
+
focusStripCompact = !focusStripCompact;
|
|
302
|
+
localStorage.setItem(STORAGE_KEY_FOCUS_STRIP_MODE, focusStripCompact ? 'compact' : 'detailed');
|
|
303
|
+
applyFocusStripMode();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function applySectionExpansion(sectionIdentifier) {
|
|
307
|
+
const section = document.getElementById(sectionIdentifier);
|
|
308
|
+
if (!section) return;
|
|
309
|
+
const content = section.querySelector('.section-content');
|
|
310
|
+
const toggle = section.querySelector('.section-toggle');
|
|
311
|
+
if (!content) return;
|
|
312
|
+
|
|
313
|
+
const expanded = sectionExpandedState[sectionIdentifier] === true;
|
|
314
|
+
content.classList.toggle('hidden', !expanded);
|
|
315
|
+
section.classList.toggle('section-collapsed', !expanded);
|
|
316
|
+
if (toggle) {
|
|
317
|
+
toggle.classList.toggle('collapsed', !expanded);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function toggleSection(sectionIdentifier) {
|
|
322
|
+
if (!(sectionIdentifier in sectionExpandedState)) return;
|
|
323
|
+
sectionExpandedState[sectionIdentifier] = !sectionExpandedState[sectionIdentifier];
|
|
324
|
+
applySectionExpansion(sectionIdentifier);
|
|
325
|
+
refreshIcons();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function setLoadingFlag(source, isLoading) {
|
|
329
|
+
if (!(source in loadingState)) return;
|
|
330
|
+
const nextValue = isLoading
|
|
331
|
+
? loadingState[source] + 1
|
|
332
|
+
: Math.max(0, loadingState[source] - 1);
|
|
333
|
+
loadingState[source] = nextValue;
|
|
334
|
+
updateLoadingStateUI();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function applyQuietRefreshSectionIndicators(sectionIdentifiers) {
|
|
338
|
+
quietRefreshSections.forEach((sectionIdentifier) => {
|
|
339
|
+
const section = document.getElementById(sectionIdentifier);
|
|
340
|
+
if (!section) return;
|
|
341
|
+
section.classList.toggle('is-refreshing', sectionIdentifiers.includes(sectionIdentifier));
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getLoadingMessageKey() {
|
|
346
|
+
const activeSource = Object.entries(loadingState).find(([, value]) => value > 0)?.[0];
|
|
347
|
+
return activeSource ? loadingLabelBySource[activeSource] : 'loading.data';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function updateLoadingStateUI() {
|
|
351
|
+
const isLoading = Object.values(loadingState).some((value) => value > 0);
|
|
352
|
+
const loadingPresentation = getLoadingPresentation(loadingState, { hasLoadedStatusOnce });
|
|
353
|
+
const loadingElement = document.getElementById('data-loading-state');
|
|
354
|
+
if (!loadingElement) return;
|
|
355
|
+
|
|
356
|
+
const loadingLabel = document.getElementById('i18n-loading-data');
|
|
357
|
+
if (loadingLabel) {
|
|
358
|
+
loadingLabel.textContent = t(getLoadingMessageKey());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (isLoading) {
|
|
362
|
+
document.body.classList.toggle('is-quiet-refresh', loadingPresentation.isQuietRefresh);
|
|
363
|
+
const quietSectionIdentifiers = getQuietRefreshSectionIdentifiers(loadingState, { hasLoadedStatusOnce });
|
|
364
|
+
applyQuietRefreshSectionIndicators(quietSectionIdentifiers);
|
|
365
|
+
if (!loadingPresentation.showGlobalLoading) {
|
|
366
|
+
if (loadingShowTimeout) {
|
|
367
|
+
clearTimeout(loadingShowTimeout);
|
|
368
|
+
loadingShowTimeout = null;
|
|
369
|
+
}
|
|
370
|
+
if (loadingHideTimeout) {
|
|
371
|
+
clearTimeout(loadingHideTimeout);
|
|
372
|
+
loadingHideTimeout = null;
|
|
373
|
+
}
|
|
374
|
+
loadingVisibleSince = null;
|
|
375
|
+
loadingElement.classList.add('hidden');
|
|
376
|
+
document.body.classList.remove('is-loading-data');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (loadingHideTimeout) {
|
|
381
|
+
clearTimeout(loadingHideTimeout);
|
|
382
|
+
loadingHideTimeout = null;
|
|
383
|
+
}
|
|
384
|
+
if (!loadingVisibleSince && !loadingShowTimeout) {
|
|
385
|
+
loadingShowTimeout = setTimeout(() => {
|
|
386
|
+
loadingElement.classList.remove('hidden');
|
|
387
|
+
document.body.classList.add('is-loading-data');
|
|
388
|
+
loadingVisibleSince = Date.now();
|
|
389
|
+
loadingShowTimeout = null;
|
|
390
|
+
refreshIcons();
|
|
391
|
+
}, loadingDelayMs);
|
|
392
|
+
}
|
|
224
393
|
} else {
|
|
225
|
-
|
|
394
|
+
document.body.classList.remove('is-quiet-refresh');
|
|
395
|
+
applyQuietRefreshSectionIndicators([]);
|
|
396
|
+
if (loadingShowTimeout) {
|
|
397
|
+
clearTimeout(loadingShowTimeout);
|
|
398
|
+
loadingShowTimeout = null;
|
|
399
|
+
}
|
|
400
|
+
if (!loadingVisibleSince) {
|
|
401
|
+
loadingElement.classList.add('hidden');
|
|
402
|
+
document.body.classList.remove('is-loading-data');
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const elapsed = Date.now() - loadingVisibleSince;
|
|
406
|
+
const remaining = Math.max(0, loadingMinimumVisibleMs - elapsed);
|
|
407
|
+
loadingHideTimeout = setTimeout(() => {
|
|
408
|
+
loadingElement.classList.add('hidden');
|
|
409
|
+
document.body.classList.remove('is-loading-data');
|
|
410
|
+
loadingVisibleSince = null;
|
|
411
|
+
loadingHideTimeout = null;
|
|
412
|
+
updateUI();
|
|
413
|
+
}, remaining);
|
|
226
414
|
}
|
|
227
|
-
if (diff < 60) return `${diff}s`;
|
|
228
|
-
if (diff < 3600) return `${Math.floor(diff / 60)}m ${diff % 60}s`;
|
|
229
|
-
return `${Math.floor(diff / 3600)}h ${Math.floor((diff % 3600) / 60)}m`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function formatPhase(phase) {
|
|
233
|
-
const labels = {
|
|
234
|
-
'initializing': 'Initialisation',
|
|
235
|
-
'agents-running': 'Agents en cours',
|
|
236
|
-
'synthesizing': 'Synthèse',
|
|
237
|
-
'publishing': 'Publication',
|
|
238
|
-
'completed': 'Terminé'
|
|
239
|
-
};
|
|
240
|
-
return labels[phase] || phase;
|
|
241
415
|
}
|
|
242
416
|
|
|
243
|
-
function
|
|
244
|
-
const
|
|
245
|
-
|
|
417
|
+
function formatSessionDuration(milliseconds) {
|
|
418
|
+
const seconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
419
|
+
if (seconds < 60) return `${seconds}s`;
|
|
420
|
+
const minutes = Math.floor(seconds / 60);
|
|
421
|
+
const remainingSeconds = seconds % 60;
|
|
422
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function formatActionBreakdown(actionBreakdown) {
|
|
426
|
+
return `${t('metrics.action.followup')} ${actionBreakdown.followup}, ${t('metrics.action.open')} ${actionBreakdown.open}, ${t('metrics.action.approve')} ${actionBreakdown.approve}, ${t('metrics.action.cancelReview')} ${actionBreakdown.cancelReview}, ${t('metrics.action.syncThreads')} ${actionBreakdown.syncThreads}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function updateSessionMetricsUI() {
|
|
430
|
+
const metricsElement = document.getElementById('session-metrics');
|
|
431
|
+
if (!metricsElement) return;
|
|
432
|
+
|
|
433
|
+
const metricsSnapshot = getSessionMetricsSnapshot(sessionMetrics);
|
|
434
|
+
const firstActionLabel = metricsSnapshot.firstUsefulActionDelayMs === null
|
|
435
|
+
? t('metrics.pending')
|
|
436
|
+
: formatSessionDuration(metricsSnapshot.firstUsefulActionDelayMs);
|
|
437
|
+
const priorityResolutionLabel = metricsSnapshot.averagePriorityResolutionMs === null
|
|
438
|
+
? t('metrics.pending')
|
|
439
|
+
: formatSessionDuration(metricsSnapshot.averagePriorityResolutionMs);
|
|
440
|
+
const actionBreakdownLabel = formatActionBreakdown(metricsSnapshot.actionBreakdown);
|
|
441
|
+
metricsElement.textContent = `${t('metrics.session')}: ${t('metrics.firstAction')} ${firstActionLabel} • ${metricsSnapshot.actionCount} ${t('metrics.actions')} • ${t('metrics.priorityResolution')} ${priorityResolutionLabel} • ${t('metrics.breakdown')} ${actionBreakdownLabel}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function trackUsefulAction(actionType = 'other') {
|
|
445
|
+
sessionMetrics = trackSessionAction(sessionMetrics, actionType, Date.now());
|
|
446
|
+
updateSessionMetricsUI();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function onUsefulLinkAction() {
|
|
450
|
+
trackUsefulAction('open');
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function activateOnKeydown(event) {
|
|
455
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
456
|
+
event.preventDefault();
|
|
457
|
+
event.currentTarget.click();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function safeDecodeURIComponent(value) {
|
|
461
|
+
try {
|
|
462
|
+
return decodeURIComponent(String(value ?? ''));
|
|
463
|
+
} catch {
|
|
464
|
+
return String(value ?? '');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getNotificationMrNumber(review) {
|
|
469
|
+
return typeof review.mrNumber === 'number' ? String(review.mrNumber) : '?';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function mapNotificationToToast(notification) {
|
|
473
|
+
const mrNumber = getNotificationMrNumber(notification.review);
|
|
474
|
+
switch (notification.kind) {
|
|
475
|
+
case 'reviewStarted':
|
|
476
|
+
return { message: t('notify.reviewStarted', { mrNumber }), type: 'info' };
|
|
477
|
+
case 'followupStarted':
|
|
478
|
+
return { message: t('notify.followupStarted', { mrNumber }), type: 'info' };
|
|
479
|
+
case 'reviewCompleted':
|
|
480
|
+
return { message: t('notify.reviewCompleted', { mrNumber }), type: 'success' };
|
|
481
|
+
case 'followupCompleted':
|
|
482
|
+
return { message: t('notify.followupCompleted', { mrNumber }), type: 'success' };
|
|
483
|
+
case 'reviewFailed':
|
|
484
|
+
return { message: t('notify.reviewFailed', { mrNumber }), type: 'error' };
|
|
485
|
+
default:
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function maybeShowDesktopNotification(notification) {
|
|
491
|
+
if (typeof Notification === 'undefined') return;
|
|
492
|
+
|
|
493
|
+
const shouldNotify = shouldNotifyDesktop({
|
|
494
|
+
permission: Notification.permission,
|
|
495
|
+
isDocumentHidden: document.visibilityState !== 'visible',
|
|
496
|
+
});
|
|
497
|
+
if (!shouldNotify) return;
|
|
498
|
+
|
|
499
|
+
const desktopPayload = getDesktopNotificationPayload(notification, t);
|
|
500
|
+
if (!desktopPayload) return;
|
|
501
|
+
new Notification(desktopPayload.title, {
|
|
502
|
+
body: desktopPayload.body,
|
|
503
|
+
tag: desktopPayload.tag,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function dispatchReviewNotifications(activeReviews, recentReviews) {
|
|
508
|
+
const result = collectReviewNotifications(reviewNotificationState, activeReviews, recentReviews);
|
|
509
|
+
reviewNotificationState = result.nextState;
|
|
510
|
+
result.notifications.forEach((notification) => {
|
|
511
|
+
const toastPayload = mapNotificationToToast(notification);
|
|
512
|
+
if (!toastPayload) return;
|
|
513
|
+
showToast(toastPayload.message, toastPayload.type);
|
|
514
|
+
maybeShowDesktopNotification(notification);
|
|
515
|
+
});
|
|
246
516
|
}
|
|
247
517
|
|
|
248
518
|
function renderAgentTimeline(progress) {
|
|
249
519
|
if (!progress?.agents?.length) return '';
|
|
520
|
+
const allowedAgentStatuses = new Set(['running', 'completed', 'failed', 'pending', 'queued']);
|
|
250
521
|
const agentsHtml = progress.agents.map(agent => `
|
|
251
|
-
<div class="agent-box ${agent.status}" title="${agent.displayName}: ${agent.status}">
|
|
522
|
+
<div class="agent-box ${allowedAgentStatuses.has(agent.status) ? agent.status : 'queued'}" title="${escapeHtml(agent.displayName)}: ${escapeHtml(agent.status)}">
|
|
252
523
|
<span class="agent-icon">${getAgentIcon(agent.status)}</span>
|
|
253
|
-
<span class="agent-name">${agent.displayName}</span>
|
|
524
|
+
<span class="agent-name">${escapeHtml(agent.displayName)}</span>
|
|
254
525
|
</div>
|
|
255
526
|
`).join('');
|
|
256
527
|
return `<div class="agent-timeline">${agentsHtml}</div>`;
|
|
@@ -258,72 +529,89 @@
|
|
|
258
529
|
|
|
259
530
|
function renderProgressBar(progress) {
|
|
260
531
|
if (!progress) return '';
|
|
532
|
+
const overallProgress = typeof progress.overallProgress === 'number'
|
|
533
|
+
? Math.max(0, Math.min(100, progress.overallProgress))
|
|
534
|
+
: 0;
|
|
261
535
|
return `
|
|
262
536
|
<div class="progress-container">
|
|
263
537
|
<div class="progress-bar-wrapper">
|
|
264
538
|
<div class="progress-bar">
|
|
265
|
-
<div class="progress-bar-fill" style="width: ${
|
|
539
|
+
<div class="progress-bar-fill" style="width: ${overallProgress}%"></div>
|
|
266
540
|
</div>
|
|
267
|
-
<span class="progress-percent">${
|
|
541
|
+
<span class="progress-percent">${overallProgress}%</span>
|
|
268
542
|
</div>
|
|
269
543
|
<div class="progress-phase">${formatPhase(progress.currentPhase)}</div>
|
|
270
544
|
</div>
|
|
271
545
|
`;
|
|
272
546
|
}
|
|
273
547
|
|
|
548
|
+
function getReviewStatusPresentation(status) {
|
|
549
|
+
const statusMap = {
|
|
550
|
+
running: { icon: 'activity', label: t('review.status.running') },
|
|
551
|
+
queued: { icon: 'clock-3', label: t('review.status.queued') },
|
|
552
|
+
completed: { icon: 'check-circle-2', label: t('review.status.completed') },
|
|
553
|
+
failed: { icon: 'triangle-alert', label: t('review.status.failed') },
|
|
554
|
+
};
|
|
555
|
+
return statusMap[status] || { icon: 'circle-help', label: status };
|
|
556
|
+
}
|
|
557
|
+
|
|
274
558
|
function renderReview(review, isActive) {
|
|
275
|
-
const statusClass = review.status;
|
|
559
|
+
const statusClass = typeof review.status === 'string' ? review.status : 'queued';
|
|
560
|
+
const safeStatusClass = ['running', 'queued', 'completed', 'failed'].includes(statusClass) ? statusClass : 'queued';
|
|
561
|
+
const statusPresentation = getReviewStatusPresentation(statusClass);
|
|
276
562
|
const mrNumber = review.mrNumber;
|
|
277
|
-
const project = review.project.split('/').pop();
|
|
278
|
-
const
|
|
279
|
-
const
|
|
563
|
+
const project = typeof review.project === 'string' ? review.project.split('/').pop() : '';
|
|
564
|
+
const reviewId = typeof review.id === 'string' ? review.id : '';
|
|
565
|
+
const reviewIsGitHub = reviewId.startsWith('github');
|
|
566
|
+
const mrPrefix = reviewIsGitHub ? '#' : '!';
|
|
567
|
+
const mrLabel = getMrLabel(reviewIsGitHub ? 'github' : 'gitlab');
|
|
568
|
+
const safeMrUrl = sanitizeHttpUrl(typeof review.mrUrl === 'string' ? review.mrUrl : null);
|
|
280
569
|
|
|
281
570
|
const progressHtml = isActive && review.progress ? `
|
|
282
571
|
${renderAgentTimeline(review.progress)}
|
|
283
572
|
${renderProgressBar(review.progress)}
|
|
284
573
|
` : '';
|
|
285
574
|
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
const assignerDisplay = review.assignedBy?.displayName || assignerUsername;
|
|
575
|
+
const trackedMergeRequests = [...currentData.pendingFix, ...currentData.pendingApproval];
|
|
576
|
+
const assignerDisplay = resolveReviewAssigneeDisplay(review, trackedMergeRequests);
|
|
289
577
|
const assignerInitial = assignerDisplay.charAt(0).toUpperCase();
|
|
578
|
+
const safeReviewAccordionId = reviewId.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
579
|
+
const safeJobType = review.jobType === 'followup' ? 'followup' : 'review';
|
|
290
580
|
|
|
291
|
-
// Description accordion (for active reviews)
|
|
292
581
|
const descriptionHtml = isActive && review.description ? `
|
|
293
582
|
<div class="review-description-accordion">
|
|
294
|
-
<div class="review-description-toggle" onclick="toggleReviewDescription('${
|
|
295
|
-
<i data-lucide="chevron-right"></i>
|
|
583
|
+
<div class="review-description-toggle" onclick="toggleReviewDescription('${safeReviewAccordionId}')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)">
|
|
584
|
+
<i data-lucide="chevron-right"></i> ${t('review.description')}
|
|
296
585
|
</div>
|
|
297
|
-
<div class="review-description-content" id="review-desc-${
|
|
586
|
+
<div class="review-description-content" id="review-desc-${safeReviewAccordionId}">
|
|
298
587
|
<div class="review-description-text">${escapeHtml(review.description)}</div>
|
|
299
588
|
</div>
|
|
300
589
|
</div>
|
|
301
590
|
` : '';
|
|
302
591
|
|
|
303
|
-
// Title: use MR title if available, else fallback to project name
|
|
304
592
|
const displayTitle = review.title || project;
|
|
305
593
|
|
|
306
594
|
return `
|
|
307
|
-
<div class="review-item" data-job-id="${
|
|
595
|
+
<div class="review-item" data-job-id="${escapeHtml(reviewId)}">
|
|
308
596
|
<div class="review-header">
|
|
309
|
-
<div class="review-status ${
|
|
597
|
+
<div class="review-status ${safeStatusClass}" title="${escapeHtml(statusPresentation.label)}"></div>
|
|
310
598
|
<div class="review-info">
|
|
311
599
|
<div class="review-title">
|
|
312
|
-
<a href="${
|
|
600
|
+
<a href="${safeMrUrl}" target="_blank" rel="noopener noreferrer">${mrPrefix}${mrNumber}</a> - ${escapeHtml(displayTitle)}
|
|
313
601
|
</div>
|
|
314
602
|
<div class="review-meta">
|
|
315
|
-
<span class="badge ${
|
|
603
|
+
<span class="badge ${safeStatusClass}"><i data-lucide="${statusPresentation.icon}"></i> ${escapeHtml(statusPresentation.label)}</span>
|
|
316
604
|
${isActive ? `<i data-lucide="clock"></i> ${formatDuration(review.startedAt)}` : ''}
|
|
317
605
|
</div>
|
|
318
|
-
${review.error ? `<div class="error-message">${review.error}</div>` : ''}
|
|
606
|
+
${review.error ? `<div class="error-message">${escapeHtml(review.error)}</div>` : ''}
|
|
319
607
|
</div>
|
|
320
|
-
${isActive ? `<button class="btn-cancel-review" onclick="event.stopPropagation(); showCancelModal('${
|
|
608
|
+
${isActive ? `<button class="btn-cancel-review" onclick="event.stopPropagation(); showCancelModal('${encodeURIComponent(reviewId)}', ${mrNumber}, '${safeJobType}')" title="${t('button.cancel')}"><i data-lucide="x"></i> ${t('button.cancel')}</button>` : ''}
|
|
321
609
|
<div class="review-assigner">
|
|
322
610
|
<div class="review-assigner-info">
|
|
323
|
-
<span class="review-assigner-name">${assignerDisplay}</span>
|
|
611
|
+
<span class="review-assigner-name">${escapeHtml(assignerDisplay)}</span>
|
|
324
612
|
<span class="review-assigner-time">${isActive ? formatTime(review.startedAt) : formatTime(review.completedAt)}</span>
|
|
325
613
|
</div>
|
|
326
|
-
<div class="review-avatar" title="${assignerDisplay}">${assignerInitial}</div>
|
|
614
|
+
<div class="review-avatar" title="${escapeHtml(assignerDisplay)}">${escapeHtml(assignerInitial)}</div>
|
|
327
615
|
</div>
|
|
328
616
|
</div>
|
|
329
617
|
${descriptionHtml}
|
|
@@ -349,21 +637,16 @@
|
|
|
349
637
|
}
|
|
350
638
|
}
|
|
351
639
|
|
|
352
|
-
function escapeHtml(text) {
|
|
353
|
-
if (!text) return '';
|
|
354
|
-
const div = document.createElement('div');
|
|
355
|
-
div.textContent = text;
|
|
356
|
-
return div.innerHTML;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
640
|
function renderLog(log) {
|
|
360
|
-
const dataStr = log.data ? JSON.stringify(log.data, null, 2) : '';
|
|
641
|
+
const dataStr = log.data ? escapeHtml(JSON.stringify(log.data, null, 2)) : '';
|
|
642
|
+
const logLevel = typeof log.level === 'string' ? log.level : 'info';
|
|
643
|
+
const safeLogLevelClass = logLevel.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
361
644
|
return `
|
|
362
645
|
<div class="log-entry">
|
|
363
646
|
<span class="log-time">${formatLogTime(log.timestamp)}</span>
|
|
364
|
-
<span class="log-level ${
|
|
647
|
+
<span class="log-level ${safeLogLevelClass}">${escapeHtml(logLevel.toUpperCase())}</span>
|
|
365
648
|
<div class="log-message">
|
|
366
|
-
${log.message}
|
|
649
|
+
${escapeHtml(String(log.message ?? ''))}
|
|
367
650
|
${dataStr ? `<div class="log-data">${dataStr}</div>` : ''}
|
|
368
651
|
</div>
|
|
369
652
|
</div>
|
|
@@ -371,31 +654,36 @@
|
|
|
371
654
|
}
|
|
372
655
|
|
|
373
656
|
function updateUI() {
|
|
374
|
-
// Separate reviews and followups
|
|
375
657
|
const reviews = currentData.activeReviews.filter(r => r.jobType !== 'followup');
|
|
376
658
|
const followups = currentData.activeReviews.filter(r => r.jobType === 'followup');
|
|
377
659
|
|
|
378
660
|
const running = currentData.activeReviews.filter(r => r.status === 'running').length;
|
|
379
661
|
const queued = currentData.activeReviews.filter(r => r.status === 'queued').length;
|
|
662
|
+
const blocked = currentData.pendingFix.length;
|
|
663
|
+
const nowCount = running + blocked;
|
|
664
|
+
const nextCount = queued + currentData.pendingApproval.length;
|
|
380
665
|
document.getElementById('running-count').textContent = running;
|
|
381
666
|
document.getElementById('queued-count').textContent = queued;
|
|
382
667
|
document.getElementById('completed-count').textContent = currentData.reviewFiles.length;
|
|
668
|
+
document.getElementById('focus-now-count').textContent = String(nowCount);
|
|
669
|
+
document.getElementById('focus-next-count').textContent = String(nextCount);
|
|
670
|
+
document.getElementById('focus-blocked-count').textContent = String(blocked);
|
|
383
671
|
|
|
384
|
-
// Update Reviews section
|
|
385
672
|
const activeReviewsSection = document.getElementById('active-reviews-section');
|
|
386
673
|
const activeReviewsEl = document.getElementById('active-reviews');
|
|
387
674
|
const activeReviewsCount = document.getElementById('active-reviews-count');
|
|
388
675
|
|
|
676
|
+
activeReviewsSection.classList.remove('hidden');
|
|
389
677
|
if (reviews.length === 0) {
|
|
390
|
-
|
|
678
|
+
const showInitialLoadingState = !hasLoadedStatusOnce && loadingState.status > 0;
|
|
679
|
+
activeReviewsEl.innerHTML = `<div class="empty-state">${showInitialLoadingState ? t('loading.section') : t('empty.activeReviews')}</div>`;
|
|
680
|
+
activeReviewsCount.classList.add('hidden');
|
|
391
681
|
} else {
|
|
392
|
-
activeReviewsSection.classList.remove('hidden');
|
|
393
682
|
activeReviewsEl.innerHTML = reviews.map(r => renderReview(r, true)).join('');
|
|
394
683
|
activeReviewsCount.textContent = reviews.length;
|
|
395
684
|
activeReviewsCount.classList.remove('hidden');
|
|
396
685
|
}
|
|
397
686
|
|
|
398
|
-
// Update Followups section
|
|
399
687
|
const activeFollowupsSection = document.getElementById('active-followups-section');
|
|
400
688
|
const activeFollowupsEl = document.getElementById('active-followups');
|
|
401
689
|
const activeFollowupsCount = document.getElementById('active-followups-count');
|
|
@@ -407,10 +695,10 @@
|
|
|
407
695
|
activeFollowupsEl.innerHTML = followups.map(r => renderReview(r, true)).join('');
|
|
408
696
|
activeFollowupsCount.textContent = followups.length;
|
|
409
697
|
activeFollowupsCount.classList.remove('hidden');
|
|
698
|
+
applySectionExpansion('active-followups-section');
|
|
410
699
|
}
|
|
411
700
|
|
|
412
701
|
refreshIcons();
|
|
413
|
-
// Note: recent-reviews is managed by updateReviewFilesUI() - don't overwrite here
|
|
414
702
|
}
|
|
415
703
|
|
|
416
704
|
function updateLogs() {
|
|
@@ -419,112 +707,68 @@
|
|
|
419
707
|
const errorBadge = document.getElementById('error-count');
|
|
420
708
|
|
|
421
709
|
if (errorCount > 0) {
|
|
422
|
-
errorBadge.textContent =
|
|
710
|
+
errorBadge.textContent = t('logs.errorCount', { count: errorCount });
|
|
423
711
|
errorBadge.classList.remove('hidden');
|
|
424
712
|
} else {
|
|
425
713
|
errorBadge.classList.add('hidden');
|
|
426
714
|
}
|
|
427
715
|
|
|
428
716
|
if (currentData.logs.length === 0) {
|
|
429
|
-
logsEl.innerHTML =
|
|
717
|
+
logsEl.innerHTML = `<div class="empty-state">${t('empty.logs')}</div>`;
|
|
430
718
|
} else {
|
|
431
719
|
logsEl.innerHTML = currentData.logs.slice().reverse().map(renderLog).join('');
|
|
432
720
|
}
|
|
433
721
|
}
|
|
434
722
|
|
|
435
|
-
function updateConnectionStatus(status,
|
|
723
|
+
function updateConnectionStatus(status, textKey, textFallback) {
|
|
436
724
|
const statusEl = document.getElementById('server-status');
|
|
437
725
|
statusEl.className = `status-indicator ${status}`;
|
|
438
|
-
|
|
726
|
+
const displayText = textKey ? t(textKey) : textFallback;
|
|
727
|
+
statusEl.innerHTML = `<span class="status-dot"></span><span>${escapeHtml(String(displayText ?? ''))}</span>`;
|
|
439
728
|
|
|
440
729
|
const modeEl = document.getElementById('connection-mode');
|
|
441
730
|
modeEl.textContent = status === 'online' && wsConnected
|
|
442
|
-
? '
|
|
443
|
-
: status === 'online' ? '
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Simple markdown to HTML converter
|
|
447
|
-
function markdownToHtml(md) {
|
|
448
|
-
let html = md
|
|
449
|
-
// Escape HTML
|
|
450
|
-
.replace(/&/g, '&')
|
|
451
|
-
.replace(/</g, '<')
|
|
452
|
-
.replace(/>/g, '>')
|
|
453
|
-
// Code blocks
|
|
454
|
-
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
|
455
|
-
// Inline code
|
|
456
|
-
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
457
|
-
// Headers
|
|
458
|
-
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
|
459
|
-
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
460
|
-
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
461
|
-
// Bold
|
|
462
|
-
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
463
|
-
// Italic
|
|
464
|
-
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
465
|
-
// Links
|
|
466
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
|
467
|
-
// Blockquotes
|
|
468
|
-
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
|
|
469
|
-
// Unordered lists
|
|
470
|
-
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
471
|
-
// Ordered lists
|
|
472
|
-
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
|
473
|
-
// Horizontal rules
|
|
474
|
-
.replace(/^---$/gm, '<hr>')
|
|
475
|
-
// Tables (simple)
|
|
476
|
-
.replace(/^\|(.+)\|$/gm, (match, content) => {
|
|
477
|
-
const cells = content.split('|').map(c => c.trim());
|
|
478
|
-
if (cells.every(c => c.match(/^-+$/))) return '';
|
|
479
|
-
const cellTag = match.includes('---') ? 'th' : 'td';
|
|
480
|
-
return '<tr>' + cells.map(c => `<${cellTag}>${c}</${cellTag}>`).join('') + '</tr>';
|
|
481
|
-
})
|
|
482
|
-
// Wrap consecutive table rows
|
|
483
|
-
.replace(/(<tr>[\s\S]*?<\/tr>)+/g, '<table>$&</table>')
|
|
484
|
-
// Wrap consecutive list items
|
|
485
|
-
.replace(/(<li>[\s\S]*?<\/li>)+/g, '<ul>$&</ul>')
|
|
486
|
-
// Paragraphs
|
|
487
|
-
.replace(/\n\n/g, '</p><p>')
|
|
488
|
-
.replace(/\n/g, '<br>');
|
|
489
|
-
|
|
490
|
-
return '<p>' + html + '</p>';
|
|
731
|
+
? t('connection.websocket')
|
|
732
|
+
: status === 'online' ? t('connection.polling') : t('connection.disconnected');
|
|
491
733
|
}
|
|
492
734
|
|
|
493
735
|
function renderReviewFile(review) {
|
|
494
736
|
const typeIcon = review.type === 'review' ? 'file-text' : review.type === 'followup' ? 'refresh-cw' : 'file';
|
|
495
|
-
const typeLabel = review.type === 'review' ? '
|
|
737
|
+
const typeLabel = review.type === 'review' ? t('review.type.review') : review.type === 'followup' ? t('review.type.followup') : review.type;
|
|
496
738
|
const sizeKb = (review.size / 1024).toFixed(1);
|
|
497
739
|
const mrLabel = getMrLabel();
|
|
740
|
+
const encodedFilename = encodeURIComponent(String(review.filename ?? ''));
|
|
741
|
+
const filenameDomId = encodedFilename.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
498
742
|
|
|
499
743
|
return `
|
|
500
|
-
<div class="review-accordion" data-filename="${
|
|
744
|
+
<div class="review-accordion" data-filename="${encodedFilename}">
|
|
501
745
|
<div class="review-accordion-header">
|
|
502
|
-
<div class="review-accordion-toggle" onclick="toggleReviewAccordion('${
|
|
503
|
-
<div class="review-status completed" onclick="toggleReviewAccordion('${
|
|
504
|
-
<div class="review-info" onclick="toggleReviewAccordion('${
|
|
505
|
-
<div class="review-title">${mrLabel} ${review.mrNumber}${review.title ? ` - ${review.title}` : ''}</div>
|
|
746
|
+
<div class="review-accordion-toggle" onclick="toggleReviewAccordion('${encodedFilename}')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)"><i data-lucide="chevron-right"></i></div>
|
|
747
|
+
<div class="review-status completed" onclick="toggleReviewAccordion('${encodedFilename}')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)"></div>
|
|
748
|
+
<div class="review-info" onclick="toggleReviewAccordion('${encodedFilename}')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)" style="cursor: pointer;">
|
|
749
|
+
<div class="review-title">${mrLabel} ${review.mrNumber}${review.title ? ` - ${escapeHtml(String(review.title))}` : ''}</div>
|
|
506
750
|
<div class="review-meta">
|
|
507
751
|
<span class="badge completed"><i data-lucide="${typeIcon}"></i> ${typeLabel}</span>
|
|
508
752
|
<span style="margin-left: 0.5rem; color: #71717a;">${sizeKb} KB</span>
|
|
509
753
|
</div>
|
|
510
754
|
</div>
|
|
511
|
-
<div class="review-time" onclick="toggleReviewAccordion('${
|
|
512
|
-
<button class="btn-delete" onclick="deleteReviewFile('${
|
|
755
|
+
<div class="review-time" onclick="toggleReviewAccordion('${encodedFilename}')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)" style="cursor: pointer;">${escapeHtml(String(review.date ?? ''))}</div>
|
|
756
|
+
<button class="btn-delete" onclick="deleteReviewFile('${encodedFilename}')" title="${t('button.delete')}"><i data-lucide="trash-2"></i></button>
|
|
513
757
|
</div>
|
|
514
758
|
<div class="review-accordion-content">
|
|
515
|
-
<div class="markdown-content" id="review-content-${
|
|
516
|
-
<div class="empty-state"
|
|
759
|
+
<div class="markdown-content" id="review-content-${filenameDomId}">
|
|
760
|
+
<div class="empty-state">${t('status.loading')}</div>
|
|
517
761
|
</div>
|
|
518
762
|
</div>
|
|
519
763
|
</div>
|
|
520
764
|
`;
|
|
521
765
|
}
|
|
522
766
|
|
|
523
|
-
async function toggleReviewAccordion(
|
|
524
|
-
const
|
|
767
|
+
async function toggleReviewAccordion(encodedFilename) {
|
|
768
|
+
const filename = safeDecodeURIComponent(encodedFilename);
|
|
769
|
+
const accordion = document.querySelector(`.review-accordion[data-filename="${encodedFilename}"]`);
|
|
525
770
|
const isOpen = accordion.classList.contains('open');
|
|
526
771
|
|
|
527
|
-
// Close all other accordions
|
|
528
772
|
document.querySelectorAll('.review-accordion.open').forEach(el => {
|
|
529
773
|
if (el !== accordion) el.classList.remove('open');
|
|
530
774
|
});
|
|
@@ -535,17 +779,16 @@
|
|
|
535
779
|
}
|
|
536
780
|
|
|
537
781
|
accordion.classList.add('open');
|
|
538
|
-
const contentId = `review-content-${
|
|
782
|
+
const contentId = `review-content-${encodedFilename.replace(/[^a-zA-Z0-9-]/g, '-')}`;
|
|
539
783
|
const contentEl = document.getElementById(contentId);
|
|
540
784
|
|
|
541
|
-
// Load content if not cached
|
|
542
785
|
if (!loadedReviews[filename]) {
|
|
543
786
|
try {
|
|
544
787
|
const response = await fetch(`${API_URL}/api/reviews/${filename}`);
|
|
545
788
|
const data = await response.json();
|
|
546
789
|
loadedReviews[filename] = data.content;
|
|
547
790
|
} catch (error) {
|
|
548
|
-
contentEl.innerHTML =
|
|
791
|
+
contentEl.innerHTML = `<div class="empty-state">${t('error.loading')}</div>`;
|
|
549
792
|
return;
|
|
550
793
|
}
|
|
551
794
|
}
|
|
@@ -554,8 +797,8 @@
|
|
|
554
797
|
}
|
|
555
798
|
|
|
556
799
|
async function fetchReviewFiles() {
|
|
800
|
+
setLoadingFlag('reviewFiles', true);
|
|
557
801
|
try {
|
|
558
|
-
// Fetch reviews for current project if loaded, otherwise all
|
|
559
802
|
const url = currentProjectPath
|
|
560
803
|
? `${API_URL}/api/reviews?path=${encodeURIComponent(currentProjectPath)}`
|
|
561
804
|
: `${API_URL}/api/reviews`;
|
|
@@ -565,14 +808,18 @@
|
|
|
565
808
|
updateReviewFilesUI();
|
|
566
809
|
} catch (error) {
|
|
567
810
|
console.error('Error fetching review files:', error);
|
|
811
|
+
} finally {
|
|
812
|
+
setLoadingFlag('reviewFiles', false);
|
|
568
813
|
}
|
|
569
814
|
}
|
|
570
815
|
|
|
571
816
|
async function fetchProjectStats() {
|
|
572
817
|
const statsEl = document.getElementById('project-stats');
|
|
818
|
+
setLoadingFlag('stats', true);
|
|
573
819
|
|
|
574
820
|
if (!currentProjectPath) {
|
|
575
|
-
statsEl.innerHTML =
|
|
821
|
+
statsEl.innerHTML = `<div class="empty-state">${t('empty.statsNoProject')}</div>`;
|
|
822
|
+
setLoadingFlag('stats', false);
|
|
576
823
|
return;
|
|
577
824
|
}
|
|
578
825
|
|
|
@@ -582,58 +829,70 @@
|
|
|
582
829
|
|
|
583
830
|
if (data.summary) {
|
|
584
831
|
const s = data.summary;
|
|
585
|
-
const trendIcon = (
|
|
832
|
+
const trendIcon = (trend) => trend === 'up'
|
|
833
|
+
? '<span class="stat-trend up"><i data-lucide="trending-up"></i></span>'
|
|
834
|
+
: trend === 'down'
|
|
835
|
+
? '<span class="stat-trend down"><i data-lucide="trending-down"></i></span>'
|
|
836
|
+
: '<span class="stat-trend flat"><i data-lucide="minus"></i></span>';
|
|
586
837
|
|
|
587
838
|
statsEl.innerHTML = `
|
|
588
|
-
<div class="stat-card">
|
|
589
|
-
<div class="stat-value">${s.totalReviews}</div>
|
|
590
|
-
<div class="stat-label"><i data-lucide="file-search"></i>
|
|
839
|
+
<div class="stat-card metric-reviews">
|
|
840
|
+
<div class="stat-value"><span class="stat-main">${s.totalReviews}</span></div>
|
|
841
|
+
<div class="stat-label"><i data-lucide="file-search"></i> ${t('stats.reviews')}</div>
|
|
591
842
|
</div>
|
|
592
|
-
<div class="stat-card">
|
|
593
|
-
<div class="stat-value">${s.averageScore}
|
|
594
|
-
<div class="stat-label"><i data-lucide="star"></i>
|
|
843
|
+
<div class="stat-card metric-score">
|
|
844
|
+
<div class="stat-value"><span class="stat-main">${s.averageScore}</span><span class="stat-denominator">/10</span>${trendIcon(s.trend.score)}</div>
|
|
845
|
+
<div class="stat-label"><i data-lucide="star"></i> ${t('stats.averageScore')}</div>
|
|
595
846
|
</div>
|
|
596
|
-
<div class="stat-card">
|
|
597
|
-
<div class="stat-value">${s.totalTime}</div>
|
|
598
|
-
<div class="stat-label"><i data-lucide="timer"></i>
|
|
847
|
+
<div class="stat-card metric-time">
|
|
848
|
+
<div class="stat-value"><span class="stat-main">${s.totalTime}</span></div>
|
|
849
|
+
<div class="stat-label"><i data-lucide="timer"></i> ${t('stats.totalTime')}</div>
|
|
599
850
|
</div>
|
|
600
|
-
<div class="stat-card">
|
|
601
|
-
<div class="stat-value">${s.averageTime}</div>
|
|
602
|
-
<div class="stat-label"><i data-lucide="clock"></i>
|
|
851
|
+
<div class="stat-card metric-average-time">
|
|
852
|
+
<div class="stat-value"><span class="stat-main">${s.averageTime}</span></div>
|
|
853
|
+
<div class="stat-label"><i data-lucide="clock"></i> ${t('stats.averageTime')}</div>
|
|
603
854
|
</div>
|
|
604
|
-
<div class="stat-card warning">
|
|
605
|
-
<div class="stat-value">${s.totalBlocking}
|
|
606
|
-
<div class="stat-label"><i data-lucide="octagon-alert"></i>
|
|
855
|
+
<div class="stat-card warning metric-blocking">
|
|
856
|
+
<div class="stat-value"><span class="stat-main">${s.totalBlocking}</span>${trendIcon(s.trend.blocking)}</div>
|
|
857
|
+
<div class="stat-label"><i data-lucide="octagon-alert"></i> ${t('stats.blocking')}</div>
|
|
607
858
|
</div>
|
|
608
|
-
<div class="stat-card">
|
|
609
|
-
<div class="stat-value">${s.totalWarnings}</div>
|
|
610
|
-
<div class="stat-label"><i data-lucide="alert-triangle"></i>
|
|
859
|
+
<div class="stat-card metric-warnings">
|
|
860
|
+
<div class="stat-value"><span class="stat-main">${s.totalWarnings}</span></div>
|
|
861
|
+
<div class="stat-label"><i data-lucide="alert-triangle"></i> ${t('stats.warnings')}</div>
|
|
611
862
|
</div>
|
|
612
863
|
`;
|
|
613
864
|
refreshIcons();
|
|
614
865
|
} else {
|
|
615
|
-
statsEl.innerHTML =
|
|
866
|
+
statsEl.innerHTML = `<div class="empty-state">${t('empty.statsNoData')}</div>`;
|
|
616
867
|
}
|
|
617
868
|
} catch (error) {
|
|
618
869
|
console.error('Error fetching stats:', error);
|
|
619
|
-
statsEl.innerHTML =
|
|
870
|
+
statsEl.innerHTML = `<div class="empty-state">${t('error.loadingStats')}</div>`;
|
|
871
|
+
} finally {
|
|
872
|
+
setLoadingFlag('stats', false);
|
|
620
873
|
}
|
|
621
874
|
}
|
|
622
875
|
|
|
623
876
|
function updateReviewFilesUI() {
|
|
624
877
|
const recentEl = document.getElementById('recent-reviews');
|
|
878
|
+
const completedSection = document.getElementById('completed-reviews-section');
|
|
625
879
|
|
|
626
880
|
if (currentData.reviewFiles.length === 0) {
|
|
627
|
-
|
|
881
|
+
completedSection?.classList.remove('hidden');
|
|
882
|
+
recentEl.innerHTML = `<div class="empty-state">${loadingState.reviewFiles ? t('loading.section') : t('empty.reviewFiles')}</div>`;
|
|
883
|
+
applySectionExpansion('completed-reviews-section');
|
|
628
884
|
return;
|
|
629
885
|
}
|
|
630
886
|
|
|
887
|
+
completedSection?.classList.remove('hidden');
|
|
631
888
|
recentEl.innerHTML = currentData.reviewFiles.map(renderReviewFile).join('');
|
|
889
|
+
applySectionExpansion('completed-reviews-section');
|
|
632
890
|
refreshIcons();
|
|
633
891
|
}
|
|
634
892
|
|
|
635
|
-
async function deleteReviewFile(
|
|
636
|
-
|
|
893
|
+
async function deleteReviewFile(encodedFilename) {
|
|
894
|
+
const filename = safeDecodeURIComponent(encodedFilename);
|
|
895
|
+
if (!confirm(t('confirm.deleteReview', { filename }))) return;
|
|
637
896
|
|
|
638
897
|
try {
|
|
639
898
|
const response = await fetch(`${API_URL}/api/reviews/${filename}`, {
|
|
@@ -642,18 +901,16 @@
|
|
|
642
901
|
const data = await response.json();
|
|
643
902
|
|
|
644
903
|
if (data.success) {
|
|
645
|
-
// Remove from cache
|
|
646
904
|
delete loadedReviews[filename];
|
|
647
|
-
// Remove from list and update UI
|
|
648
905
|
currentData.reviewFiles = currentData.reviewFiles.filter(r => r.filename !== filename);
|
|
649
906
|
updateReviewFilesUI();
|
|
650
|
-
updateUI();
|
|
907
|
+
updateUI();
|
|
651
908
|
} else {
|
|
652
|
-
alert('
|
|
909
|
+
alert(t('error.deleteReview') + ': ' + data.error);
|
|
653
910
|
}
|
|
654
911
|
} catch (error) {
|
|
655
912
|
console.error('Error deleting review:', error);
|
|
656
|
-
alert('
|
|
913
|
+
alert(t('error.deleteReview'));
|
|
657
914
|
}
|
|
658
915
|
}
|
|
659
916
|
|
|
@@ -662,7 +919,7 @@
|
|
|
662
919
|
const loginSection = document.getElementById('claude-login-section');
|
|
663
920
|
|
|
664
921
|
claudeEl.className = 'card-claude checking';
|
|
665
|
-
claudeEl.innerHTML =
|
|
922
|
+
claudeEl.innerHTML = `<span class="status">${t('status.checking')}</span>`;
|
|
666
923
|
|
|
667
924
|
try {
|
|
668
925
|
const response = await fetch(`${API_URL}/api/claude/status`);
|
|
@@ -671,29 +928,28 @@
|
|
|
671
928
|
if (data.available) {
|
|
672
929
|
claudeEl.className = 'card-claude available';
|
|
673
930
|
claudeEl.innerHTML = `
|
|
674
|
-
<span class="status"><i data-lucide="check-circle"></i>
|
|
675
|
-
<span class="version">${data.version}</span>
|
|
931
|
+
<span class="status"><i data-lucide="check-circle"></i> ${t('status.operational')}</span>
|
|
932
|
+
<span class="version">${escapeHtml(String(data.version ?? ''))}</span>
|
|
676
933
|
`;
|
|
677
934
|
loginSection.classList.add('hidden');
|
|
678
935
|
refreshIcons();
|
|
679
936
|
} else {
|
|
680
937
|
claudeEl.className = 'card-claude unavailable';
|
|
681
938
|
claudeEl.innerHTML = `
|
|
682
|
-
<span class="status"><i data-lucide="x-circle"></i> ${data.message}</span>
|
|
939
|
+
<span class="status"><i data-lucide="x-circle"></i> ${escapeHtml(String(data.message ?? ''))}</span>
|
|
683
940
|
`;
|
|
684
941
|
refreshIcons();
|
|
685
|
-
if (data.message.includes('authentifié') || data.message.includes('login')) {
|
|
942
|
+
if (data.message.includes('authentifié') || data.message.includes('login') || data.message.includes('authenticated')) {
|
|
686
943
|
loginSection.classList.remove('hidden');
|
|
687
944
|
}
|
|
688
945
|
}
|
|
689
946
|
} catch (error) {
|
|
690
947
|
claudeEl.className = 'card-claude unavailable';
|
|
691
|
-
claudeEl.innerHTML =
|
|
948
|
+
claudeEl.innerHTML = `<span class="status"><i data-lucide="x-circle"></i> ${t('error.checkStatus')}</span>`;
|
|
692
949
|
refreshIcons();
|
|
693
950
|
}
|
|
694
951
|
}
|
|
695
952
|
|
|
696
|
-
// Active platform from config (gitlab or github)
|
|
697
953
|
let activePlatform = null;
|
|
698
954
|
|
|
699
955
|
function updateGitCliUI() {
|
|
@@ -702,44 +958,43 @@
|
|
|
702
958
|
const loginSection = document.getElementById('git-login-section');
|
|
703
959
|
|
|
704
960
|
if (!activePlatform) {
|
|
705
|
-
labelEl.textContent = '
|
|
961
|
+
labelEl.textContent = t('card.gitCli');
|
|
706
962
|
statusEl.className = 'card-claude checking';
|
|
707
|
-
statusEl.innerHTML =
|
|
963
|
+
statusEl.innerHTML = `<span class="status">${t('status.loadProject')}</span>`;
|
|
708
964
|
loginSection.classList.add('hidden');
|
|
709
965
|
return;
|
|
710
966
|
}
|
|
711
967
|
|
|
712
968
|
const isGitlab = activePlatform === 'gitlab';
|
|
713
|
-
labelEl.textContent = isGitlab ? '
|
|
969
|
+
labelEl.textContent = isGitlab ? t('card.gitlabCli') : t('card.githubCli');
|
|
714
970
|
|
|
715
|
-
// Update login section with CLI + Webhook instructions
|
|
716
971
|
const titleEl = document.getElementById('git-login-title');
|
|
717
972
|
const instructionsEl = document.getElementById('git-login-instructions');
|
|
718
973
|
|
|
719
974
|
if (isGitlab) {
|
|
720
|
-
titleEl.innerHTML =
|
|
975
|
+
titleEl.innerHTML = `<i data-lucide="alert-triangle"></i> ${t('login.gitlab.title')}`;
|
|
721
976
|
instructionsEl.innerHTML = `
|
|
722
|
-
<p><strong
|
|
977
|
+
<p><strong>${t('setup.installAndAuth', { cli: 'glab' })}</strong></p>
|
|
723
978
|
<p style="margin: 0.5rem 0;"><code>sudo apt install glab</code></p>
|
|
724
979
|
<p style="margin: 0.5rem 0;"><code>glab auth login</code></p>
|
|
725
|
-
<p style="margin-top: 1rem;"><strong
|
|
726
|
-
<p style="margin: 0.5rem 0; font-size: 0.8rem;"
|
|
980
|
+
<p style="margin-top: 1rem;"><strong>${t('setup.configureWebhook', { platform: 'GitLab' })}</strong></p>
|
|
981
|
+
<p style="margin: 0.5rem 0; font-size: 0.8rem;">${t('setup.webhookPath')}</p>
|
|
727
982
|
<p style="margin: 0.25rem 0; font-size: 0.8rem;">URL: <code>http://<your-server>:3847/webhooks/gitlab</code></p>
|
|
728
|
-
<p style="margin: 0.25rem 0; font-size: 0.8rem;"
|
|
729
|
-
<p style="margin-top: 0.75rem; font-size: 0.75rem; color: #a1a1aa;"
|
|
983
|
+
<p style="margin: 0.25rem 0; font-size: 0.8rem;">${t('setup.gitlab.trigger')}</p>
|
|
984
|
+
<p style="margin-top: 0.75rem; font-size: 0.75rem; color: #a1a1aa;">${t('setup.reload')}</p>
|
|
730
985
|
`;
|
|
731
986
|
} else {
|
|
732
|
-
titleEl.innerHTML =
|
|
987
|
+
titleEl.innerHTML = `<i data-lucide="alert-triangle"></i> ${t('login.github.title')}`;
|
|
733
988
|
instructionsEl.innerHTML = `
|
|
734
|
-
<p><strong
|
|
989
|
+
<p><strong>${t('setup.installAndAuth', { cli: 'gh' })}</strong></p>
|
|
735
990
|
<p style="margin: 0.5rem 0;"><code>sudo apt install gh</code></p>
|
|
736
991
|
<p style="margin: 0.5rem 0;"><code>gh auth login</code></p>
|
|
737
|
-
<p style="margin-top: 1rem;"><strong
|
|
738
|
-
<p style="margin: 0.5rem 0; font-size: 0.8rem;"
|
|
992
|
+
<p style="margin-top: 1rem;"><strong>${t('setup.configureWebhook', { platform: 'GitHub' })}</strong></p>
|
|
993
|
+
<p style="margin: 0.5rem 0; font-size: 0.8rem;">${t('setup.webhookPath')}</p>
|
|
739
994
|
<p style="margin: 0.25rem 0; font-size: 0.8rem;">Payload URL: <code>http://<your-server>:3847/webhooks/github</code></p>
|
|
740
|
-
<p style="margin: 0.25rem 0; font-size: 0.8rem;"
|
|
741
|
-
<p style="margin: 0.25rem 0; font-size: 0.8rem;"
|
|
742
|
-
<p style="margin-top: 0.75rem; font-size: 0.75rem; color: #a1a1aa;"
|
|
995
|
+
<p style="margin: 0.25rem 0; font-size: 0.8rem;">${t('setup.github.contentType')}</p>
|
|
996
|
+
<p style="margin: 0.25rem 0; font-size: 0.8rem;">${t('setup.github.events')}</p>
|
|
997
|
+
<p style="margin-top: 0.75rem; font-size: 0.75rem; color: #a1a1aa;">${t('setup.reload')}</p>
|
|
743
998
|
`;
|
|
744
999
|
}
|
|
745
1000
|
|
|
@@ -756,7 +1011,7 @@
|
|
|
756
1011
|
const endpoint = isGitlab ? '/api/gitlab/status' : '/api/github/status';
|
|
757
1012
|
|
|
758
1013
|
statusEl.className = 'card-claude checking';
|
|
759
|
-
statusEl.innerHTML =
|
|
1014
|
+
statusEl.innerHTML = `<span class="status">${t('status.checking')}</span>`;
|
|
760
1015
|
|
|
761
1016
|
try {
|
|
762
1017
|
const response = await fetch(`${API_URL}${endpoint}`);
|
|
@@ -765,25 +1020,25 @@
|
|
|
765
1020
|
if (data.available && data.authenticated) {
|
|
766
1021
|
statusEl.className = 'card-claude available';
|
|
767
1022
|
statusEl.innerHTML = `
|
|
768
|
-
<span class="status"><i data-lucide="check-circle"></i>
|
|
769
|
-
<span class="version">@${data.username}</span>
|
|
1023
|
+
<span class="status"><i data-lucide="check-circle"></i> ${t('status.operational')}</span>
|
|
1024
|
+
<span class="version">@${escapeHtml(String(data.username ?? ''))}</span>
|
|
770
1025
|
`;
|
|
771
1026
|
loginSection.classList.add('hidden');
|
|
772
1027
|
refreshIcons();
|
|
773
1028
|
} else if (data.available && !data.authenticated) {
|
|
774
1029
|
statusEl.className = 'card-claude unavailable';
|
|
775
|
-
statusEl.innerHTML = `<span class="status"><i data-lucide="x-circle"></i> ${data.message}</span>`;
|
|
1030
|
+
statusEl.innerHTML = `<span class="status"><i data-lucide="x-circle"></i> ${escapeHtml(String(data.message ?? ''))}</span>`;
|
|
776
1031
|
loginSection.classList.remove('hidden');
|
|
777
1032
|
refreshIcons();
|
|
778
1033
|
} else {
|
|
779
1034
|
statusEl.className = 'card-claude unavailable';
|
|
780
|
-
statusEl.innerHTML = `<span class="status"><i data-lucide="x-circle"></i> ${data.message}</span>`;
|
|
1035
|
+
statusEl.innerHTML = `<span class="status"><i data-lucide="x-circle"></i> ${escapeHtml(String(data.message ?? ''))}</span>`;
|
|
781
1036
|
loginSection.classList.remove('hidden');
|
|
782
1037
|
refreshIcons();
|
|
783
1038
|
}
|
|
784
1039
|
} catch (error) {
|
|
785
1040
|
statusEl.className = 'card-claude unavailable';
|
|
786
|
-
statusEl.innerHTML =
|
|
1041
|
+
statusEl.innerHTML = `<span class="status"><i data-lucide="x-circle"></i> ${t('error.checkStatus')}</span>`;
|
|
787
1042
|
refreshIcons();
|
|
788
1043
|
}
|
|
789
1044
|
}
|
|
@@ -795,11 +1050,11 @@
|
|
|
795
1050
|
|
|
796
1051
|
if (logsVisible) {
|
|
797
1052
|
logsSection.classList.remove('hidden');
|
|
798
|
-
btn.innerHTML =
|
|
1053
|
+
btn.innerHTML = `<i data-lucide="scroll-text"></i> ${t('header.hideLogs')}`;
|
|
799
1054
|
fetchLogs();
|
|
800
1055
|
} else {
|
|
801
1056
|
logsSection.classList.add('hidden');
|
|
802
|
-
btn.innerHTML =
|
|
1057
|
+
btn.innerHTML = `<i data-lucide="scroll-text"></i> ${t('header.logs')}`;
|
|
803
1058
|
}
|
|
804
1059
|
refreshIcons();
|
|
805
1060
|
}
|
|
@@ -820,26 +1075,26 @@
|
|
|
820
1075
|
|
|
821
1076
|
function renderMrItem(mr, type) {
|
|
822
1077
|
const mrPrefix = mr.platform === 'github' ? '#' : '!';
|
|
1078
|
+
const encodedMrId = encodeURIComponent(String(mr.id ?? ''));
|
|
823
1079
|
const threadInfo = type === 'pending-fix'
|
|
824
|
-
? `<span class="mr-threads has-open"><i data-lucide="message-circle"></i> ${mr.
|
|
1080
|
+
? `<span class="mr-threads has-open"><i data-lucide="message-circle"></i> ${t('mr.threads.openAction', { count: mr.openThreads })}</span>`
|
|
825
1081
|
: (type === 'pending-approval' && mr.totalWarnings > 0)
|
|
826
|
-
? `<span class="mr-threads has-warnings"><i data-lucide="alert-triangle"></i> ${mr.
|
|
827
|
-
: `<span class="mr-threads all-resolved"><i data-lucide="check-circle"></i>
|
|
1082
|
+
? `<span class="mr-threads has-warnings"><i data-lucide="alert-triangle"></i> ${t('mr.threads.warningAction', { count: mr.totalWarnings })}</span>`
|
|
1083
|
+
: `<span class="mr-threads all-resolved"><i data-lucide="check-circle"></i> ${t('mr.threads.resolvedAction')}</span>`;
|
|
828
1084
|
|
|
829
|
-
const openBtn = `<a href="${mr.url}" target="_blank" class="btn-action open"><i data-lucide="external-link"></i>
|
|
1085
|
+
const openBtn = `<a href="${sanitizeHttpUrl(mr.url)}" target="_blank" rel="noopener noreferrer" class="btn-action open" onclick="return onUsefulLinkAction()"><i data-lucide="external-link"></i> ${t('button.open')}</a>`;
|
|
830
1086
|
const autoFollowupChecked = mr.autoFollowup !== false ? 'checked' : '';
|
|
831
1087
|
const showFollowupActions = type === 'pending-fix' ||
|
|
832
1088
|
(type === 'pending-approval' && mr.totalWarnings > 0);
|
|
833
1089
|
|
|
834
1090
|
const actions = showFollowupActions
|
|
835
|
-
? `<label class="auto-followup-toggle" onclick="event.stopPropagation()" title="
|
|
836
|
-
<input type="checkbox" ${autoFollowupChecked} onchange="toggleAutoFollowup('${
|
|
837
|
-
|
|
1091
|
+
? `<label class="auto-followup-toggle" onclick="event.stopPropagation()" title="${t('button.autoFollowup')}">
|
|
1092
|
+
<input type="checkbox" ${autoFollowupChecked} onchange="toggleAutoFollowup('${encodedMrId}', this.checked)">
|
|
1093
|
+
${t('button.autoFollowup')}
|
|
838
1094
|
</label>
|
|
839
|
-
<button class="btn-action" onclick="triggerFollowup('${
|
|
1095
|
+
<button class="btn-action" onclick="triggerFollowup('${encodedMrId}')"><i data-lucide="refresh-cw"></i> ${t('button.followup')}</button>${openBtn}`
|
|
840
1096
|
: openBtn;
|
|
841
1097
|
|
|
842
|
-
// Stats badges
|
|
843
1098
|
const statsBadges = [];
|
|
844
1099
|
if (mr.totalBlocking > 0) statsBadges.push(`<span class="stat-badge blocking"><i data-lucide="octagon-alert"></i> ${mr.totalBlocking}</span>`);
|
|
845
1100
|
if (mr.totalWarnings > 0) statsBadges.push(`<span class="stat-badge warning"><i data-lucide="alert-triangle"></i> ${mr.totalWarnings}</span>`);
|
|
@@ -848,22 +1103,20 @@
|
|
|
848
1103
|
|
|
849
1104
|
const durationFormatted = mr.totalDurationMs ? formatDuration(null, null, mr.totalDurationMs) : '';
|
|
850
1105
|
|
|
851
|
-
// Assigner info with avatar
|
|
852
1106
|
const assignerUsername = mr.assignment?.username || 'unknown';
|
|
853
1107
|
const assignerDisplay = mr.assignment?.displayName || assignerUsername;
|
|
854
1108
|
const assignerInitial = assignerDisplay.charAt(0).toUpperCase();
|
|
855
1109
|
const assignedAt = mr.assignment?.assignedAt ? formatTime(mr.assignment.assignedAt) : '';
|
|
856
1110
|
|
|
857
|
-
// Sanitize ID for accordion toggle
|
|
858
1111
|
const accordionId = mr.id.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
859
1112
|
|
|
860
1113
|
return `
|
|
861
1114
|
<div class="mr-item-accordion" data-mr-id="${mr.id}">
|
|
862
|
-
<div class="mr-item-header" onclick="toggleMrAccordion('${accordionId}')">
|
|
1115
|
+
<div class="mr-item-header" onclick="toggleMrAccordion('${accordionId}')" role="button" tabindex="0" onkeydown="activateOnKeydown(event)">
|
|
863
1116
|
<div class="review-status ${type === 'pending-fix' ? 'running' : (type === 'pending-approval' && mr.totalWarnings > 0) ? 'warnings' : 'completed'}"></div>
|
|
864
1117
|
<div class="mr-info">
|
|
865
1118
|
<div class="mr-title">
|
|
866
|
-
<a href="${mr.url}" target="_blank" onclick="event.stopPropagation()">${mrPrefix}${mr.mrNumber}</a> - ${mr.title}
|
|
1119
|
+
<a href="${sanitizeHttpUrl(mr.url)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">${mrPrefix}${mr.mrNumber}</a> - ${escapeHtml(String(mr.title ?? ''))}
|
|
867
1120
|
</div>
|
|
868
1121
|
<div class="mr-meta">
|
|
869
1122
|
${threadInfo}
|
|
@@ -873,43 +1126,43 @@
|
|
|
873
1126
|
</div>
|
|
874
1127
|
<div class="mr-assigner">
|
|
875
1128
|
<div class="mr-assigner-info">
|
|
876
|
-
<span class="mr-assigner-name">${assignerDisplay}</span>
|
|
1129
|
+
<span class="mr-assigner-name">${escapeHtml(assignerDisplay)}</span>
|
|
877
1130
|
<span class="mr-assigner-time">${assignedAt}</span>
|
|
878
1131
|
</div>
|
|
879
|
-
<div class="mr-avatar" title="${assignerDisplay}">${assignerInitial}</div>
|
|
1132
|
+
<div class="mr-avatar" title="${escapeHtml(assignerDisplay)}">${escapeHtml(assignerInitial)}</div>
|
|
880
1133
|
</div>
|
|
881
1134
|
<div class="mr-toggle"><i data-lucide="chevron-down"></i></div>
|
|
882
1135
|
</div>
|
|
883
1136
|
<div class="mr-item-content" id="mr-content-${accordionId}">
|
|
884
1137
|
<div class="mr-details">
|
|
885
1138
|
<div class="mr-detail-row">
|
|
886
|
-
<span class="mr-detail-label"><i data-lucide="git-branch"></i>
|
|
887
|
-
<span class="mr-detail-value">${mr.sourceBranch}</span>
|
|
1139
|
+
<span class="mr-detail-label"><i data-lucide="git-branch"></i> ${t('mr.detail.source')}</span>
|
|
1140
|
+
<span class="mr-detail-value">${escapeHtml(String(mr.sourceBranch ?? ''))}</span>
|
|
888
1141
|
</div>
|
|
889
1142
|
<div class="mr-detail-row">
|
|
890
|
-
<span class="mr-detail-label"><i data-lucide="git-merge"></i>
|
|
891
|
-
<span class="mr-detail-value">${mr.targetBranch}</span>
|
|
1143
|
+
<span class="mr-detail-label"><i data-lucide="git-merge"></i> ${t('mr.detail.target')}</span>
|
|
1144
|
+
<span class="mr-detail-value">${escapeHtml(String(mr.targetBranch ?? ''))}</span>
|
|
892
1145
|
</div>
|
|
893
1146
|
<div class="mr-detail-row">
|
|
894
|
-
<span class="mr-detail-label"><i data-lucide="calendar"></i>
|
|
1147
|
+
<span class="mr-detail-label"><i data-lucide="calendar"></i> ${t('mr.detail.created')}</span>
|
|
895
1148
|
<span class="mr-detail-value">${formatTime(mr.createdAt)}</span>
|
|
896
1149
|
</div>
|
|
897
1150
|
${mr.lastReviewAt ? `
|
|
898
1151
|
<div class="mr-detail-row">
|
|
899
|
-
<span class="mr-detail-label"><i data-lucide="file-search"></i>
|
|
1152
|
+
<span class="mr-detail-label"><i data-lucide="file-search"></i> ${t('mr.detail.lastReview')}</span>
|
|
900
1153
|
<span class="mr-detail-value">${formatTime(mr.lastReviewAt)}</span>
|
|
901
1154
|
</div>
|
|
902
1155
|
` : ''}
|
|
903
1156
|
${mr.reviews?.length ? `
|
|
904
1157
|
<div class="mr-reviews-history">
|
|
905
|
-
<div class="mr-detail-label"><i data-lucide="history"></i>
|
|
1158
|
+
<div class="mr-detail-label"><i data-lucide="history"></i> ${t('mr.detail.history', { count: mr.reviews.length })}</div>
|
|
906
1159
|
<div class="mr-reviews-list">
|
|
907
1160
|
${mr.reviews.slice(-5).reverse().map(r => `
|
|
908
1161
|
<div class="mr-review-event ${r.type}">
|
|
909
|
-
<span class="review-event-type">${r.type === 'review' ? '
|
|
1162
|
+
<span class="review-event-type">${r.type === 'review' ? t('review.type.review') : t('review.type.followup')}</span>
|
|
910
1163
|
<span class="review-event-time">${formatTime(r.timestamp)}</span>
|
|
911
1164
|
${r.score !== null ? `<span class="review-event-score">${r.score}/10</span>` : ''}
|
|
912
|
-
${r.blocking > 0 ? `<span class="review-event-blocking">${
|
|
1165
|
+
${r.blocking > 0 ? `<span class="review-event-blocking">${t('mr.review.blocking', { count: r.blocking })}</span>` : ''}
|
|
913
1166
|
</div>
|
|
914
1167
|
`).join('')}
|
|
915
1168
|
</div>
|
|
@@ -924,6 +1177,121 @@
|
|
|
924
1177
|
`;
|
|
925
1178
|
}
|
|
926
1179
|
|
|
1180
|
+
function renderNowLane(mr) {
|
|
1181
|
+
const mrPrefix = mr.platform === 'github' ? '#' : '!';
|
|
1182
|
+
const assigneeUsername = mr.assignment?.username || 'unknown';
|
|
1183
|
+
const assigneeDisplay = mr.assignment?.displayName || assigneeUsername;
|
|
1184
|
+
const ownerLabel = t('lane.owner', { owner: assigneeDisplay });
|
|
1185
|
+
const qualityScore = typeof mr.latestScore === 'number' ? mr.latestScore : null;
|
|
1186
|
+
const qualityProgress = getQualityProgress(qualityScore, QUALITY_TARGET_SCORE);
|
|
1187
|
+
const qualityTarget = qualityProgress.qualityTarget;
|
|
1188
|
+
const qualityDelta = qualityProgress.targetDelta;
|
|
1189
|
+
const qualityState = qualityScore === null
|
|
1190
|
+
? 'unknown'
|
|
1191
|
+
: qualityScore >= QUALITY_TARGET_SCORE
|
|
1192
|
+
? 'excellent'
|
|
1193
|
+
: qualityScore >= 6
|
|
1194
|
+
? 'warning'
|
|
1195
|
+
: 'critical';
|
|
1196
|
+
const qualityLabel = qualityScore === null
|
|
1197
|
+
? t('quality.notAvailable')
|
|
1198
|
+
: qualityScore >= 10
|
|
1199
|
+
? t('quality.perfect')
|
|
1200
|
+
: qualityScore >= QUALITY_TARGET_SCORE
|
|
1201
|
+
? t('quality.onTarget')
|
|
1202
|
+
: qualityScore >= 6
|
|
1203
|
+
? t('quality.belowTarget')
|
|
1204
|
+
: t('quality.lovableQuality');
|
|
1205
|
+
const deltaLabel = qualityProgress.targetDeltaLabel;
|
|
1206
|
+
const qualityTrend = getQualityTrend(mr);
|
|
1207
|
+
const qualityTrendLabel = qualityTrend.direction === 'up'
|
|
1208
|
+
? t('quality.trendUp', { delta: qualityTrend.label })
|
|
1209
|
+
: qualityTrend.direction === 'down'
|
|
1210
|
+
? t('quality.trendDown', { delta: qualityTrend.label })
|
|
1211
|
+
: qualityTrend.direction === 'flat'
|
|
1212
|
+
? t('quality.trendFlat')
|
|
1213
|
+
: t('quality.trendUnknown');
|
|
1214
|
+
const qualityTrendIcon = qualityTrend.direction === 'up'
|
|
1215
|
+
? 'trending-up'
|
|
1216
|
+
: qualityTrend.direction === 'down'
|
|
1217
|
+
? 'trending-down'
|
|
1218
|
+
: qualityTrend.direction === 'flat'
|
|
1219
|
+
? 'minus'
|
|
1220
|
+
: 'circle-help';
|
|
1221
|
+
|
|
1222
|
+
return `
|
|
1223
|
+
<div class="now-lane">
|
|
1224
|
+
<div class="now-lane-copy">
|
|
1225
|
+
<span class="now-lane-kicker">${t('lane.nowKicker')}</span>
|
|
1226
|
+
<div class="now-lane-title">${mrPrefix}${mr.mrNumber} - ${escapeHtml(String(mr.title ?? ''))}</div>
|
|
1227
|
+
<div class="now-lane-meta">${t('lane.nowMeta', { count: mr.openThreads })}</div>
|
|
1228
|
+
<div class="now-lane-owner">${escapeHtml(ownerLabel)}</div>
|
|
1229
|
+
</div>
|
|
1230
|
+
<div class="now-lane-quality ${qualityState}">
|
|
1231
|
+
<span class="quality-kicker">${t('quality.kicker')}</span>
|
|
1232
|
+
<div class="quality-main">
|
|
1233
|
+
<span class="quality-score">${qualityScore !== null ? qualityScore.toFixed(1) : '-'}</span>
|
|
1234
|
+
<span class="quality-over">/10</span>
|
|
1235
|
+
</div>
|
|
1236
|
+
<div class="quality-target">${t('quality.target', { target: qualityTarget })} ${deltaLabel ? `(${deltaLabel})` : ''}</div>
|
|
1237
|
+
<div class="quality-progress">
|
|
1238
|
+
<div class="quality-progress-track">
|
|
1239
|
+
<div class="quality-progress-fill ${qualityState}" style="width: ${qualityProgress.clampedProgressPercent}%"></div>
|
|
1240
|
+
</div>
|
|
1241
|
+
<span class="quality-progress-value">${qualityProgress.progressPercent !== null ? `${qualityProgress.clampedProgressPercent.toFixed(0)}%` : '-'}</span>
|
|
1242
|
+
</div>
|
|
1243
|
+
<div class="quality-label">${qualityLabel}</div>
|
|
1244
|
+
<div class="quality-trend ${qualityTrend.direction}">
|
|
1245
|
+
<i data-lucide="${qualityTrendIcon}"></i>
|
|
1246
|
+
<span>${qualityTrendLabel}</span>
|
|
1247
|
+
</div>
|
|
1248
|
+
<div class="now-lane-actions">
|
|
1249
|
+
<button class="btn-action" onclick="triggerFollowup('${encodeURIComponent(String(mr.id ?? ''))}')"><i data-lucide="refresh-cw"></i> ${t('button.followup')}</button>
|
|
1250
|
+
<a href="${sanitizeHttpUrl(mr.url)}" target="_blank" rel="noopener noreferrer" class="btn-action open" onclick="return onUsefulLinkAction()"><i data-lucide="external-link"></i> ${t('button.open')}</a>
|
|
1251
|
+
</div>
|
|
1252
|
+
</div>
|
|
1253
|
+
</div>
|
|
1254
|
+
`;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function renderQueueLanes(queueLanesModel) {
|
|
1258
|
+
const nowLaneContent = queueLanesModel.nowLaneItem
|
|
1259
|
+
? renderNowLane(queueLanesModel.nowLaneItem)
|
|
1260
|
+
: `<div class="empty-state">${t('queueLane.emptyNow')}</div>`;
|
|
1261
|
+
const needsFixContent = queueLanesModel.needsFixItems.length > 0
|
|
1262
|
+
? queueLanesModel.needsFixItems.map((mergeRequest) => renderMrItem(mergeRequest, 'pending-fix')).join('')
|
|
1263
|
+
: `<div class="empty-state">${t('queueLane.emptyNeedsFix')}</div>`;
|
|
1264
|
+
const readyToApproveContent = queueLanesModel.readyToApproveItems.length > 0
|
|
1265
|
+
? queueLanesModel.readyToApproveItems.map((mergeRequest) => renderMrItem(mergeRequest, 'pending-approval')).join('')
|
|
1266
|
+
: `<div class="empty-state">${t('queueLane.emptyReadyToApprove')}</div>`;
|
|
1267
|
+
|
|
1268
|
+
return `
|
|
1269
|
+
<div class="queue-lanes-grid">
|
|
1270
|
+
<div class="queue-lane">
|
|
1271
|
+
<div class="queue-lane-header">
|
|
1272
|
+
<span class="queue-lane-title">${t('queueLane.now')}</span>
|
|
1273
|
+
<span class="queue-lane-count">${queueLanesModel.nowLaneCount}</span>
|
|
1274
|
+
</div>
|
|
1275
|
+
<div class="queue-lane-body">${nowLaneContent}</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
<div class="queue-lane">
|
|
1278
|
+
<div class="queue-lane-header">
|
|
1279
|
+
<span class="queue-lane-title">${t('queueLane.needsFix')}</span>
|
|
1280
|
+
<span class="queue-lane-count">${queueLanesModel.needsFixCount}</span>
|
|
1281
|
+
</div>
|
|
1282
|
+
<div class="queue-lane-body">${needsFixContent}</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
<div class="queue-lane">
|
|
1285
|
+
<div class="queue-lane-header">
|
|
1286
|
+
<span class="queue-lane-title">${t('queueLane.readyToApprove')}</span>
|
|
1287
|
+
<span class="queue-lane-count">${queueLanesModel.readyToApproveCount}</span>
|
|
1288
|
+
</div>
|
|
1289
|
+
<div class="queue-lane-body">${readyToApproveContent}</div>
|
|
1290
|
+
</div>
|
|
1291
|
+
</div>
|
|
1292
|
+
`;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
927
1295
|
function toggleMrAccordion(accordionId) {
|
|
928
1296
|
const content = document.getElementById(`mr-content-${accordionId}`);
|
|
929
1297
|
const accordion = content?.closest('.mr-item-accordion');
|
|
@@ -931,7 +1299,6 @@
|
|
|
931
1299
|
|
|
932
1300
|
const isOpen = accordion.classList.contains('open');
|
|
933
1301
|
|
|
934
|
-
// Close all other MR accordions
|
|
935
1302
|
document.querySelectorAll('.mr-item-accordion.open').forEach(el => {
|
|
936
1303
|
if (el !== accordion) {
|
|
937
1304
|
el.classList.remove('open');
|
|
@@ -950,43 +1317,40 @@
|
|
|
950
1317
|
const pendingFixSection = document.getElementById('pending-fix-section');
|
|
951
1318
|
const pendingFixEl = document.getElementById('pending-fix-reviews');
|
|
952
1319
|
const pendingApprovalSection = document.getElementById('pending-approval-section');
|
|
953
|
-
const pendingApprovalEl = document.getElementById('pending-approval-reviews');
|
|
954
1320
|
const pendingFixCount = document.getElementById('pending-fix-count');
|
|
955
1321
|
const pendingApprovalCount = document.getElementById('pending-approval-count');
|
|
956
|
-
const mrLabel = getMrLabel();
|
|
957
1322
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1323
|
+
pendingFixSection.classList.remove('hidden');
|
|
1324
|
+
const rankedPendingFix = rankPendingFixForNowLane(currentData.pendingFix);
|
|
1325
|
+
const queueLanesModel = buildQueueLanesModel(rankedPendingFix, currentData.pendingApproval);
|
|
1326
|
+
sessionMetrics = updatePriorityItemTracking(sessionMetrics, {
|
|
1327
|
+
nowLaneItemId: queueLanesModel.nowLaneItem?.id ?? null,
|
|
1328
|
+
pendingFixIds: rankedPendingFix.map((mergeRequest) => String(mergeRequest.id)),
|
|
1329
|
+
nowMs: Date.now(),
|
|
1330
|
+
});
|
|
1331
|
+
updateSessionMetricsUI();
|
|
1332
|
+
if (queueLanesModel.nowLaneCount + queueLanesModel.needsFixCount + queueLanesModel.readyToApproveCount === 0) {
|
|
1333
|
+
pendingFixEl.innerHTML = `<div class="empty-state">${loadingState.mrTracking ? t('loading.section') : t('empty.pendingFix')}</div>`;
|
|
962
1334
|
} else {
|
|
963
|
-
|
|
964
|
-
pendingFixEl.innerHTML = currentData.pendingFix.map(mr => renderMrItem(mr, 'pending-fix')).join('');
|
|
965
|
-
pendingFixCount.textContent = currentData.pendingFix.length;
|
|
966
|
-
pendingFixCount.classList.remove('hidden');
|
|
1335
|
+
pendingFixEl.innerHTML = renderQueueLanes(queueLanesModel);
|
|
967
1336
|
}
|
|
1337
|
+
pendingFixCount.textContent = String(queueLanesModel.nowLaneCount + queueLanesModel.needsFixCount + queueLanesModel.readyToApproveCount);
|
|
1338
|
+
pendingFixCount.classList.remove('hidden');
|
|
968
1339
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
pendingApprovalSection.classList.add('hidden');
|
|
972
|
-
pendingApprovalCount.classList.add('hidden');
|
|
973
|
-
} else {
|
|
974
|
-
pendingApprovalSection.classList.remove('hidden');
|
|
975
|
-
pendingApprovalEl.innerHTML = currentData.pendingApproval.map(mr => renderMrItem(mr, 'pending-approval')).join('');
|
|
976
|
-
pendingApprovalCount.textContent = currentData.pendingApproval.length;
|
|
977
|
-
pendingApprovalCount.classList.remove('hidden');
|
|
978
|
-
}
|
|
1340
|
+
pendingApprovalSection.classList.add('hidden');
|
|
1341
|
+
pendingApprovalCount.classList.add('hidden');
|
|
979
1342
|
|
|
980
1343
|
refreshIcons();
|
|
981
1344
|
}
|
|
982
1345
|
|
|
983
1346
|
async function fetchMrTracking() {
|
|
1347
|
+
setLoadingFlag('mrTracking', true);
|
|
984
1348
|
if (!currentProjectPath) {
|
|
985
1349
|
currentData.pendingFix = [];
|
|
986
1350
|
currentData.pendingApproval = [];
|
|
987
|
-
// Hide sections when no project loaded
|
|
988
1351
|
document.getElementById('pending-fix-section').classList.add('hidden');
|
|
989
1352
|
document.getElementById('pending-approval-section').classList.add('hidden');
|
|
1353
|
+
setLoadingFlag('mrTracking', false);
|
|
990
1354
|
return;
|
|
991
1355
|
}
|
|
992
1356
|
|
|
@@ -1000,10 +1364,14 @@
|
|
|
1000
1364
|
updateMrTrackingUI();
|
|
1001
1365
|
} catch (error) {
|
|
1002
1366
|
console.error('Error fetching MR tracking:', error);
|
|
1367
|
+
} finally {
|
|
1368
|
+
setLoadingFlag('mrTracking', false);
|
|
1003
1369
|
}
|
|
1004
1370
|
}
|
|
1005
1371
|
|
|
1006
|
-
async function triggerFollowup(
|
|
1372
|
+
async function triggerFollowup(encodedMrId) {
|
|
1373
|
+
const mrId = safeDecodeURIComponent(encodedMrId);
|
|
1374
|
+
trackUsefulAction('followup');
|
|
1007
1375
|
try {
|
|
1008
1376
|
const response = await fetch(`${API_URL}/api/mr-tracking/followup`, {
|
|
1009
1377
|
method: 'POST',
|
|
@@ -1013,16 +1381,18 @@
|
|
|
1013
1381
|
const data = await response.json();
|
|
1014
1382
|
if (data.success) {
|
|
1015
1383
|
console.log('Followup triggered for MR:', mrId);
|
|
1384
|
+
showToast(t('notify.followupRequested', { mrNumber: String(data.mrNumber ?? '?') }), 'info');
|
|
1016
1385
|
} else {
|
|
1017
|
-
alert('
|
|
1386
|
+
alert(t('error.triggerFollowup') + ': ' + data.error);
|
|
1018
1387
|
}
|
|
1019
1388
|
} catch (error) {
|
|
1020
1389
|
console.error('Error triggering followup:', error);
|
|
1021
|
-
alert('
|
|
1390
|
+
alert(t('error.triggerFollowup'));
|
|
1022
1391
|
}
|
|
1023
1392
|
}
|
|
1024
1393
|
|
|
1025
|
-
async function toggleAutoFollowup(
|
|
1394
|
+
async function toggleAutoFollowup(encodedMrId, enabled) {
|
|
1395
|
+
const mrId = safeDecodeURIComponent(encodedMrId);
|
|
1026
1396
|
try {
|
|
1027
1397
|
const response = await fetch(`${API_URL}/api/mr-tracking/auto-followup`, {
|
|
1028
1398
|
method: 'POST',
|
|
@@ -1031,16 +1401,18 @@
|
|
|
1031
1401
|
});
|
|
1032
1402
|
const data = await response.json();
|
|
1033
1403
|
if (!data.success) {
|
|
1034
|
-
alert('
|
|
1404
|
+
alert(t('error.toggleAutoFollowup') + ': ' + data.error);
|
|
1035
1405
|
}
|
|
1036
1406
|
} catch (error) {
|
|
1037
1407
|
console.error('Error toggling auto-followup:', error);
|
|
1038
|
-
alert('
|
|
1408
|
+
alert(t('error.toggleAutoFollowup'));
|
|
1039
1409
|
}
|
|
1040
1410
|
}
|
|
1041
1411
|
|
|
1042
|
-
async function approveMr(
|
|
1043
|
-
|
|
1412
|
+
async function approveMr(encodedMrId) {
|
|
1413
|
+
const mrId = safeDecodeURIComponent(encodedMrId);
|
|
1414
|
+
if (!confirm(t('confirm.approveMr', { label: getMrLabel() }))) return;
|
|
1415
|
+
trackUsefulAction('approve');
|
|
1044
1416
|
|
|
1045
1417
|
try {
|
|
1046
1418
|
const response = await fetch(`${API_URL}/api/mr-tracking/approve`, {
|
|
@@ -1050,30 +1422,29 @@
|
|
|
1050
1422
|
});
|
|
1051
1423
|
const data = await response.json();
|
|
1052
1424
|
if (data.success) {
|
|
1053
|
-
// Remove from pending approval list
|
|
1054
1425
|
currentData.pendingApproval = currentData.pendingApproval.filter(mr => mr.id !== mrId);
|
|
1055
1426
|
updateMrTrackingUI();
|
|
1056
1427
|
} else {
|
|
1057
|
-
alert('
|
|
1428
|
+
alert(t('error.approveMr') + ': ' + data.error);
|
|
1058
1429
|
}
|
|
1059
1430
|
} catch (error) {
|
|
1060
1431
|
console.error('Error approving MR:', error);
|
|
1061
|
-
alert('
|
|
1432
|
+
alert(t('error.approveMr'));
|
|
1062
1433
|
}
|
|
1063
1434
|
}
|
|
1064
1435
|
|
|
1065
1436
|
async function syncGitLabThreads() {
|
|
1066
1437
|
if (!currentProjectPath) {
|
|
1067
|
-
alert('
|
|
1438
|
+
alert(t('error.projectNotLoaded'));
|
|
1068
1439
|
return;
|
|
1069
1440
|
}
|
|
1441
|
+
trackUsefulAction('syncThreads');
|
|
1070
1442
|
|
|
1071
1443
|
const btn = document.getElementById('sync-threads-btn');
|
|
1072
|
-
const
|
|
1444
|
+
const iconEl = btn.querySelector('i');
|
|
1073
1445
|
|
|
1074
|
-
// Add spinning animation
|
|
1075
1446
|
btn.disabled = true;
|
|
1076
|
-
|
|
1447
|
+
iconEl.classList.add('spinning');
|
|
1077
1448
|
|
|
1078
1449
|
try {
|
|
1079
1450
|
const response = await fetch(`${API_URL}/api/mr-tracking/sync`, {
|
|
@@ -1083,18 +1454,17 @@
|
|
|
1083
1454
|
});
|
|
1084
1455
|
const data = await response.json();
|
|
1085
1456
|
if (data.success) {
|
|
1086
|
-
// Refresh MR tracking data
|
|
1087
1457
|
await fetchMrTracking();
|
|
1088
1458
|
console.log('Threads synchronized successfully');
|
|
1089
1459
|
} else {
|
|
1090
|
-
alert('
|
|
1460
|
+
alert(t('error.syncThreads') + ': ' + data.error);
|
|
1091
1461
|
}
|
|
1092
1462
|
} catch (error) {
|
|
1093
1463
|
console.error('Error syncing threads:', error);
|
|
1094
|
-
alert('
|
|
1464
|
+
alert(t('error.syncThreads'));
|
|
1095
1465
|
} finally {
|
|
1096
1466
|
btn.disabled = false;
|
|
1097
|
-
|
|
1467
|
+
iconEl.classList.remove('spinning');
|
|
1098
1468
|
}
|
|
1099
1469
|
}
|
|
1100
1470
|
|
|
@@ -1127,7 +1497,7 @@
|
|
|
1127
1497
|
function connectWebSocket() {
|
|
1128
1498
|
if (ws?.readyState === WebSocket.OPEN) return;
|
|
1129
1499
|
|
|
1130
|
-
updateConnectionStatus('connecting', '
|
|
1500
|
+
updateConnectionStatus('connecting', 'status.connecting');
|
|
1131
1501
|
|
|
1132
1502
|
try {
|
|
1133
1503
|
ws = new WebSocket(WS_URL);
|
|
@@ -1135,7 +1505,7 @@
|
|
|
1135
1505
|
ws.onopen = () => {
|
|
1136
1506
|
wsConnected = true;
|
|
1137
1507
|
reconnectAttempts = 0;
|
|
1138
|
-
updateConnectionStatus('online', '
|
|
1508
|
+
updateConnectionStatus('online', 'connection.online');
|
|
1139
1509
|
};
|
|
1140
1510
|
|
|
1141
1511
|
ws.onmessage = (event) => {
|
|
@@ -1147,8 +1517,8 @@
|
|
|
1147
1517
|
console.log(`[WS] ${message.type}: ${message.activeReviews?.length || 0} active reviews`, message.activeReviews);
|
|
1148
1518
|
currentData.activeReviews = message.activeReviews || [];
|
|
1149
1519
|
currentData.recentReviews = message.recentReviews || [];
|
|
1520
|
+
dispatchReviewNotifications(currentData.activeReviews, currentData.recentReviews);
|
|
1150
1521
|
updateUI();
|
|
1151
|
-
// Also refresh MR tracking when state changes
|
|
1152
1522
|
if (message.type === 'state') {
|
|
1153
1523
|
fetchMrTracking();
|
|
1154
1524
|
}
|
|
@@ -1170,7 +1540,7 @@
|
|
|
1170
1540
|
ws.onclose = () => {
|
|
1171
1541
|
wsConnected = false;
|
|
1172
1542
|
ws = null;
|
|
1173
|
-
updateConnectionStatus('offline', '
|
|
1543
|
+
updateConnectionStatus('offline', 'connection.offline');
|
|
1174
1544
|
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
|
1175
1545
|
reconnectAttempts++;
|
|
1176
1546
|
setTimeout(connectWebSocket, RECONNECT_DELAY);
|
|
@@ -1192,22 +1562,27 @@
|
|
|
1192
1562
|
}, 30000);
|
|
1193
1563
|
|
|
1194
1564
|
async function fetchStatus() {
|
|
1565
|
+
setLoadingFlag('status', true);
|
|
1195
1566
|
try {
|
|
1196
1567
|
const response = await fetch(`${API_URL}/api/status`);
|
|
1197
1568
|
const data = await response.json();
|
|
1198
1569
|
currentData.activeReviews = data.jobs?.active || [];
|
|
1199
1570
|
currentData.recentReviews = data.jobs?.recent || [];
|
|
1200
|
-
|
|
1571
|
+
dispatchReviewNotifications(currentData.activeReviews, currentData.recentReviews);
|
|
1572
|
+
if (!wsConnected) updateConnectionStatus('online', 'connection.onlinePolling');
|
|
1201
1573
|
updateUI();
|
|
1202
1574
|
} catch (error) {
|
|
1203
1575
|
if (!wsConnected) {
|
|
1204
|
-
updateConnectionStatus('offline', '
|
|
1576
|
+
updateConnectionStatus('offline', 'connection.offline');
|
|
1205
1577
|
document.getElementById('running-count').textContent = '-';
|
|
1206
1578
|
document.getElementById('queued-count').textContent = '-';
|
|
1207
1579
|
document.getElementById('completed-count').textContent = '-';
|
|
1208
|
-
document.getElementById('active-reviews').innerHTML =
|
|
1209
|
-
document.getElementById('recent-reviews').innerHTML =
|
|
1580
|
+
document.getElementById('active-reviews').innerHTML = `<div class="empty-state">${t('empty.serverNotAccessible')}</div>`;
|
|
1581
|
+
document.getElementById('recent-reviews').innerHTML = `<div class="empty-state">${t('empty.serverNotAccessible')}</div>`;
|
|
1210
1582
|
}
|
|
1583
|
+
} finally {
|
|
1584
|
+
hasLoadedStatusOnce = true;
|
|
1585
|
+
setLoadingFlag('status', false);
|
|
1211
1586
|
}
|
|
1212
1587
|
}
|
|
1213
1588
|
|
|
@@ -1240,9 +1615,48 @@
|
|
|
1240
1615
|
}
|
|
1241
1616
|
}
|
|
1242
1617
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1618
|
+
async function loadLanguageSetting() {
|
|
1619
|
+
try {
|
|
1620
|
+
const response = await fetch(`${API_URL}/api/settings`);
|
|
1621
|
+
const data = await response.json();
|
|
1622
|
+
const select = document.getElementById('language-select');
|
|
1623
|
+
if (select && data.language) {
|
|
1624
|
+
select.value = data.language;
|
|
1625
|
+
setLanguage(data.language);
|
|
1626
|
+
document.documentElement.lang = data.language;
|
|
1627
|
+
renderStaticLabels();
|
|
1628
|
+
}
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
console.error('Error loading language setting:', error);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
async function changeLanguage(language) {
|
|
1635
|
+
setLanguage(language);
|
|
1636
|
+
document.documentElement.lang = language;
|
|
1637
|
+
renderStaticLabels();
|
|
1638
|
+
// Re-render all dynamic content
|
|
1639
|
+
updateUI();
|
|
1640
|
+
updateLogs();
|
|
1641
|
+
updateReviewFilesUI();
|
|
1642
|
+
updateMrTrackingUI();
|
|
1643
|
+
updateGitCliUI();
|
|
1644
|
+
// Also persist on server
|
|
1645
|
+
try {
|
|
1646
|
+
const response = await fetch(`${API_URL}/api/settings/language`, {
|
|
1647
|
+
method: 'POST',
|
|
1648
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1649
|
+
body: JSON.stringify({ language })
|
|
1650
|
+
});
|
|
1651
|
+
const data = await response.json();
|
|
1652
|
+
if (data.success) {
|
|
1653
|
+
console.log('Language changed to:', language);
|
|
1654
|
+
}
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
console.error('Error changing language:', error);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1246
1660
|
let currentProjectConfig = null;
|
|
1247
1661
|
let currentProjectPath = null;
|
|
1248
1662
|
|
|
@@ -1260,11 +1674,8 @@
|
|
|
1260
1674
|
|
|
1261
1675
|
function addProjectToHistory(path) {
|
|
1262
1676
|
const projects = getStoredProjects();
|
|
1263
|
-
// Remove if already exists (to move to top)
|
|
1264
1677
|
const filtered = projects.filter(p => p !== path);
|
|
1265
|
-
// Add to beginning
|
|
1266
1678
|
filtered.unshift(path);
|
|
1267
|
-
// Keep max 10 projects
|
|
1268
1679
|
saveProjects(filtered.slice(0, 10));
|
|
1269
1680
|
updateProjectSelect();
|
|
1270
1681
|
}
|
|
@@ -1280,7 +1691,7 @@
|
|
|
1280
1691
|
const projects = getStoredProjects();
|
|
1281
1692
|
const current = localStorage.getItem(STORAGE_KEY_CURRENT) || '';
|
|
1282
1693
|
|
|
1283
|
-
select.innerHTML =
|
|
1694
|
+
select.innerHTML = `<option value="">${t('project.selectPlaceholder')}</option>`;
|
|
1284
1695
|
for (const path of projects) {
|
|
1285
1696
|
const shortName = path.split('/').slice(-2).join('/');
|
|
1286
1697
|
const option = document.createElement('option');
|
|
@@ -1302,11 +1713,10 @@
|
|
|
1302
1713
|
async function loadProjectConfig() {
|
|
1303
1714
|
const input = document.getElementById('project-path-input');
|
|
1304
1715
|
const select = document.getElementById('project-select');
|
|
1305
|
-
// Prioritize input field, fallback to select
|
|
1306
1716
|
const projectPath = input.value.trim() || select.value;
|
|
1307
1717
|
|
|
1308
1718
|
if (!projectPath) {
|
|
1309
|
-
showConfigStatus('
|
|
1719
|
+
showConfigStatus(t('error.selectOrEnterPath'), 'error');
|
|
1310
1720
|
return;
|
|
1311
1721
|
}
|
|
1312
1722
|
|
|
@@ -1317,7 +1727,7 @@
|
|
|
1317
1727
|
const status = document.getElementById('config-status');
|
|
1318
1728
|
const info = document.getElementById('config-info');
|
|
1319
1729
|
|
|
1320
|
-
showConfigStatus('
|
|
1730
|
+
showConfigStatus(t('status.loading'), 'loading');
|
|
1321
1731
|
info.classList.add('hidden');
|
|
1322
1732
|
|
|
1323
1733
|
try {
|
|
@@ -1328,19 +1738,16 @@
|
|
|
1328
1738
|
currentProjectConfig = data.config;
|
|
1329
1739
|
currentProjectPath = projectPath;
|
|
1330
1740
|
|
|
1331
|
-
// Save to history and set as current
|
|
1332
1741
|
addProjectToHistory(projectPath);
|
|
1333
1742
|
localStorage.setItem(STORAGE_KEY_CURRENT, projectPath);
|
|
1334
1743
|
|
|
1335
|
-
// Update select to show current project
|
|
1336
1744
|
document.getElementById('project-select').value = projectPath;
|
|
1337
1745
|
document.getElementById('project-path-input').value = '';
|
|
1338
1746
|
|
|
1339
1747
|
const shortName = projectPath.split('/').slice(-2).join('/');
|
|
1340
|
-
showConfigStatus(`<i data-lucide="check-circle"></i> ${shortName}`, 'success');
|
|
1748
|
+
showConfigStatus(`<i data-lucide="check-circle"></i> ${escapeHtml(shortName)}`, 'success');
|
|
1341
1749
|
refreshIcons();
|
|
1342
1750
|
|
|
1343
|
-
// Update model selector if defaultModel is set
|
|
1344
1751
|
if (data.config.defaultModel) {
|
|
1345
1752
|
const modelSelect = document.getElementById('model-select');
|
|
1346
1753
|
if (modelSelect) {
|
|
@@ -1349,34 +1756,30 @@
|
|
|
1349
1756
|
}
|
|
1350
1757
|
}
|
|
1351
1758
|
|
|
1352
|
-
// Update active platform and check CLI
|
|
1353
1759
|
activePlatform = data.config.gitlab ? 'gitlab' : (data.config.github ? 'github' : null);
|
|
1354
1760
|
updateGitCliUI();
|
|
1355
1761
|
|
|
1356
|
-
// Reload reviews, stats, and MR tracking for this project
|
|
1357
1762
|
fetchReviewFiles();
|
|
1358
1763
|
fetchProjectStats();
|
|
1359
1764
|
fetchMrTracking();
|
|
1360
1765
|
|
|
1361
|
-
// Show stats section when project is loaded
|
|
1362
1766
|
document.getElementById('stats-section').classList.remove('hidden');
|
|
1363
1767
|
|
|
1364
|
-
// Display config info (only show active platform)
|
|
1365
1768
|
const platformIcon = activePlatform === 'gitlab' ? '<i data-lucide="gitlab"></i> GitLab' : '<i data-lucide="github"></i> GitHub';
|
|
1366
1769
|
info.innerHTML = `
|
|
1367
1770
|
<span>${platformIcon}</span>
|
|
1368
|
-
<span><i data-lucide="bot"></i> ${data.config.defaultModel || '
|
|
1369
|
-
<span><i data-lucide="file-text"></i> ${data.config.reviewSkill}</span>
|
|
1370
|
-
<span><i data-lucide="refresh-cw"></i> ${data.config.reviewFollowupSkill}</span>
|
|
1771
|
+
<span><i data-lucide="bot"></i> ${escapeHtml(String(data.config.defaultModel || t('status.undefined')))}</span>
|
|
1772
|
+
<span><i data-lucide="file-text"></i> ${escapeHtml(String(data.config.reviewSkill ?? ''))}</span>
|
|
1773
|
+
<span><i data-lucide="refresh-cw"></i> ${escapeHtml(String(data.config.reviewFollowupSkill ?? ''))}</span>
|
|
1371
1774
|
`;
|
|
1372
1775
|
info.classList.remove('hidden');
|
|
1373
1776
|
refreshIcons();
|
|
1374
1777
|
} else {
|
|
1375
|
-
showConfigStatus('<i data-lucide="x-circle"></i> ' + data.error, 'error');
|
|
1778
|
+
showConfigStatus('<i data-lucide="x-circle"></i> ' + escapeHtml(String(data.error ?? '')), 'error');
|
|
1376
1779
|
refreshIcons();
|
|
1377
1780
|
}
|
|
1378
1781
|
} catch (error) {
|
|
1379
|
-
showConfigStatus(
|
|
1782
|
+
showConfigStatus(`<i data-lucide="x-circle"></i> ${t('error.loadingConfig')}`, 'error');
|
|
1380
1783
|
refreshIcons();
|
|
1381
1784
|
console.error('Error loading project config:', error);
|
|
1382
1785
|
}
|
|
@@ -1394,56 +1797,43 @@
|
|
|
1394
1797
|
const select = document.getElementById('project-select');
|
|
1395
1798
|
const path = select.value;
|
|
1396
1799
|
if (!path) {
|
|
1397
|
-
showConfigStatus('
|
|
1800
|
+
showConfigStatus(t('project.noProjectSelected'), 'error');
|
|
1398
1801
|
return;
|
|
1399
1802
|
}
|
|
1400
|
-
|
|
1803
|
+
const shortName = path.split('/').slice(-2).join('/');
|
|
1804
|
+
if (confirm(t('confirm.removeProject', { name: shortName }))) {
|
|
1401
1805
|
removeProjectFromHistory(path);
|
|
1402
1806
|
localStorage.removeItem(STORAGE_KEY_CURRENT);
|
|
1403
1807
|
currentProjectPath = null;
|
|
1404
1808
|
currentProjectConfig = null;
|
|
1405
1809
|
document.getElementById('config-info').classList.add('hidden');
|
|
1406
|
-
showConfigStatus('
|
|
1810
|
+
showConfigStatus(t('project.removed'), 'success');
|
|
1407
1811
|
}
|
|
1408
1812
|
}
|
|
1409
1813
|
|
|
1410
1814
|
function initProjectLoader() {
|
|
1411
1815
|
updateProjectSelect();
|
|
1412
|
-
// Auto-load last project
|
|
1413
1816
|
const lastProject = localStorage.getItem(STORAGE_KEY_CURRENT);
|
|
1414
1817
|
if (lastProject) {
|
|
1415
1818
|
loadProjectConfigFromPath(lastProject);
|
|
1416
1819
|
} else {
|
|
1417
|
-
// No project loaded - show empty state
|
|
1418
1820
|
document.getElementById('recent-reviews').innerHTML =
|
|
1419
|
-
|
|
1821
|
+
`<div class="empty-state">${t('empty.reviewsNoProject')}</div>`;
|
|
1420
1822
|
document.getElementById('project-stats').innerHTML =
|
|
1421
|
-
|
|
1823
|
+
`<div class="empty-state">${t('empty.statsNoProject')}</div>`;
|
|
1422
1824
|
}
|
|
1423
1825
|
}
|
|
1424
1826
|
|
|
1425
|
-
// Lucide icon helper - call after dynamic content is added
|
|
1426
|
-
function refreshIcons() {
|
|
1427
|
-
lucide.createIcons();
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
// Icon helper for dynamic HTML
|
|
1431
|
-
function icon(name, className = '') {
|
|
1432
|
-
return `<i data-lucide="${name}" class="${className}"></i>`;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
// Platform-aware MR/PR label
|
|
1436
1827
|
function getMrLabel(platform = activePlatform) {
|
|
1437
1828
|
return platform === 'github' ? 'PR' : 'MR';
|
|
1438
1829
|
}
|
|
1439
1830
|
|
|
1440
|
-
// Cancel review modal state
|
|
1441
1831
|
let cancelModalJobId = null;
|
|
1442
1832
|
|
|
1443
|
-
function showCancelModal(
|
|
1444
|
-
cancelModalJobId =
|
|
1445
|
-
const typeLabel = jobType === 'followup' ? 'followup' : 'review';
|
|
1446
|
-
document.getElementById('cancel-modal-title').textContent =
|
|
1833
|
+
function showCancelModal(encodedJobId, mrNumber, jobType) {
|
|
1834
|
+
cancelModalJobId = safeDecodeURIComponent(encodedJobId);
|
|
1835
|
+
const typeLabel = jobType === 'followup' ? t('review.type.followup').toLowerCase() : t('review.type.review').toLowerCase();
|
|
1836
|
+
document.getElementById('cancel-modal-title').textContent = t('modal.cancel.title', { type: typeLabel, label: getMrLabel(), number: mrNumber });
|
|
1447
1837
|
const modal = document.getElementById('cancel-modal');
|
|
1448
1838
|
modal.classList.remove('hidden');
|
|
1449
1839
|
requestAnimationFrame(() => modal.classList.add('visible'));
|
|
@@ -1459,6 +1849,7 @@
|
|
|
1459
1849
|
|
|
1460
1850
|
async function confirmCancelReview() {
|
|
1461
1851
|
if (!cancelModalJobId) return;
|
|
1852
|
+
trackUsefulAction('cancelReview');
|
|
1462
1853
|
|
|
1463
1854
|
const jobId = cancelModalJobId;
|
|
1464
1855
|
closeCancelModal();
|
|
@@ -1477,17 +1868,17 @@
|
|
|
1477
1868
|
if (data.success) {
|
|
1478
1869
|
const card = document.querySelector(`.review-item[data-job-id="${jobId}"]`);
|
|
1479
1870
|
if (card) card.remove();
|
|
1480
|
-
showToast('
|
|
1871
|
+
showToast(t('success.reviewCancelled'), 'success');
|
|
1481
1872
|
fetchStatus();
|
|
1482
1873
|
} else if (data.status === 'already-completed') {
|
|
1483
|
-
showToast('
|
|
1874
|
+
showToast(t('success.reviewAlreadyCompleted'), 'info');
|
|
1484
1875
|
fetchStatus();
|
|
1485
1876
|
} else {
|
|
1486
|
-
showToast(data.error || '
|
|
1877
|
+
showToast(data.error || t('error.cancelReview'), 'error');
|
|
1487
1878
|
}
|
|
1488
1879
|
} catch (error) {
|
|
1489
1880
|
console.error('Error cancelling review:', error);
|
|
1490
|
-
showToast('
|
|
1881
|
+
showToast(t('error.cancelReview'), 'error');
|
|
1491
1882
|
}
|
|
1492
1883
|
}
|
|
1493
1884
|
|
|
@@ -1506,12 +1897,202 @@
|
|
|
1506
1897
|
}, 3000);
|
|
1507
1898
|
}
|
|
1508
1899
|
|
|
1900
|
+
function renderStaticLabels() {
|
|
1901
|
+
// Header buttons
|
|
1902
|
+
const checkClaudeSpan = document.getElementById('i18n-check-claude');
|
|
1903
|
+
if (checkClaudeSpan) checkClaudeSpan.textContent = t('header.checkClaude');
|
|
1904
|
+
|
|
1905
|
+
const logsBtnSpan = document.getElementById('i18n-logs-btn');
|
|
1906
|
+
if (logsBtnSpan) logsBtnSpan.textContent = logsVisible ? t('header.hideLogs') : t('header.logs');
|
|
1907
|
+
|
|
1908
|
+
const serverStatusSpan = document.getElementById('i18n-server-status');
|
|
1909
|
+
if (serverStatusSpan) serverStatusSpan.textContent = t('status.connecting');
|
|
1910
|
+
|
|
1911
|
+
// Cards
|
|
1912
|
+
const cardRunning = document.getElementById('i18n-card-running');
|
|
1913
|
+
if (cardRunning) cardRunning.textContent = t('card.running');
|
|
1914
|
+
|
|
1915
|
+
const cardQueued = document.getElementById('i18n-card-queued');
|
|
1916
|
+
if (cardQueued) cardQueued.textContent = t('card.queued');
|
|
1917
|
+
|
|
1918
|
+
const cardCompleted = document.getElementById('i18n-card-completed');
|
|
1919
|
+
if (cardCompleted) cardCompleted.textContent = t('card.completed');
|
|
1920
|
+
|
|
1921
|
+
const cardClaudeCli = document.getElementById('i18n-card-claude-cli');
|
|
1922
|
+
if (cardClaudeCli) cardClaudeCli.textContent = t('card.claudeCli');
|
|
1923
|
+
|
|
1924
|
+
const claudeChecking = document.getElementById('i18n-claude-checking');
|
|
1925
|
+
if (claudeChecking) claudeChecking.textContent = t('status.checking');
|
|
1926
|
+
|
|
1927
|
+
const gitLoadProject = document.getElementById('i18n-git-load-project');
|
|
1928
|
+
if (gitLoadProject) gitLoadProject.textContent = t('status.loadProject');
|
|
1929
|
+
|
|
1930
|
+
const gitCliLabel = document.getElementById('git-cli-label');
|
|
1931
|
+
if (gitCliLabel && !activePlatform) gitCliLabel.textContent = t('card.gitCli');
|
|
1932
|
+
|
|
1933
|
+
const cardModel = document.getElementById('i18n-card-model');
|
|
1934
|
+
if (cardModel) cardModel.textContent = t('card.model');
|
|
1935
|
+
|
|
1936
|
+
const cardLanguage = document.getElementById('i18n-card-language');
|
|
1937
|
+
if (cardLanguage) cardLanguage.textContent = t('card.language');
|
|
1938
|
+
|
|
1939
|
+
const stripNow = document.getElementById('i18n-strip-now');
|
|
1940
|
+
if (stripNow) stripNow.textContent = t('strip.now');
|
|
1941
|
+
const stripNowMeta = document.getElementById('i18n-strip-now-meta');
|
|
1942
|
+
if (stripNowMeta) stripNowMeta.textContent = t('strip.nowMeta');
|
|
1943
|
+
|
|
1944
|
+
const stripNext = document.getElementById('i18n-strip-next');
|
|
1945
|
+
if (stripNext) stripNext.textContent = t('strip.next');
|
|
1946
|
+
const stripNextMeta = document.getElementById('i18n-strip-next-meta');
|
|
1947
|
+
if (stripNextMeta) stripNextMeta.textContent = t('strip.nextMeta');
|
|
1948
|
+
|
|
1949
|
+
const stripBlocked = document.getElementById('i18n-strip-blocked');
|
|
1950
|
+
if (stripBlocked) stripBlocked.textContent = t('strip.blocked');
|
|
1951
|
+
const stripBlockedMeta = document.getElementById('i18n-strip-blocked-meta');
|
|
1952
|
+
if (stripBlockedMeta) stripBlockedMeta.textContent = t('strip.blockedMeta');
|
|
1953
|
+
applyFocusStripMode();
|
|
1954
|
+
|
|
1955
|
+
const loadingDataLabel = document.getElementById('i18n-loading-data');
|
|
1956
|
+
if (loadingDataLabel) loadingDataLabel.textContent = t(getLoadingMessageKey());
|
|
1957
|
+
updateSessionMetricsUI();
|
|
1958
|
+
|
|
1959
|
+
// Model options
|
|
1960
|
+
const modelOpus = document.getElementById('i18n-model-opus');
|
|
1961
|
+
if (modelOpus) modelOpus.textContent = t('model.opus');
|
|
1962
|
+
|
|
1963
|
+
const modelSonnet = document.getElementById('i18n-model-sonnet');
|
|
1964
|
+
if (modelSonnet) modelSonnet.textContent = t('model.sonnet');
|
|
1965
|
+
|
|
1966
|
+
// Project loader
|
|
1967
|
+
const projectPlaceholder = document.getElementById('i18n-project-placeholder');
|
|
1968
|
+
if (projectPlaceholder) projectPlaceholder.textContent = t('project.selectPlaceholder');
|
|
1969
|
+
|
|
1970
|
+
const projectPathInput = document.getElementById('project-path-input');
|
|
1971
|
+
if (projectPathInput) projectPathInput.placeholder = t('project.inputPlaceholder');
|
|
1972
|
+
|
|
1973
|
+
const projectLoad = document.getElementById('i18n-project-load');
|
|
1974
|
+
if (projectLoad) projectLoad.textContent = t('project.load');
|
|
1975
|
+
|
|
1976
|
+
const removeProjectBtn = document.getElementById('remove-project-btn');
|
|
1977
|
+
if (removeProjectBtn) removeProjectBtn.title = t('project.removeTooltip');
|
|
1978
|
+
|
|
1979
|
+
// Login sections
|
|
1980
|
+
const claudeLoginTitle = document.getElementById('i18n-claude-login-title');
|
|
1981
|
+
if (claudeLoginTitle) claudeLoginTitle.textContent = t('login.claude.title');
|
|
1982
|
+
|
|
1983
|
+
const claudeLoginInstruction = document.getElementById('i18n-claude-login-instruction');
|
|
1984
|
+
if (claudeLoginInstruction) claudeLoginInstruction.textContent = t('login.claude.instruction');
|
|
1985
|
+
|
|
1986
|
+
const claudeLoginReload = document.getElementById('i18n-claude-login-reload');
|
|
1987
|
+
if (claudeLoginReload) claudeLoginReload.textContent = t('login.claude.reload');
|
|
1988
|
+
|
|
1989
|
+
const gitLoginTitle = document.getElementById('i18n-git-login-title');
|
|
1990
|
+
if (gitLoginTitle) gitLoginTitle.textContent = t('login.git.title');
|
|
1991
|
+
|
|
1992
|
+
// Section headers
|
|
1993
|
+
const sectionLogs = document.getElementById('i18n-section-logs');
|
|
1994
|
+
if (sectionLogs) sectionLogs.textContent = t('section.logs');
|
|
1995
|
+
|
|
1996
|
+
const sectionStats = document.getElementById('i18n-section-stats');
|
|
1997
|
+
if (sectionStats) sectionStats.textContent = t('section.stats');
|
|
1998
|
+
|
|
1999
|
+
const sectionActiveReviews = document.getElementById('i18n-section-active-reviews');
|
|
2000
|
+
if (sectionActiveReviews) sectionActiveReviews.textContent = t('section.activeReviews');
|
|
2001
|
+
|
|
2002
|
+
const sectionActiveFollowups = document.getElementById('i18n-section-active-followups');
|
|
2003
|
+
if (sectionActiveFollowups) sectionActiveFollowups.textContent = t('section.activeFollowups');
|
|
2004
|
+
|
|
2005
|
+
const sectionPendingFix = document.getElementById('i18n-section-pending-fix');
|
|
2006
|
+
if (sectionPendingFix) sectionPendingFix.textContent = t('section.queueLanes');
|
|
2007
|
+
|
|
2008
|
+
const sectionPendingApproval = document.getElementById('i18n-section-pending-approval');
|
|
2009
|
+
if (sectionPendingApproval) sectionPendingApproval.textContent = t('section.pendingApproval');
|
|
2010
|
+
|
|
2011
|
+
const sectionCompletedReviews = document.getElementById('i18n-section-completed-reviews');
|
|
2012
|
+
if (sectionCompletedReviews) sectionCompletedReviews.textContent = t('section.completedReviews');
|
|
2013
|
+
|
|
2014
|
+
// Empty states
|
|
2015
|
+
const emptyLogs = document.getElementById('i18n-empty-logs');
|
|
2016
|
+
if (emptyLogs) emptyLogs.textContent = t('empty.logs');
|
|
2017
|
+
|
|
2018
|
+
const emptyStats = document.getElementById('i18n-empty-stats');
|
|
2019
|
+
if (emptyStats) emptyStats.textContent = t('empty.stats');
|
|
2020
|
+
|
|
2021
|
+
const emptyActiveReviews = document.getElementById('i18n-empty-active-reviews');
|
|
2022
|
+
if (emptyActiveReviews) emptyActiveReviews.textContent = t('empty.activeReviews');
|
|
2023
|
+
|
|
2024
|
+
const emptyActiveFollowups = document.getElementById('i18n-empty-active-followups');
|
|
2025
|
+
if (emptyActiveFollowups) emptyActiveFollowups.textContent = t('empty.activeFollowups');
|
|
2026
|
+
|
|
2027
|
+
const emptyPendingFix = document.getElementById('i18n-empty-pending-fix');
|
|
2028
|
+
if (emptyPendingFix) emptyPendingFix.textContent = t('empty.pendingFix');
|
|
2029
|
+
|
|
2030
|
+
const emptyPendingApproval = document.getElementById('i18n-empty-pending-approval');
|
|
2031
|
+
if (emptyPendingApproval) emptyPendingApproval.textContent = t('empty.pendingApproval');
|
|
2032
|
+
|
|
2033
|
+
const emptyLoading = document.getElementById('i18n-empty-loading');
|
|
2034
|
+
if (emptyLoading) emptyLoading.textContent = t('status.loading');
|
|
2035
|
+
|
|
2036
|
+
// Sync threads button
|
|
2037
|
+
const syncThreadsBtn = document.getElementById('sync-threads-btn');
|
|
2038
|
+
if (syncThreadsBtn) syncThreadsBtn.title = t('button.syncThreads');
|
|
2039
|
+
|
|
2040
|
+
// Connection footer
|
|
2041
|
+
const connectionFallback = document.getElementById('i18n-connection-fallback');
|
|
2042
|
+
if (connectionFallback) connectionFallback.textContent = t('connection.fallback');
|
|
2043
|
+
|
|
2044
|
+
// Modal
|
|
2045
|
+
const modalMessage = document.getElementById('i18n-modal-message');
|
|
2046
|
+
if (modalMessage) modalMessage.textContent = t('modal.cancel.message');
|
|
2047
|
+
|
|
2048
|
+
const modalBack = document.getElementById('i18n-modal-back');
|
|
2049
|
+
if (modalBack) modalBack.textContent = t('modal.back');
|
|
2050
|
+
|
|
2051
|
+
const modalConfirm = document.getElementById('cancel-modal-confirm');
|
|
2052
|
+
if (modalConfirm) modalConfirm.textContent = t('modal.confirm');
|
|
2053
|
+
|
|
2054
|
+
// Update project select placeholder (re-render the select)
|
|
2055
|
+
updateProjectSelect();
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// Expose functions to HTML onclick handlers
|
|
2059
|
+
window.checkClaudeStatus = checkClaudeStatus;
|
|
2060
|
+
window.toggleLogs = toggleLogs;
|
|
2061
|
+
window.toggleStats = toggleStats;
|
|
2062
|
+
window.changeModel = changeModel;
|
|
2063
|
+
window.changeLanguage = changeLanguage;
|
|
2064
|
+
window.onProjectSelect = onProjectSelect;
|
|
2065
|
+
window.loadProjectConfig = loadProjectConfig;
|
|
2066
|
+
window.removeCurrentProject = removeCurrentProject;
|
|
2067
|
+
window.toggleReviewAccordion = toggleReviewAccordion;
|
|
2068
|
+
window.toggleReviewDescription = toggleReviewDescription;
|
|
2069
|
+
window.deleteReviewFile = deleteReviewFile;
|
|
2070
|
+
window.toggleMrAccordion = toggleMrAccordion;
|
|
2071
|
+
window.triggerFollowup = triggerFollowup;
|
|
2072
|
+
window.toggleAutoFollowup = toggleAutoFollowup;
|
|
2073
|
+
window.approveMr = approveMr;
|
|
2074
|
+
window.syncGitLabThreads = syncGitLabThreads;
|
|
2075
|
+
window.showCancelModal = showCancelModal;
|
|
2076
|
+
window.closeCancelModal = closeCancelModal;
|
|
2077
|
+
window.confirmCancelReview = confirmCancelReview;
|
|
2078
|
+
window.toggleFocusStripMode = toggleFocusStripMode;
|
|
2079
|
+
window.toggleSection = toggleSection;
|
|
2080
|
+
window.onUsefulLinkAction = onUsefulLinkAction;
|
|
2081
|
+
window.activateOnKeydown = activateOnKeydown;
|
|
2082
|
+
|
|
1509
2083
|
// Init
|
|
2084
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
|
2085
|
+
Notification.requestPermission().catch(() => {});
|
|
2086
|
+
}
|
|
2087
|
+
loadFocusStripMode();
|
|
2088
|
+
renderStaticLabels();
|
|
2089
|
+
updateSessionMetricsUI();
|
|
1510
2090
|
connectWebSocket();
|
|
1511
2091
|
fetchStatus();
|
|
1512
2092
|
checkClaudeStatus();
|
|
1513
2093
|
loadModelSetting();
|
|
1514
|
-
|
|
2094
|
+
loadLanguageSetting();
|
|
2095
|
+
initProjectLoader();
|
|
1515
2096
|
|
|
1516
2097
|
// Initialize Lucide icons
|
|
1517
2098
|
refreshIcons();
|
|
@@ -1521,7 +2102,7 @@
|
|
|
1521
2102
|
fetchReviewFiles();
|
|
1522
2103
|
fetchProjectStats();
|
|
1523
2104
|
fetchMrTracking();
|
|
1524
|
-
}, 30000);
|
|
2105
|
+
}, 30000);
|
|
1525
2106
|
|
|
1526
2107
|
setInterval(() => {
|
|
1527
2108
|
document.querySelectorAll('.review-item').forEach(el => {
|