task_detail.templ 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118
  1. package app
  2. import (
  3. "fmt"
  4. "sort"
  5. "github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
  6. "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
  7. )
  8. // sortedKeys returns the sorted keys for a string map
  9. func sortedKeys(m map[string]string) []string {
  10. keys := make([]string, 0, len(m))
  11. for k := range m {
  12. keys = append(keys, k)
  13. }
  14. sort.Strings(keys)
  15. return keys
  16. }
  17. templ TaskDetail(data *maintenance.TaskDetailData) {
  18. <div class="container-fluid">
  19. <!-- Header -->
  20. <div class="row mb-4">
  21. <div class="col-12">
  22. <div class="d-flex justify-content-between align-items-center">
  23. <div>
  24. <nav aria-label="breadcrumb">
  25. <ol class="breadcrumb mb-1">
  26. <li class="breadcrumb-item"><a href="/maintenance">Maintenance</a></li>
  27. <li class="breadcrumb-item active" aria-current="page">Task Detail</li>
  28. </ol>
  29. </nav>
  30. <h2 class="mb-0">
  31. <i class="fas fa-tasks me-2"></i>
  32. Task Detail: {data.Task.ID}
  33. </h2>
  34. </div>
  35. <div class="btn-group">
  36. <button type="button" class="btn btn-secondary" onclick="history.back()">
  37. <i class="fas fa-arrow-left me-1"></i>
  38. Back
  39. </button>
  40. <button type="button" class="btn btn-secondary" onclick="refreshPage()">
  41. <i class="fas fa-sync-alt me-1"></i>
  42. Refresh
  43. </button>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. <!-- Task Overview Card -->
  49. <div class="row mb-4">
  50. <div class="col-12">
  51. <div class="card">
  52. <div class="card-header">
  53. <h5 class="mb-0">
  54. <i class="fas fa-info-circle me-2"></i>
  55. Task Overview
  56. </h5>
  57. </div>
  58. <div class="card-body">
  59. <div class="row">
  60. <div class="col-md-6">
  61. <dl class="row">
  62. <dt class="col-sm-4">Task ID:</dt>
  63. <dd class="col-sm-8"><code>{data.Task.ID}</code></dd>
  64. <dt class="col-sm-4">Type:</dt>
  65. <dd class="col-sm-8">
  66. <span class="badge bg-info">{string(data.Task.Type)}</span>
  67. </dd>
  68. <dt class="col-sm-4">Status:</dt>
  69. <dd class="col-sm-8">
  70. if data.Task.Status == maintenance.TaskStatusPending {
  71. <span class="badge bg-secondary">Pending</span>
  72. } else if data.Task.Status == maintenance.TaskStatusAssigned {
  73. <span class="badge bg-info">Assigned</span>
  74. } else if data.Task.Status == maintenance.TaskStatusInProgress {
  75. <span class="badge bg-warning">In Progress</span>
  76. } else if data.Task.Status == maintenance.TaskStatusCompleted {
  77. <span class="badge bg-success">Completed</span>
  78. } else if data.Task.Status == maintenance.TaskStatusFailed {
  79. <span class="badge bg-danger">Failed</span>
  80. } else if data.Task.Status == maintenance.TaskStatusCancelled {
  81. <span class="badge bg-dark">Cancelled</span>
  82. }
  83. </dd>
  84. <dt class="col-sm-4">Priority:</dt>
  85. <dd class="col-sm-8">
  86. if data.Task.Priority == maintenance.PriorityHigh {
  87. <span class="badge bg-danger">High</span>
  88. } else if data.Task.Priority == maintenance.PriorityCritical {
  89. <span class="badge bg-danger">Critical</span>
  90. } else if data.Task.Priority == maintenance.PriorityNormal {
  91. <span class="badge bg-warning">Normal</span>
  92. } else {
  93. <span class="badge bg-secondary">Low</span>
  94. }
  95. </dd>
  96. if data.Task.Reason != "" {
  97. <dt class="col-sm-4">Reason:</dt>
  98. <dd class="col-sm-8">
  99. <span class="text-muted">{data.Task.Reason}</span>
  100. </dd>
  101. }
  102. </dl>
  103. </div>
  104. <div class="col-md-6">
  105. <!-- Task Timeline -->
  106. <div class="mb-3">
  107. <h6 class="text-primary mb-3">
  108. <i class="fas fa-clock me-1"></i>Task Timeline
  109. </h6>
  110. <div class="timeline-container">
  111. <div class="timeline-progress">
  112. <div class="timeline-step" data-step="created">
  113. <div class="timeline-circle completed">
  114. <i class="fas fa-plus"></i>
  115. </div>
  116. <div class="timeline-connector completed"></div>
  117. <div class="timeline-label">
  118. <strong>Created</strong>
  119. <small class="d-block text-muted">{data.Task.CreatedAt.Format("01-02 15:04:05")}</small>
  120. </div>
  121. </div>
  122. <div class="timeline-step" data-step="scheduled">
  123. <div class="timeline-circle completed">
  124. <i class="fas fa-calendar"></i>
  125. </div>
  126. if data.Task.StartedAt != nil {
  127. <div class="timeline-connector completed"></div>
  128. } else {
  129. <div class="timeline-connector"></div>
  130. }
  131. <div class="timeline-label">
  132. <strong>Scheduled</strong>
  133. <small class="d-block text-muted">{data.Task.ScheduledAt.Format("01-02 15:04:05")}</small>
  134. </div>
  135. </div>
  136. <div class="timeline-step" data-step="started">
  137. if data.Task.StartedAt != nil {
  138. <div class="timeline-circle completed">
  139. <i class="fas fa-play"></i>
  140. </div>
  141. } else {
  142. <div class="timeline-circle pending">
  143. <i class="fas fa-clock"></i>
  144. </div>
  145. }
  146. if data.Task.CompletedAt != nil {
  147. <div class="timeline-connector completed"></div>
  148. } else {
  149. <div class="timeline-connector"></div>
  150. }
  151. <div class="timeline-label">
  152. <strong>Started</strong>
  153. <small class="d-block text-muted">
  154. if data.Task.StartedAt != nil {
  155. {data.Task.StartedAt.Format("01-02 15:04:05")}
  156. } else {
  157. }
  158. </small>
  159. </div>
  160. </div>
  161. <div class="timeline-step" data-step="completed">
  162. if data.Task.CompletedAt != nil {
  163. <div class="timeline-circle completed">
  164. if data.Task.Status == maintenance.TaskStatusCompleted {
  165. <i class="fas fa-check"></i>
  166. } else if data.Task.Status == maintenance.TaskStatusFailed {
  167. <i class="fas fa-times"></i>
  168. } else {
  169. <i class="fas fa-stop"></i>
  170. }
  171. </div>
  172. } else {
  173. <div class="timeline-circle pending">
  174. <i class="fas fa-hourglass-half"></i>
  175. </div>
  176. }
  177. <div class="timeline-label">
  178. <strong>
  179. if data.Task.Status == maintenance.TaskStatusCompleted {
  180. Completed
  181. } else if data.Task.Status == maintenance.TaskStatusFailed {
  182. Failed
  183. } else if data.Task.Status == maintenance.TaskStatusCancelled {
  184. Cancelled
  185. } else {
  186. Pending
  187. }
  188. </strong>
  189. <small class="d-block text-muted">
  190. if data.Task.CompletedAt != nil {
  191. {data.Task.CompletedAt.Format("01-02 15:04:05")}
  192. } else {
  193. }
  194. </small>
  195. </div>
  196. </div>
  197. </div>
  198. </div>
  199. </div>
  200. <!-- Additional Info -->
  201. if data.Task.WorkerID != "" {
  202. <dl class="row">
  203. <dt class="col-sm-4">Worker:</dt>
  204. <dd class="col-sm-8"><code>{data.Task.WorkerID}</code></dd>
  205. </dl>
  206. }
  207. <dl class="row">
  208. if data.Task.TypedParams != nil && data.Task.TypedParams.VolumeSize > 0 {
  209. <dt class="col-sm-4">Volume Size:</dt>
  210. <dd class="col-sm-8">
  211. <span class="badge bg-primary">{formatBytes(int64(data.Task.TypedParams.VolumeSize))}</span>
  212. </dd>
  213. }
  214. if data.Task.TypedParams != nil && data.Task.TypedParams.Collection != "" {
  215. <dt class="col-sm-4">Collection:</dt>
  216. <dd class="col-sm-8">
  217. <span class="badge bg-info"><i class="fas fa-folder me-1"></i>{data.Task.TypedParams.Collection}</span>
  218. </dd>
  219. }
  220. if data.Task.TypedParams != nil && data.Task.TypedParams.DataCenter != "" {
  221. <dt class="col-sm-4">Data Center:</dt>
  222. <dd class="col-sm-8">
  223. <span class="badge bg-secondary"><i class="fas fa-building me-1"></i>{data.Task.TypedParams.DataCenter}</span>
  224. </dd>
  225. }
  226. if data.Task.Progress > 0 {
  227. <dt class="col-sm-4">Progress:</dt>
  228. <dd class="col-sm-8">
  229. <div class="progress" style="height: 20px;">
  230. <div class="progress-bar" role="progressbar"
  231. style={fmt.Sprintf("width: %.1f%%", data.Task.Progress)}
  232. aria-valuenow={fmt.Sprintf("%.1f", data.Task.Progress)}
  233. aria-valuemin="0" aria-valuemax="100">
  234. {fmt.Sprintf("%.1f%%", data.Task.Progress)}
  235. </div>
  236. </div>
  237. </dd>
  238. }
  239. </dl>
  240. </div>
  241. </div>
  242. if data.Task.DetailedReason != "" {
  243. <div class="row mt-3">
  244. <div class="col-12">
  245. <h6>Detailed Reason:</h6>
  246. <p class="text-muted">{data.Task.DetailedReason}</p>
  247. </div>
  248. </div>
  249. }
  250. if data.Task.Error != "" {
  251. <div class="row mt-3">
  252. <div class="col-12">
  253. <h6>Error:</h6>
  254. <div class="alert alert-danger">
  255. <code>{data.Task.Error}</code>
  256. </div>
  257. </div>
  258. </div>
  259. }
  260. </div>
  261. </div>
  262. </div>
  263. </div>
  264. <!-- Task Configuration Card -->
  265. if data.Task.TypedParams != nil {
  266. <div class="row mb-4">
  267. <div class="col-12">
  268. <div class="card">
  269. <div class="card-header">
  270. <h5 class="mb-0">
  271. <i class="fas fa-cog me-2"></i>
  272. Task Configuration
  273. </h5>
  274. </div>
  275. <div class="card-body">
  276. <!-- Source Servers (Unified) -->
  277. if len(data.Task.TypedParams.Sources) > 0 {
  278. <div class="mb-4">
  279. <h6 class="text-info d-flex align-items-center">
  280. <i class="fas fa-server me-2"></i>
  281. Source Servers
  282. <span class="badge bg-info ms-2">{fmt.Sprintf("%d", len(data.Task.TypedParams.Sources))}</span>
  283. </h6>
  284. <div class="bg-light p-3 rounded">
  285. <div class="d-flex flex-column gap-2">
  286. for i, source := range data.Task.TypedParams.Sources {
  287. <div class="d-grid" style="grid-template-columns: auto 1fr auto auto auto auto; gap: 0.5rem; align-items: center;">
  288. <span class="badge bg-primary">{fmt.Sprintf("#%d", i+1)}</span>
  289. <code>{source.Node}</code>
  290. <div>
  291. if source.DataCenter != "" {
  292. <small class="text-muted">
  293. <i class="fas fa-building me-1"></i>{source.DataCenter}
  294. </small>
  295. }
  296. </div>
  297. <div>
  298. if source.Rack != "" {
  299. <small class="text-muted">
  300. <i class="fas fa-server me-1"></i>{source.Rack}
  301. </small>
  302. }
  303. </div>
  304. <div>
  305. if source.VolumeId > 0 {
  306. <small class="text-muted">
  307. <i class="fas fa-hdd me-1"></i>Vol:{fmt.Sprintf("%d", source.VolumeId)}
  308. </small>
  309. }
  310. </div>
  311. <div>
  312. if len(source.ShardIds) > 0 {
  313. <small class="text-muted">
  314. <i class="fas fa-puzzle-piece me-1"></i>Shards:
  315. for j, shardId := range source.ShardIds {
  316. if j > 0 {
  317. <span>, </span>
  318. }
  319. if shardId < erasure_coding.DataShardsCount {
  320. <span class="badge badge-sm bg-primary ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Data shard %d", shardId)}>{fmt.Sprintf("%d", shardId)}</span>
  321. } else {
  322. <span class="badge badge-sm bg-warning text-dark ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Parity shard %d", shardId)}>{fmt.Sprintf("P%d", shardId-erasure_coding.DataShardsCount)}</span>
  323. }
  324. }
  325. </small>
  326. }
  327. </div>
  328. </div>
  329. }
  330. </div>
  331. </div>
  332. </div>
  333. }
  334. <!-- Task Flow Indicator -->
  335. if len(data.Task.TypedParams.Sources) > 0 || len(data.Task.TypedParams.Targets) > 0 {
  336. <div class="text-center mb-3">
  337. <i class="fas fa-arrow-down text-primary" style="font-size: 1.5rem;"></i>
  338. <br/>
  339. <small class="text-muted">Task: {string(data.Task.Type)}</small>
  340. </div>
  341. }
  342. <!-- Target/Destination (Generic) -->
  343. if len(data.Task.TypedParams.Targets) > 0 {
  344. <div class="mb-4">
  345. <h6 class="text-success d-flex align-items-center">
  346. <i class="fas fa-bullseye me-2"></i>
  347. Target Servers
  348. <span class="badge bg-success ms-2">{fmt.Sprintf("%d", len(data.Task.TypedParams.Targets))}</span>
  349. </h6>
  350. <div class="bg-light p-3 rounded">
  351. <div class="d-flex flex-column gap-2">
  352. for i, target := range data.Task.TypedParams.Targets {
  353. <div class="d-grid" style="grid-template-columns: auto 1fr auto auto auto auto; gap: 0.5rem; align-items: center;">
  354. <span class="badge bg-success">{fmt.Sprintf("#%d", i+1)}</span>
  355. <code>{target.Node}</code>
  356. <div>
  357. if target.DataCenter != "" {
  358. <small class="text-muted">
  359. <i class="fas fa-building me-1"></i>{target.DataCenter}
  360. </small>
  361. }
  362. </div>
  363. <div>
  364. if target.Rack != "" {
  365. <small class="text-muted">
  366. <i class="fas fa-server me-1"></i>{target.Rack}
  367. </small>
  368. }
  369. </div>
  370. <div>
  371. if target.VolumeId > 0 {
  372. <small class="text-muted">
  373. <i class="fas fa-hdd me-1"></i>Vol:{fmt.Sprintf("%d", target.VolumeId)}
  374. </small>
  375. }
  376. </div>
  377. <div>
  378. if len(target.ShardIds) > 0 {
  379. <small class="text-muted">
  380. <i class="fas fa-puzzle-piece me-1"></i>Shards:
  381. for j, shardId := range target.ShardIds {
  382. if j > 0 {
  383. <span>, </span>
  384. }
  385. if shardId < erasure_coding.DataShardsCount {
  386. <span class="badge badge-sm bg-primary ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Data shard %d", shardId)}>{fmt.Sprintf("%d", shardId)}</span>
  387. } else {
  388. <span class="badge badge-sm bg-warning text-dark ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Parity shard %d", shardId)}>{fmt.Sprintf("P%d", shardId-erasure_coding.DataShardsCount)}</span>
  389. }
  390. }
  391. </small>
  392. }
  393. </div>
  394. </div>
  395. }
  396. </div>
  397. </div>
  398. </div>
  399. }
  400. </div>
  401. </div>
  402. </div>
  403. </div>
  404. }
  405. <!-- Worker Information Card -->
  406. if data.WorkerInfo != nil {
  407. <div class="row mb-4">
  408. <div class="col-12">
  409. <div class="card">
  410. <div class="card-header">
  411. <h5 class="mb-0">
  412. <i class="fas fa-server me-2"></i>
  413. Worker Information
  414. </h5>
  415. </div>
  416. <div class="card-body">
  417. <div class="row">
  418. <div class="col-md-6">
  419. <dl class="row">
  420. <dt class="col-sm-4">Worker ID:</dt>
  421. <dd class="col-sm-8"><code>{data.WorkerInfo.ID}</code></dd>
  422. <dt class="col-sm-4">Address:</dt>
  423. <dd class="col-sm-8"><code>{data.WorkerInfo.Address}</code></dd>
  424. <dt class="col-sm-4">Status:</dt>
  425. <dd class="col-sm-8">
  426. if data.WorkerInfo.Status == "active" {
  427. <span class="badge bg-success">Active</span>
  428. } else if data.WorkerInfo.Status == "busy" {
  429. <span class="badge bg-warning">Busy</span>
  430. } else {
  431. <span class="badge bg-secondary">Inactive</span>
  432. }
  433. </dd>
  434. </dl>
  435. </div>
  436. <div class="col-md-6">
  437. <dl class="row">
  438. <dt class="col-sm-4">Last Heartbeat:</dt>
  439. <dd class="col-sm-8">{data.WorkerInfo.LastHeartbeat.Format("2006-01-02 15:04:05")}</dd>
  440. <dt class="col-sm-4">Current Load:</dt>
  441. <dd class="col-sm-8">{fmt.Sprintf("%d/%d", data.WorkerInfo.CurrentLoad, data.WorkerInfo.MaxConcurrent)}</dd>
  442. <dt class="col-sm-4">Capabilities:</dt>
  443. <dd class="col-sm-8">
  444. for _, capability := range data.WorkerInfo.Capabilities {
  445. <span class="badge bg-info me-1">{string(capability)}</span>
  446. }
  447. </dd>
  448. </dl>
  449. </div>
  450. </div>
  451. </div>
  452. </div>
  453. </div>
  454. </div>
  455. }
  456. <!-- Assignment History Card -->
  457. if len(data.AssignmentHistory) > 0 {
  458. <div class="row mb-4">
  459. <div class="col-12">
  460. <div class="card">
  461. <div class="card-header">
  462. <h5 class="mb-0">
  463. <i class="fas fa-history me-2"></i>
  464. Assignment History
  465. </h5>
  466. </div>
  467. <div class="card-body">
  468. <div class="table-responsive">
  469. <table class="table table-striped">
  470. <thead>
  471. <tr>
  472. <th>Worker ID</th>
  473. <th>Worker Address</th>
  474. <th>Assigned At</th>
  475. <th>Unassigned At</th>
  476. <th>Reason</th>
  477. </tr>
  478. </thead>
  479. <tbody>
  480. for _, assignment := range data.AssignmentHistory {
  481. <tr>
  482. <td><code>{assignment.WorkerID}</code></td>
  483. <td><code>{assignment.WorkerAddress}</code></td>
  484. <td>{assignment.AssignedAt.Format("2006-01-02 15:04:05")}</td>
  485. <td>
  486. if assignment.UnassignedAt != nil {
  487. {assignment.UnassignedAt.Format("2006-01-02 15:04:05")}
  488. } else {
  489. <span class="text-muted">—</span>
  490. }
  491. </td>
  492. <td>{assignment.Reason}</td>
  493. </tr>
  494. }
  495. </tbody>
  496. </table>
  497. </div>
  498. </div>
  499. </div>
  500. </div>
  501. </div>
  502. }
  503. <!-- Execution Logs Card -->
  504. if len(data.ExecutionLogs) > 0 {
  505. <div class="row mb-4">
  506. <div class="col-12">
  507. <div class="card">
  508. <div class="card-header">
  509. <h5 class="mb-0">
  510. <i class="fas fa-file-alt me-2"></i>
  511. Execution Logs
  512. </h5>
  513. </div>
  514. <div class="card-body">
  515. <div class="table-responsive">
  516. <table class="table table-striped table-sm">
  517. <thead>
  518. <tr>
  519. <th width="150">Timestamp</th>
  520. <th width="80">Level</th>
  521. <th>Message</th>
  522. <th>Details</th>
  523. </tr>
  524. </thead>
  525. <tbody>
  526. for _, log := range data.ExecutionLogs {
  527. <tr>
  528. <td><small>{log.Timestamp.Format("15:04:05")}</small></td>
  529. <td>
  530. if log.Level == "error" {
  531. <span class="badge bg-danger">{log.Level}</span>
  532. } else if log.Level == "warn" {
  533. <span class="badge bg-warning">{log.Level}</span>
  534. } else if log.Level == "info" {
  535. <span class="badge bg-info">{log.Level}</span>
  536. } else {
  537. <span class="badge bg-secondary">{log.Level}</span>
  538. }
  539. </td>
  540. <td><code>{log.Message}</code></td>
  541. <td>
  542. if log.Fields != nil && len(log.Fields) > 0 {
  543. <small>
  544. for _, k := range sortedKeys(log.Fields) {
  545. <span class="badge bg-light text-dark me-1">{k}=<i>{log.Fields[k]}</i></span>
  546. }
  547. </small>
  548. } else if log.Progress != nil || log.Status != "" {
  549. <small>
  550. if log.Progress != nil {
  551. <span class="badge bg-secondary me-1">progress=<i>{fmt.Sprintf("%.0f%%", *log.Progress)}</i></span>
  552. }
  553. if log.Status != "" {
  554. <span class="badge bg-secondary">status=<i>{log.Status}</i></span>
  555. }
  556. </small>
  557. } else {
  558. <span class="text-muted">-</span>
  559. }
  560. </td>
  561. </tr>
  562. }
  563. </tbody>
  564. </table>
  565. </div>
  566. </div>
  567. </div>
  568. </div>
  569. </div>
  570. }
  571. <!-- Related Tasks Card -->
  572. if len(data.RelatedTasks) > 0 {
  573. <div class="row mb-4">
  574. <div class="col-12">
  575. <div class="card">
  576. <div class="card-header">
  577. <h5 class="mb-0">
  578. <i class="fas fa-link me-2"></i>
  579. Related Tasks
  580. </h5>
  581. </div>
  582. <div class="card-body">
  583. <div class="table-responsive">
  584. <table class="table table-striped">
  585. <thead>
  586. <tr>
  587. <th>Task ID</th>
  588. <th>Type</th>
  589. <th>Status</th>
  590. <th>Volume ID</th>
  591. <th>Server</th>
  592. <th>Created</th>
  593. </tr>
  594. </thead>
  595. <tbody>
  596. for _, relatedTask := range data.RelatedTasks {
  597. <tr>
  598. <td>
  599. <a href={fmt.Sprintf("/maintenance/tasks/%s", relatedTask.ID)}>
  600. <code>{relatedTask.ID}</code>
  601. </a>
  602. </td>
  603. <td><span class="badge bg-info">{string(relatedTask.Type)}</span></td>
  604. <td>
  605. if relatedTask.Status == maintenance.TaskStatusCompleted {
  606. <span class="badge bg-success">Completed</span>
  607. } else if relatedTask.Status == maintenance.TaskStatusFailed {
  608. <span class="badge bg-danger">Failed</span>
  609. } else if relatedTask.Status == maintenance.TaskStatusInProgress {
  610. <span class="badge bg-warning">In Progress</span>
  611. } else {
  612. <span class="badge bg-secondary">{string(relatedTask.Status)}</span>
  613. }
  614. </td>
  615. <td>
  616. if relatedTask.VolumeID != 0 {
  617. {fmt.Sprintf("%d", relatedTask.VolumeID)}
  618. } else {
  619. <span class="text-muted">-</span>
  620. }
  621. </td>
  622. <td>
  623. if relatedTask.Server != "" {
  624. <code>{relatedTask.Server}</code>
  625. } else {
  626. <span class="text-muted">-</span>
  627. }
  628. </td>
  629. <td><small>{relatedTask.CreatedAt.Format("2006-01-02 15:04:05")}</small></td>
  630. </tr>
  631. }
  632. </tbody>
  633. </table>
  634. </div>
  635. </div>
  636. </div>
  637. </div>
  638. </div>
  639. }
  640. <!-- Actions Card -->
  641. <div class="row mb-4">
  642. <div class="col-12">
  643. <div class="card">
  644. <div class="card-header">
  645. <h5 class="mb-0">
  646. <i class="fas fa-cogs me-2"></i>
  647. Actions
  648. </h5>
  649. </div>
  650. <div class="card-body">
  651. if data.Task.Status == maintenance.TaskStatusPending || data.Task.Status == maintenance.TaskStatusAssigned {
  652. <button type="button" class="btn btn-danger me-2" data-task-id={data.Task.ID} onclick="cancelTask(this.getAttribute('data-task-id'))">
  653. <i class="fas fa-times me-1"></i>
  654. Cancel Task
  655. </button>
  656. }
  657. if data.Task.WorkerID != "" {
  658. <button type="button" class="btn btn-primary me-2" data-task-id={data.Task.ID} data-worker-id={data.Task.WorkerID} onclick="showTaskLogs(this.getAttribute('data-task-id'), this.getAttribute('data-worker-id'))">
  659. <i class="fas fa-file-text me-1"></i>
  660. Show Task Logs
  661. </button>
  662. }
  663. <button type="button" class="btn btn-info" data-task-id={data.Task.ID} onclick="exportTaskDetail(this.getAttribute('data-task-id'))">
  664. <i class="fas fa-download me-1"></i>
  665. Export Details
  666. </button>
  667. </div>
  668. </div>
  669. </div>
  670. </div>
  671. </div>
  672. <!-- Task Logs Modal -->
  673. <div class="modal fade" id="taskLogsModal" tabindex="-1" aria-labelledby="taskLogsModalLabel" aria-hidden="true">
  674. <div class="modal-dialog modal-xl">
  675. <div class="modal-content">
  676. <div class="modal-header">
  677. <h5 class="modal-title" id="taskLogsModalLabel">
  678. <i class="fas fa-file-text me-2"></i>Task Logs
  679. </h5>
  680. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  681. </div>
  682. <div class="modal-body">
  683. <div id="logsLoadingSpinner" class="text-center py-4" style="display: none;">
  684. <div class="spinner-border text-primary" role="status">
  685. <span class="visually-hidden">Loading logs...</span>
  686. </div>
  687. <p class="mt-2">Fetching logs from worker...</p>
  688. </div>
  689. <div id="logsError" class="alert alert-danger" style="display: none;">
  690. <i class="fas fa-exclamation-triangle me-2"></i>
  691. <span id="logsErrorMessage"></span>
  692. </div>
  693. <div id="logsContent" style="display: none;">
  694. <div class="d-flex justify-content-between align-items-center mb-3">
  695. <div>
  696. <strong>Task:</strong> <span id="logsTaskId"></span> |
  697. <strong>Worker:</strong> <span id="logsWorkerId"></span> |
  698. <strong>Entries:</strong> <span id="logsCount"></span>
  699. </div>
  700. <div class="btn-group">
  701. <button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshModalLogs()">
  702. <i class="fas fa-sync-alt me-1"></i>Refresh
  703. </button>
  704. <button type="button" class="btn btn-sm btn-outline-success" onclick="downloadTaskLogs()">
  705. <i class="fas fa-download me-1"></i>Download
  706. </button>
  707. </div>
  708. </div>
  709. <div class="card">
  710. <div class="card-header">
  711. <div class="d-flex justify-content-between align-items-center">
  712. <span>Log Entries (Last 100)</span>
  713. <small class="text-muted">Newest entries first</small>
  714. </div>
  715. </div>
  716. <div class="card-body p-0">
  717. <pre id="logsDisplay" class="bg-dark text-light p-3 mb-0" style="max-height: 400px; overflow-y: auto; font-size: 0.85rem; line-height: 1.4;"></pre>
  718. </div>
  719. </div>
  720. </div>
  721. </div>
  722. <div class="modal-footer">
  723. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  724. </div>
  725. </div>
  726. </div>
  727. </div>
  728. <style>
  729. .timeline-container {
  730. position: relative;
  731. padding: 20px 0;
  732. }
  733. .timeline-progress {
  734. display: flex;
  735. justify-content: space-between;
  736. align-items: flex-start;
  737. position: relative;
  738. max-width: 100%;
  739. }
  740. .timeline-step {
  741. display: flex;
  742. flex-direction: column;
  743. align-items: center;
  744. flex: 1;
  745. position: relative;
  746. }
  747. .timeline-circle {
  748. width: 40px;
  749. height: 40px;
  750. border-radius: 50%;
  751. display: flex;
  752. align-items: center;
  753. justify-content: center;
  754. color: white;
  755. font-weight: bold;
  756. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  757. z-index: 2;
  758. position: relative;
  759. }
  760. .timeline-circle.completed {
  761. background-color: #28a745;
  762. border: 3px solid #1e7e34;
  763. }
  764. .timeline-circle.pending {
  765. background-color: #6c757d;
  766. border: 3px solid #495057;
  767. }
  768. .timeline-connector {
  769. position: absolute;
  770. top: 20px;
  771. left: 50%;
  772. right: -50%;
  773. height: 4px;
  774. z-index: 1;
  775. margin-left: 20px;
  776. margin-right: 20px;
  777. }
  778. .timeline-connector.completed {
  779. background-color: #28a745;
  780. }
  781. .timeline-connector:not(.completed) {
  782. background-color: #dee2e6;
  783. }
  784. .timeline-step:last-child .timeline-connector {
  785. display: none;
  786. }
  787. .timeline-label {
  788. margin-top: 15px;
  789. text-align: center;
  790. min-height: 60px;
  791. }
  792. .timeline-label strong {
  793. display: block;
  794. font-size: 0.9rem;
  795. margin-bottom: 4px;
  796. }
  797. .timeline-label small {
  798. font-size: 0.75rem;
  799. line-height: 1.2;
  800. }
  801. @media (max-width: 768px) {
  802. .timeline-progress {
  803. flex-direction: column;
  804. align-items: stretch;
  805. }
  806. .timeline-step {
  807. flex-direction: row;
  808. align-items: center;
  809. margin-bottom: 20px;
  810. }
  811. .timeline-circle {
  812. margin-right: 15px;
  813. flex-shrink: 0;
  814. }
  815. .timeline-connector {
  816. display: none;
  817. }
  818. .timeline-label {
  819. text-align: left;
  820. margin-top: 0;
  821. min-height: auto;
  822. }
  823. }
  824. </style>
  825. <script>
  826. // Global variables for current logs modal
  827. let currentTaskId = '';
  828. let currentWorkerId = '';
  829. function refreshPage() {
  830. location.reload();
  831. }
  832. function showTaskLogs(taskId, workerId) {
  833. currentTaskId = taskId;
  834. currentWorkerId = workerId;
  835. // Show the modal
  836. const modal = new bootstrap.Modal(document.getElementById('taskLogsModal'));
  837. modal.show();
  838. // Load logs
  839. loadTaskLogs(taskId, workerId);
  840. }
  841. function loadTaskLogs(taskId, workerId) {
  842. // Show loading spinner
  843. document.getElementById('logsLoadingSpinner').style.display = 'block';
  844. document.getElementById('logsError').style.display = 'none';
  845. document.getElementById('logsContent').style.display = 'none';
  846. // Update modal info
  847. document.getElementById('logsTaskId').textContent = taskId;
  848. document.getElementById('logsWorkerId').textContent = workerId;
  849. // Fetch logs from the API
  850. fetch(`/api/maintenance/workers/${workerId}/logs?taskId=${taskId}&maxEntries=100`)
  851. .then(response => response.json())
  852. .then(data => {
  853. document.getElementById('logsLoadingSpinner').style.display = 'none';
  854. if (data.error) {
  855. showLogsError(data.error);
  856. return;
  857. }
  858. // Display logs
  859. displayLogs(data.logs, data.count || 0);
  860. })
  861. .catch(error => {
  862. document.getElementById('logsLoadingSpinner').style.display = 'none';
  863. showLogsError('Failed to fetch logs: ' + error.message);
  864. });
  865. }
  866. function displayLogs(logs, count) {
  867. document.getElementById('logsError').style.display = 'none';
  868. document.getElementById('logsContent').style.display = 'block';
  869. document.getElementById('logsCount').textContent = count;
  870. const logsDisplay = document.getElementById('logsDisplay');
  871. if (!logs || logs.length === 0) {
  872. logsDisplay.textContent = 'No logs found for this task.';
  873. return;
  874. }
  875. // Format and display logs with structured fields
  876. let logText = '';
  877. logs.forEach(entry => {
  878. const timestamp = entry.timestamp ? new Date(entry.timestamp * 1000).toISOString() : 'N/A';
  879. const level = entry.level || 'INFO';
  880. const message = entry.message || '';
  881. logText += `[${timestamp}] ${level}: ${message}`;
  882. // Add structured fields if they exist
  883. if (entry.fields && Object.keys(entry.fields).length > 0) {
  884. const fieldsStr = Object.entries(entry.fields)
  885. .map(([key, value]) => `${key}=${value}`)
  886. .join(', ');
  887. logText += ` | ${fieldsStr}`;
  888. }
  889. // Add progress if available
  890. if (entry.progress !== undefined && entry.progress !== null) {
  891. logText += ` | progress=${entry.progress}%`;
  892. }
  893. // Add status if available
  894. if (entry.status) {
  895. logText += ` | status=${entry.status}`;
  896. }
  897. logText += '\n';
  898. });
  899. logsDisplay.textContent = logText;
  900. // Scroll to top
  901. logsDisplay.scrollTop = 0;
  902. }
  903. function showLogsError(errorMessage) {
  904. document.getElementById('logsError').style.display = 'block';
  905. document.getElementById('logsContent').style.display = 'none';
  906. document.getElementById('logsErrorMessage').textContent = errorMessage;
  907. }
  908. function refreshModalLogs() {
  909. if (currentTaskId && currentWorkerId) {
  910. loadTaskLogs(currentTaskId, currentWorkerId);
  911. }
  912. }
  913. function downloadTaskLogs() {
  914. if (!currentTaskId || !currentWorkerId) {
  915. alert('No task logs to download');
  916. return;
  917. }
  918. // Download all logs (without maxEntries limit)
  919. const downloadUrl = `/api/maintenance/workers/${currentWorkerId}/logs?taskId=${currentTaskId}&maxEntries=0`;
  920. fetch(downloadUrl)
  921. .then(response => response.json())
  922. .then(data => {
  923. if (data.error) {
  924. alert('Error downloading logs: ' + data.error);
  925. return;
  926. }
  927. // Convert logs to text format with structured fields
  928. let logContent = '';
  929. if (data.logs && data.logs.length > 0) {
  930. data.logs.forEach(entry => {
  931. const timestamp = entry.timestamp ? new Date(entry.timestamp * 1000).toISOString() : 'N/A';
  932. const level = entry.level || 'INFO';
  933. const message = entry.message || '';
  934. logContent += `[${timestamp}] ${level}: ${message}`;
  935. // Add structured fields if they exist
  936. if (entry.fields && Object.keys(entry.fields).length > 0) {
  937. const fieldsStr = Object.entries(entry.fields)
  938. .map(([key, value]) => `${key}=${value}`)
  939. .join(', ');
  940. logContent += ` | ${fieldsStr}`;
  941. }
  942. // Add progress if available
  943. if (entry.progress !== undefined && entry.progress !== null) {
  944. logContent += ` | progress=${entry.progress}%`;
  945. }
  946. // Add status if available
  947. if (entry.status) {
  948. logContent += ` | status=${entry.status}`;
  949. }
  950. logContent += '\n';
  951. });
  952. } else {
  953. logContent = 'No logs found for this task.';
  954. }
  955. // Create and download file
  956. const blob = new Blob([logContent], { type: 'text/plain' });
  957. const url = URL.createObjectURL(blob);
  958. const link = document.createElement('a');
  959. link.href = url;
  960. link.download = `task-${currentTaskId}-logs.txt`;
  961. link.click();
  962. URL.revokeObjectURL(url);
  963. })
  964. .catch(error => {
  965. alert('Error downloading logs: ' + error.message);
  966. });
  967. }
  968. function cancelTask(taskId) {
  969. if (confirm('Are you sure you want to cancel this task?')) {
  970. fetch(`/api/maintenance/tasks/${taskId}/cancel`, {
  971. method: 'POST',
  972. headers: {
  973. 'Content-Type': 'application/json',
  974. },
  975. })
  976. .then(response => response.json())
  977. .then(data => {
  978. if (data.success) {
  979. alert('Task cancelled successfully');
  980. location.reload();
  981. } else {
  982. alert('Error cancelling task: ' + data.error);
  983. }
  984. })
  985. .catch(error => {
  986. console.error('Error:', error);
  987. alert('Error cancelling task');
  988. });
  989. }
  990. }
  991. function refreshTaskLogs(taskId) {
  992. fetch(`/api/maintenance/tasks/${taskId}/detail`)
  993. .then(response => response.json())
  994. .then(data => {
  995. location.reload();
  996. })
  997. .catch(error => {
  998. console.error('Error:', error);
  999. alert('Error refreshing logs');
  1000. });
  1001. }
  1002. function exportTaskDetail(taskId) {
  1003. fetch(`/api/maintenance/tasks/${taskId}/detail`)
  1004. .then(response => response.json())
  1005. .then(data => {
  1006. const dataStr = JSON.stringify(data, null, 2);
  1007. const dataBlob = new Blob([dataStr], {type: 'application/json'});
  1008. const url = URL.createObjectURL(dataBlob);
  1009. const link = document.createElement('a');
  1010. link.href = url;
  1011. link.download = `task-${taskId}-detail.json`;
  1012. link.click();
  1013. URL.revokeObjectURL(url);
  1014. })
  1015. .catch(error => {
  1016. console.error('Error:', error);
  1017. alert('Error exporting task detail');
  1018. });
  1019. }
  1020. // Auto-refresh every 30 seconds for active tasks
  1021. if ('{string(data.Task.Status)}' === 'in_progress') {
  1022. setInterval(refreshPage, 30000);
  1023. }
  1024. </script>
  1025. }