maintenance_queue.templ 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. package app
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
  6. )
  7. templ MaintenanceQueue(data *maintenance.MaintenanceQueueData) {
  8. <div class="container-fluid">
  9. <!-- Header -->
  10. <div class="row mb-4">
  11. <div class="col-12">
  12. <div class="d-flex justify-content-between align-items-center">
  13. <h2 class="mb-0">
  14. <i class="fas fa-tasks me-2"></i>
  15. Maintenance Queue
  16. </h2>
  17. <div class="btn-group">
  18. <button type="button" class="btn btn-primary" onclick="triggerScan()">
  19. <i class="fas fa-search me-1"></i>
  20. Trigger Scan
  21. </button>
  22. <button type="button" class="btn btn-secondary" onclick="refreshPage()">
  23. <i class="fas fa-sync-alt me-1"></i>
  24. Refresh
  25. </button>
  26. </div>
  27. </div>
  28. </div>
  29. </div>
  30. <!-- Statistics Cards -->
  31. <div class="row mb-4">
  32. <div class="col-md-3">
  33. <div class="card border-primary">
  34. <div class="card-body text-center">
  35. <i class="fas fa-clock fa-2x text-primary mb-2"></i>
  36. <h4 class="mb-1">{fmt.Sprintf("%d", data.Stats.PendingTasks)}</h4>
  37. <p class="text-muted mb-0">Pending Tasks</p>
  38. </div>
  39. </div>
  40. </div>
  41. <div class="col-md-3">
  42. <div class="card border-warning">
  43. <div class="card-body text-center">
  44. <i class="fas fa-running fa-2x text-warning mb-2"></i>
  45. <h4 class="mb-1">{fmt.Sprintf("%d", data.Stats.RunningTasks)}</h4>
  46. <p class="text-muted mb-0">Running Tasks</p>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="col-md-3">
  51. <div class="card border-success">
  52. <div class="card-body text-center">
  53. <i class="fas fa-check-circle fa-2x text-success mb-2"></i>
  54. <h4 class="mb-1">{fmt.Sprintf("%d", data.Stats.CompletedToday)}</h4>
  55. <p class="text-muted mb-0">Completed Today</p>
  56. </div>
  57. </div>
  58. </div>
  59. <div class="col-md-3">
  60. <div class="card border-danger">
  61. <div class="card-body text-center">
  62. <i class="fas fa-exclamation-triangle fa-2x text-danger mb-2"></i>
  63. <h4 class="mb-1">{fmt.Sprintf("%d", data.Stats.FailedToday)}</h4>
  64. <p class="text-muted mb-0">Failed Today</p>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. <!-- Completed Tasks -->
  70. <div class="row mb-4">
  71. <div class="col-12">
  72. <div class="card">
  73. <div class="card-header bg-success text-white">
  74. <h5 class="mb-0">
  75. <i class="fas fa-check-circle me-2"></i>
  76. Completed Tasks
  77. </h5>
  78. </div>
  79. <div class="card-body">
  80. if data.Stats.CompletedToday == 0 && data.Stats.FailedToday == 0 {
  81. <div class="text-center text-muted py-4">
  82. <i class="fas fa-check-circle fa-3x mb-3"></i>
  83. <p>No completed maintenance tasks today</p>
  84. <small>Completed tasks will appear here after workers finish processing them</small>
  85. </div>
  86. } else {
  87. <div class="table-responsive">
  88. <table class="table table-hover">
  89. <thead>
  90. <tr>
  91. <th>Type</th>
  92. <th>Status</th>
  93. <th>Volume</th>
  94. <th>Worker</th>
  95. <th>Duration</th>
  96. <th>Completed</th>
  97. </tr>
  98. </thead>
  99. <tbody>
  100. for _, task := range data.Tasks {
  101. if string(task.Status) == "completed" || string(task.Status) == "failed" || string(task.Status) == "cancelled" {
  102. if string(task.Status) == "failed" {
  103. <tr class="table-danger clickable-row" data-task-id={task.ID} onclick="navigateToTask(this)" style="cursor: pointer;">
  104. <td>
  105. @TaskTypeIcon(task.Type)
  106. {string(task.Type)}
  107. </td>
  108. <td>@StatusBadge(task.Status)</td>
  109. <td>{fmt.Sprintf("%d", task.VolumeID)}</td>
  110. <td>
  111. if task.WorkerID != "" {
  112. <small>{task.WorkerID}</small>
  113. } else {
  114. <span class="text-muted">-</span>
  115. }
  116. </td>
  117. <td>
  118. if task.StartedAt != nil && task.CompletedAt != nil {
  119. {formatDuration(task.CompletedAt.Sub(*task.StartedAt))}
  120. } else {
  121. <span class="text-muted">-</span>
  122. }
  123. </td>
  124. <td>
  125. if task.CompletedAt != nil {
  126. {task.CompletedAt.Format("2006-01-02 15:04")}
  127. } else {
  128. <span class="text-muted">-</span>
  129. }
  130. </td>
  131. </tr>
  132. } else {
  133. <tr class="clickable-row" data-task-id={task.ID} onclick="navigateToTask(this)" style="cursor: pointer;">
  134. <td>
  135. @TaskTypeIcon(task.Type)
  136. {string(task.Type)}
  137. </td>
  138. <td>@StatusBadge(task.Status)</td>
  139. <td>{fmt.Sprintf("%d", task.VolumeID)}</td>
  140. <td>
  141. if task.WorkerID != "" {
  142. <small>{task.WorkerID}</small>
  143. } else {
  144. <span class="text-muted">-</span>
  145. }
  146. </td>
  147. <td>
  148. if task.StartedAt != nil && task.CompletedAt != nil {
  149. {formatDuration(task.CompletedAt.Sub(*task.StartedAt))}
  150. } else {
  151. <span class="text-muted">-</span>
  152. }
  153. </td>
  154. <td>
  155. if task.CompletedAt != nil {
  156. {task.CompletedAt.Format("2006-01-02 15:04")}
  157. } else {
  158. <span class="text-muted">-</span>
  159. }
  160. </td>
  161. </tr>
  162. }
  163. }
  164. }
  165. </tbody>
  166. </table>
  167. </div>
  168. }
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. <!-- Pending Tasks -->
  174. <div class="row mb-4">
  175. <div class="col-12">
  176. <div class="card">
  177. <div class="card-header bg-primary text-white">
  178. <h5 class="mb-0">
  179. <i class="fas fa-clock me-2"></i>
  180. Pending Tasks
  181. </h5>
  182. </div>
  183. <div class="card-body">
  184. if data.Stats.PendingTasks == 0 {
  185. <div class="text-center text-muted py-4">
  186. <i class="fas fa-clipboard-list fa-3x mb-3"></i>
  187. <p>No pending maintenance tasks</p>
  188. <small>Pending tasks will appear here when the system detects maintenance needs</small>
  189. </div>
  190. } else {
  191. <div class="table-responsive">
  192. <table class="table table-hover">
  193. <thead>
  194. <tr>
  195. <th>Type</th>
  196. <th>Priority</th>
  197. <th>Volume</th>
  198. <th>Server</th>
  199. <th>Reason</th>
  200. <th>Created</th>
  201. </tr>
  202. </thead>
  203. <tbody>
  204. for _, task := range data.Tasks {
  205. if string(task.Status) == "pending" {
  206. <tr class="clickable-row" data-task-id={task.ID} onclick="navigateToTask(this)" style="cursor: pointer;">
  207. <td>
  208. @TaskTypeIcon(task.Type)
  209. {string(task.Type)}
  210. </td>
  211. <td>@PriorityBadge(task.Priority)</td>
  212. <td>{fmt.Sprintf("%d", task.VolumeID)}</td>
  213. <td><small>{task.Server}</small></td>
  214. <td><small>{task.Reason}</small></td>
  215. <td>{task.CreatedAt.Format("2006-01-02 15:04")}</td>
  216. </tr>
  217. }
  218. }
  219. </tbody>
  220. </table>
  221. </div>
  222. }
  223. </div>
  224. </div>
  225. </div>
  226. </div>
  227. <!-- Active Tasks -->
  228. <div class="row mb-4">
  229. <div class="col-12">
  230. <div class="card">
  231. <div class="card-header bg-warning text-dark">
  232. <h5 class="mb-0">
  233. <i class="fas fa-running me-2"></i>
  234. Active Tasks
  235. </h5>
  236. </div>
  237. <div class="card-body">
  238. if data.Stats.RunningTasks == 0 {
  239. <div class="text-center text-muted py-4">
  240. <i class="fas fa-tasks fa-3x mb-3"></i>
  241. <p>No active maintenance tasks</p>
  242. <small>Active tasks will appear here when workers start processing them</small>
  243. </div>
  244. } else {
  245. <div class="table-responsive">
  246. <table class="table table-hover">
  247. <thead>
  248. <tr>
  249. <th>Type</th>
  250. <th>Status</th>
  251. <th>Progress</th>
  252. <th>Volume</th>
  253. <th>Worker</th>
  254. <th>Started</th>
  255. </tr>
  256. </thead>
  257. <tbody>
  258. for _, task := range data.Tasks {
  259. if string(task.Status) == "assigned" || string(task.Status) == "in_progress" {
  260. <tr class="clickable-row" data-task-id={task.ID} onclick="navigateToTask(this)" style="cursor: pointer;">
  261. <td>
  262. @TaskTypeIcon(task.Type)
  263. {string(task.Type)}
  264. </td>
  265. <td>@StatusBadge(task.Status)</td>
  266. <td>@ProgressBar(task.Progress, task.Status)</td>
  267. <td>{fmt.Sprintf("%d", task.VolumeID)}</td>
  268. <td>
  269. if task.WorkerID != "" {
  270. <small>{task.WorkerID}</small>
  271. } else {
  272. <span class="text-muted">-</span>
  273. }
  274. </td>
  275. <td>
  276. if task.StartedAt != nil {
  277. {task.StartedAt.Format("2006-01-02 15:04")}
  278. } else {
  279. <span class="text-muted">-</span>
  280. }
  281. </td>
  282. </tr>
  283. }
  284. }
  285. </tbody>
  286. </table>
  287. </div>
  288. }
  289. </div>
  290. </div>
  291. </div>
  292. </div>
  293. </div>
  294. <script>
  295. // Debug output to browser console
  296. console.log("DEBUG: Maintenance Queue Template loaded");
  297. // Auto-refresh every 10 seconds
  298. setInterval(function() {
  299. if (!document.hidden) {
  300. window.location.reload();
  301. }
  302. }, 10000);
  303. window.triggerScan = function() {
  304. console.log("triggerScan called");
  305. fetch('/api/maintenance/scan', {
  306. method: 'POST',
  307. headers: {
  308. 'Content-Type': 'application/json',
  309. }
  310. })
  311. .then(response => response.json())
  312. .then(data => {
  313. if (data.success) {
  314. alert('Maintenance scan triggered successfully');
  315. setTimeout(() => window.location.reload(), 2000);
  316. } else {
  317. alert('Failed to trigger scan: ' + (data.error || 'Unknown error'));
  318. }
  319. })
  320. .catch(error => {
  321. alert('Error: ' + error.message);
  322. });
  323. };
  324. window.refreshPage = function() {
  325. console.log("refreshPage called");
  326. window.location.reload();
  327. };
  328. window.navigateToTask = function(element) {
  329. const taskId = element.getAttribute('data-task-id');
  330. if (taskId) {
  331. window.location.href = '/maintenance/tasks/' + taskId;
  332. }
  333. };
  334. </script>
  335. }
  336. // Helper components
  337. templ TaskTypeIcon(taskType maintenance.MaintenanceTaskType) {
  338. <i class={maintenance.GetTaskIcon(taskType) + " me-1"}></i>
  339. }
  340. templ PriorityBadge(priority maintenance.MaintenanceTaskPriority) {
  341. switch priority {
  342. case maintenance.PriorityCritical:
  343. <span class="badge bg-danger">Critical</span>
  344. case maintenance.PriorityHigh:
  345. <span class="badge bg-warning">High</span>
  346. case maintenance.PriorityNormal:
  347. <span class="badge bg-primary">Normal</span>
  348. case maintenance.PriorityLow:
  349. <span class="badge bg-secondary">Low</span>
  350. default:
  351. <span class="badge bg-light text-dark">Unknown</span>
  352. }
  353. }
  354. templ StatusBadge(status maintenance.MaintenanceTaskStatus) {
  355. switch status {
  356. case maintenance.TaskStatusPending:
  357. <span class="badge bg-secondary">Pending</span>
  358. case maintenance.TaskStatusAssigned:
  359. <span class="badge bg-info">Assigned</span>
  360. case maintenance.TaskStatusInProgress:
  361. <span class="badge bg-warning">Running</span>
  362. case maintenance.TaskStatusCompleted:
  363. <span class="badge bg-success">Completed</span>
  364. case maintenance.TaskStatusFailed:
  365. <span class="badge bg-danger">Failed</span>
  366. case maintenance.TaskStatusCancelled:
  367. <span class="badge bg-light text-dark">Cancelled</span>
  368. default:
  369. <span class="badge bg-light text-dark">Unknown</span>
  370. }
  371. }
  372. templ ProgressBar(progress float64, status maintenance.MaintenanceTaskStatus) {
  373. if status == maintenance.TaskStatusInProgress || status == maintenance.TaskStatusAssigned {
  374. <div class="progress" style="height: 8px; min-width: 100px;">
  375. <div class="progress-bar" role="progressbar" style={fmt.Sprintf("width: %.1f%%", progress)}>
  376. </div>
  377. </div>
  378. <small class="text-muted">{fmt.Sprintf("%.1f%%", progress)}</small>
  379. } else if status == maintenance.TaskStatusCompleted {
  380. <div class="progress" style="height: 8px; min-width: 100px;">
  381. <div class="progress-bar bg-success" role="progressbar" style="width: 100%">
  382. </div>
  383. </div>
  384. <small class="text-success">100%</small>
  385. } else {
  386. <span class="text-muted">-</span>
  387. }
  388. }
  389. func formatDuration(d time.Duration) string {
  390. if d < time.Minute {
  391. return fmt.Sprintf("%.0fs", d.Seconds())
  392. } else if d < time.Hour {
  393. return fmt.Sprintf("%.1fm", d.Minutes())
  394. } else {
  395. return fmt.Sprintf("%.1fh", d.Hours())
  396. }
  397. }
  398. func formatTimeAgo(t time.Time) string {
  399. duration := time.Since(t)
  400. if duration < time.Minute {
  401. return "just now"
  402. } else if duration < time.Hour {
  403. minutes := int(duration.Minutes())
  404. return fmt.Sprintf("%dm ago", minutes)
  405. } else if duration < 24*time.Hour {
  406. hours := int(duration.Hours())
  407. return fmt.Sprintf("%dh ago", hours)
  408. } else {
  409. days := int(duration.Hours() / 24)
  410. return fmt.Sprintf("%dd ago", days)
  411. }
  412. }