web-mojo 2.2.10 → 2.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/admin.cjs.js +1 -1
  2. package/dist/admin.cjs.js.map +1 -1
  3. package/dist/admin.es.js +480 -498
  4. package/dist/admin.es.js.map +1 -1
  5. package/dist/auth.cjs.js +1 -1
  6. package/dist/auth.es.js +1 -1
  7. package/dist/charts.cjs.js +1 -1
  8. package/dist/charts.es.js +4 -4
  9. package/dist/chunks/{ChatView-BxIeRwBQ.js → ChatView-CQnDGafI.js} +2 -2
  10. package/dist/chunks/{ChatView-BxIeRwBQ.js.map → ChatView-CQnDGafI.js.map} +1 -1
  11. package/dist/chunks/{ChatView-DfhhZKoN.js → ChatView-ppMlENSa.js} +7 -7
  12. package/dist/chunks/{ChatView-DfhhZKoN.js.map → ChatView-ppMlENSa.js.map} +1 -1
  13. package/dist/chunks/{Collection-G5_fJcpB.js → Collection-BQxqHtRi.js} +2 -2
  14. package/dist/chunks/{Collection-G5_fJcpB.js.map → Collection-BQxqHtRi.js.map} +1 -1
  15. package/dist/chunks/{Collection-BgX6OcUC.js → Collection-BlP54kxB.js} +2 -2
  16. package/dist/chunks/{Collection-BgX6OcUC.js.map → Collection-BlP54kxB.js.map} +1 -1
  17. package/dist/chunks/{ContextMenu-C4_MWleT.js → ContextMenu-BH5SaDXX.js} +2 -2
  18. package/dist/chunks/{ContextMenu-C4_MWleT.js.map → ContextMenu-BH5SaDXX.js.map} +1 -1
  19. package/dist/chunks/{ContextMenu-CMoik8q6.js → ContextMenu-BNFU-kG4.js} +3 -3
  20. package/dist/chunks/{ContextMenu-CMoik8q6.js.map → ContextMenu-BNFU-kG4.js.map} +1 -1
  21. package/dist/chunks/{DataView-i7kpJjVQ.js → DataView-BpXdthN2.js} +2 -2
  22. package/dist/chunks/{DataView-i7kpJjVQ.js.map → DataView-BpXdthN2.js.map} +1 -1
  23. package/dist/chunks/{DataView-BYNjIf--.js → DataView-R_LkYBAw.js} +2 -2
  24. package/dist/chunks/{DataView-BYNjIf--.js.map → DataView-R_LkYBAw.js.map} +1 -1
  25. package/dist/chunks/{Dialog-DX5h2QA9.js → Dialog--hl_Uh6X.js} +2 -2
  26. package/dist/chunks/{Dialog-DX5h2QA9.js.map → Dialog--hl_Uh6X.js.map} +1 -1
  27. package/dist/chunks/{Dialog-Cu_Dx46k.js → Dialog-RzLLLfJD.js} +5 -5
  28. package/dist/chunks/{Dialog-Cu_Dx46k.js.map → Dialog-RzLLLfJD.js.map} +1 -1
  29. package/dist/chunks/{FormView-B_90L1RY.js → FormView--WuITh01.js} +2 -2
  30. package/dist/chunks/{FormView-B_90L1RY.js.map → FormView--WuITh01.js.map} +1 -1
  31. package/dist/chunks/{FormView-Bwofbd8S.js → FormView-C1emfj3B.js} +2 -2
  32. package/dist/chunks/{FormView-Bwofbd8S.js.map → FormView-C1emfj3B.js.map} +1 -1
  33. package/dist/chunks/{ListView-DoF-Sr56.js → ListView-B96JeG4g.js} +5 -5
  34. package/dist/chunks/ListView-B96JeG4g.js.map +1 -0
  35. package/dist/chunks/{ListView-OWwlcGGg.js → ListView-O9AO02Rf.js} +2 -2
  36. package/dist/chunks/ListView-O9AO02Rf.js.map +1 -0
  37. package/dist/chunks/{MetricsMiniChartWidget-vXr5pxpm.js → MetricsMiniChartWidget-DoxqoF1X.js} +2 -2
  38. package/dist/chunks/{MetricsMiniChartWidget-vXr5pxpm.js.map → MetricsMiniChartWidget-DoxqoF1X.js.map} +1 -1
  39. package/dist/chunks/{MetricsMiniChartWidget-DL5stA6A.js → MetricsMiniChartWidget-DyVs4Wt0.js} +4 -4
  40. package/dist/chunks/{MetricsMiniChartWidget-DL5stA6A.js.map → MetricsMiniChartWidget-DyVs4Wt0.js.map} +1 -1
  41. package/dist/chunks/{PDFViewer-DSmi78S6.js → PDFViewer-BxFcG82d.js} +2 -2
  42. package/dist/chunks/{PDFViewer-DSmi78S6.js.map → PDFViewer-BxFcG82d.js.map} +1 -1
  43. package/dist/chunks/{PDFViewer--jlqnuVw.js → PDFViewer-CHX2NLkG.js} +3 -3
  44. package/dist/chunks/{PDFViewer--jlqnuVw.js.map → PDFViewer-CHX2NLkG.js.map} +1 -1
  45. package/dist/chunks/{Rest-ChN4Ntac.js → Rest-0oRgqNjX.js} +16 -1
  46. package/dist/chunks/Rest-0oRgqNjX.js.map +1 -0
  47. package/dist/chunks/Rest-P-KCJpjB.js +2 -0
  48. package/dist/chunks/Rest-P-KCJpjB.js.map +1 -0
  49. package/dist/chunks/{TokenManager-CEOPgnsw.js → TokenManager-CCfcK4aA.js} +2 -2
  50. package/dist/chunks/{TokenManager-CEOPgnsw.js.map → TokenManager-CCfcK4aA.js.map} +1 -1
  51. package/dist/chunks/{TokenManager-DiQfilqw.js → TokenManager-TEF4Gmwu.js} +5 -5
  52. package/dist/chunks/{TokenManager-DiQfilqw.js.map → TokenManager-TEF4Gmwu.js.map} +1 -1
  53. package/dist/chunks/{WebSocketClient-JHjYcYbU.js → WebSocketClient-BbPsISrp.js} +2 -2
  54. package/dist/chunks/{WebSocketClient-JHjYcYbU.js.map → WebSocketClient-BbPsISrp.js.map} +1 -1
  55. package/dist/chunks/{WebSocketClient-bLYhu2Wv.js → WebSocketClient-D53hpvM8.js} +2 -2
  56. package/dist/chunks/{WebSocketClient-bLYhu2Wv.js.map → WebSocketClient-D53hpvM8.js.map} +1 -1
  57. package/dist/chunks/{version-DOHckOGK.js → version-C2aAPoA6.js} +4 -4
  58. package/dist/chunks/{version-DOHckOGK.js.map → version-C2aAPoA6.js.map} +1 -1
  59. package/dist/chunks/{version-DUvrBxZl.js → version-CiqJg8U3.js} +2 -2
  60. package/dist/chunks/{version-DUvrBxZl.js.map → version-CiqJg8U3.js.map} +1 -1
  61. package/dist/docit.cjs.js +1 -1
  62. package/dist/docit.es.js +6 -6
  63. package/dist/index.cjs.js +1 -1
  64. package/dist/index.es.js +15 -15
  65. package/dist/lightbox.cjs.js +1 -1
  66. package/dist/lightbox.es.js +5 -5
  67. package/dist/map.cjs.js +1 -1
  68. package/dist/map.es.js +2 -2
  69. package/dist/timeline.cjs.js +1 -1
  70. package/dist/timeline.es.js +4 -4
  71. package/package.json +1 -1
  72. package/dist/chunks/ListView-DoF-Sr56.js.map +0 -1
  73. package/dist/chunks/ListView-OWwlcGGg.js.map +0 -1
  74. package/dist/chunks/Rest-ChN4Ntac.js.map +0 -1
  75. package/dist/chunks/Rest-DhD-U1vp.js +0 -2
  76. package/dist/chunks/Rest-DhD-U1vp.js.map +0 -1
package/dist/admin.es.js CHANGED
@@ -1,16 +1,16 @@
1
- import { P as Page, U as User, e as UserDataView, g as UserDeviceList, i as UserDeviceLocationList, C as ContextMenu, d as UserForms, c as UserList, a as Group, G as GroupList, b as GroupForms, f as UserDevice } from "./chunks/ContextMenu-CMoik8q6.js";
2
- import { V as View, M as MOJOUtils, r as rest } from "./chunks/Rest-ChN4Ntac.js";
3
- import "./chunks/WebSocketClient-bLYhu2Wv.js";
4
- import { D as Dialog$1 } from "./chunks/Dialog-Cu_Dx46k.js";
5
- import { W } from "./chunks/Dialog-Cu_Dx46k.js";
6
- import { M as MetricsChart, c as MetricsMiniChartWidget, P as PieChart } from "./chunks/MetricsMiniChartWidget-DL5stA6A.js";
7
- import { aj as MemberList, T as TableView, B as IncidentEventList, ap as PushDeviceList, ai as LogList, c as TabView, b as TablePage, M as Member, ak as MemberForms, ay as GeoLocatedIP, az as GeoLocatedIPList, aB as TicketList, J as IncidentList, a0 as IncidentStats, V as IncidentHistoryList, U as IncidentHistory, H as Incident, C as ChatView, K as IncidentForms, I as IncidentEvent, G as IncidentEventForms, aD as TicketNoteList, aC as TicketNote, aA as Ticket, aE as TicketForms, aF as TicketCategories, W as RuleSet, a2 as MatchByOptions, a1 as BundleByOptions, _ as RuleList, X as RuleSetList, k as EmailDomainForms, j as EmailDomainList, E as EmailDomain, n as MailboxForms, m as MailboxList, l as Mailbox, s as EmailTemplate, u as EmailTemplateForms, t as EmailTemplateList, o as SentMessage, q as SentMessageList, av as PushDeliveryList, aw as PushConfigForms, at as PushConfigList, ax as PushTemplateForms, ar as PushTemplateList, a6 as Job, ac as JobEventList, aa as JobLogList, a8 as JobForms, a7 as JobList, af as JobRunnerList, ad as JobsEngineStats, ag as JobRunnerForms, ae as JobRunner, ah as Log, al as MetricsPermission, an as MetricsForms, am as MetricsPermissionList, x as FileManagerForms, w as FileManagerList, y as File, A as FileForms, z as FileList, i as S3BucketForms, h as S3BucketList } from "./chunks/ChatView-DfhhZKoN.js";
8
- import DataView from "./chunks/DataView-i7kpJjVQ.js";
9
- import { F as FormView, a as applyFileDropMixin } from "./chunks/FormView-B_90L1RY.js";
1
+ import { P as Page, U as User, e as UserDataView, g as UserDeviceList, i as UserDeviceLocationList, C as ContextMenu, d as UserForms, c as UserList, a as Group, G as GroupList, b as GroupForms, f as UserDevice } from "./chunks/ContextMenu-BNFU-kG4.js";
2
+ import { V as View, M as MOJOUtils, r as rest } from "./chunks/Rest-0oRgqNjX.js";
3
+ import "./chunks/WebSocketClient-D53hpvM8.js";
4
+ import { D as Dialog$1 } from "./chunks/Dialog-RzLLLfJD.js";
5
+ import { W } from "./chunks/Dialog-RzLLLfJD.js";
6
+ import { M as MetricsChart, c as MetricsMiniChartWidget, P as PieChart } from "./chunks/MetricsMiniChartWidget-DyVs4Wt0.js";
7
+ import { aj as MemberList, T as TableView, B as IncidentEventList, ap as PushDeviceList, ai as LogList, c as TabView, b as TablePage, M as Member, ak as MemberForms, ay as GeoLocatedIP, az as GeoLocatedIPList, aB as TicketList, J as IncidentList, a0 as IncidentStats, V as IncidentHistoryList, U as IncidentHistory, H as Incident, C as ChatView, K as IncidentForms, I as IncidentEvent, G as IncidentEventForms, aD as TicketNoteList, aC as TicketNote, aA as Ticket, aE as TicketForms, aF as TicketCategories, W as RuleSet, a2 as MatchByOptions, a1 as BundleByOptions, _ as RuleList, X as RuleSetList, k as EmailDomainForms, j as EmailDomainList, E as EmailDomain, n as MailboxForms, m as MailboxList, l as Mailbox, s as EmailTemplate, u as EmailTemplateForms, t as EmailTemplateList, o as SentMessage, q as SentMessageList, av as PushDeliveryList, aw as PushConfigForms, at as PushConfigList, ax as PushTemplateForms, ar as PushTemplateList, a6 as Job, ac as JobEventList, aa as JobLogList, a8 as JobForms, ad as JobsEngineStats, a7 as JobList, af as JobRunnerList, ag as JobRunnerForms, ae as JobRunner, ah as Log, al as MetricsPermission, an as MetricsForms, am as MetricsPermissionList, x as FileManagerForms, w as FileManagerList, y as File, A as FileForms, z as FileList, i as S3BucketForms, h as S3BucketList } from "./chunks/ChatView-ppMlENSa.js";
8
+ import DataView from "./chunks/DataView-BpXdthN2.js";
9
+ import { F as FormView, a as applyFileDropMixin } from "./chunks/FormView--WuITh01.js";
10
10
  import { MapView } from "./map.es.js";
11
- import { M as Model, C as Collection } from "./chunks/Collection-G5_fJcpB.js";
12
- import { L as LightboxGallery, P as PDFViewer } from "./chunks/PDFViewer--jlqnuVw.js";
13
- import { B, a, V, b, c, d } from "./chunks/version-DOHckOGK.js";
11
+ import { M as Model, C as Collection } from "./chunks/Collection-BQxqHtRi.js";
12
+ import { L as LightboxGallery, P as PDFViewer } from "./chunks/PDFViewer-CHX2NLkG.js";
13
+ import { B, a, V, b, c, d } from "./chunks/version-C2aAPoA6.js";
14
14
  class AdminHeaderView extends View {
15
15
  constructor(options = {}) {
16
16
  super({
@@ -3003,27 +3003,27 @@ class MetricsCountryMapView extends View {
3003
3003
  ["linear"],
3004
3004
  ["get", "intensity"],
3005
3005
  0,
3006
- "rgba(32, 201, 151, 0.2)",
3006
+ "rgba(32, 201, 151, 0.6)",
3007
3007
  1,
3008
- "rgba(255, 193, 7, 0.85)"
3008
+ "rgba(255, 193, 7, 0.95)"
3009
3009
  ],
3010
3010
  "line-width": [
3011
3011
  "interpolate",
3012
3012
  ["linear"],
3013
3013
  ["get", "intensity"],
3014
3014
  0,
3015
+ 1.75,
3015
3016
  1,
3016
- 1,
3017
- 5
3017
+ 6
3018
3018
  ],
3019
3019
  "line-opacity": [
3020
3020
  "interpolate",
3021
3021
  ["linear"],
3022
3022
  ["get", "intensity"],
3023
3023
  0,
3024
- 0.2,
3024
+ 0.45,
3025
3025
  1,
3026
- 0.9
3026
+ 0.95
3027
3027
  ]
3028
3028
  };
3029
3029
  this.mapView.lineSources = this.mapView.lineSources.filter((src) => src.id !== `${this.id}-routes`);
@@ -3316,7 +3316,7 @@ class IncidentDashboardPage extends Page {
3316
3316
  title: '<i class="bi bi-geo-alt me-2"></i> Incidents by Country',
3317
3317
  endpoint: "/api/metrics/fetch",
3318
3318
  account: "incident",
3319
- category: "incident_by_country",
3319
+ category: "incidents_by_country",
3320
3320
  granularity: "days",
3321
3321
  chartType: "line",
3322
3322
  showDateRange: false,
@@ -3338,7 +3338,7 @@ class IncidentDashboardPage extends Page {
3338
3338
  containerId: "events-country-map",
3339
3339
  category: "incident_events_by_country",
3340
3340
  account: "incident",
3341
- maxCountries: 15,
3341
+ maxCountries: 20,
3342
3342
  metricLabel: "Events",
3343
3343
  height: 360,
3344
3344
  mapStyle: "dark"
@@ -6098,7 +6098,7 @@ class JobHealthView extends View {
6098
6098
  <i class="bi bi-circle-fill fs-4 {{healthStatusClass}}"></i>
6099
6099
  </div>
6100
6100
  <div>
6101
- <h5 class="mb-1">System Health: {{health.overall_status|capitalize}}</h5>
6101
+ <h5 class="mb-1">Service Health: {{health.overall_status|capitalize}}</h5>
6102
6102
  <small class="text-muted d-block">
6103
6103
  Workers: {{health.runners.active}}/{{health.runners.total}} active
6104
6104
  </small>
@@ -6574,444 +6574,36 @@ class JobDetailsView extends View {
6574
6574
  }
6575
6575
  }
6576
6576
  Job.VIEW_CLASS = JobDetailsView;
6577
- class JobsTable extends TableView {
6577
+ class JobMetricsModalView extends View {
6578
6578
  constructor(options = {}) {
6579
6579
  super({
6580
- Collection: JobList,
6581
- collectionParams: {
6582
- size: 15,
6583
- sort: "-created"
6584
- },
6585
- options: {
6586
- searchable: true,
6587
- sortable: true,
6588
- paginated: true,
6589
- size: 15,
6590
- ...options.options
6591
- },
6592
- columns: [
6593
- {
6594
- key: "id",
6595
- label: "Job ID",
6596
- formatter: "truncate_middle(12)",
6597
- sortable: true,
6598
- filter: { type: "text", placeholder: "Job ID..." }
6599
- },
6600
- {
6601
- key: "status",
6602
- label: "Status",
6603
- formatter: (value, context) => {
6604
- const job = context.row;
6605
- const badgeClass = job.getStatusBadgeClass ? job.getStatusBadgeClass() : "bg-secondary";
6606
- const icon = job.getStatusIcon ? job.getStatusIcon() : "bi-question";
6607
- return `<span class="badge ${badgeClass}"><i class="${icon} me-1"></i>${value.toUpperCase()}</span>`;
6608
- },
6609
- sortable: true,
6610
- filter: {
6611
- type: "select",
6612
- options: [
6613
- { value: "pending", label: "Pending" },
6614
- { value: "running", label: "Running" },
6615
- { value: "completed", label: "Completed" },
6616
- { value: "failed", label: "Failed" },
6617
- { value: "canceled", label: "Canceled" },
6618
- { value: "expired", label: "Expired" }
6619
- ]
6620
- }
6621
- },
6622
- {
6623
- key: "channel",
6624
- label: "Channel",
6625
- formatter: "badge",
6626
- sortable: true,
6627
- filter: { type: "text", placeholder: "Channel..." }
6628
- },
6629
- {
6630
- key: "created",
6631
- label: "Created",
6632
- formatter: "datetime",
6633
- sortable: true,
6634
- filter: {
6635
- type: "daterange",
6636
- label: "Created Date"
6637
- }
6638
- },
6639
- {
6640
- key: "started_at",
6641
- label: "Started",
6642
- formatter: "datetime",
6643
- sortable: true
6644
- },
6645
- {
6646
- key: "finished_at",
6647
- label: "Finished",
6648
- formatter: "datetime",
6649
- sortable: true
6650
- }
6651
- ],
6652
- contextMenu: [
6653
- {
6654
- label: "View Details",
6655
- action: "view-job-details",
6656
- icon: "bi-info-circle"
6657
- },
6658
- {
6659
- label: "View Events",
6660
- action: "view-job-events",
6661
- icon: "bi-clock-history"
6662
- },
6663
- { separator: true },
6664
- {
6665
- label: "Cancel Job",
6666
- action: "cancel-job",
6667
- icon: "bi-x-circle",
6668
- danger: true,
6669
- condition: (job) => job.canCancel && job.canCancel()
6670
- },
6671
- {
6672
- label: "Retry Job",
6673
- action: "retry-job",
6674
- icon: "bi-arrow-clockwise",
6675
- condition: (job) => job.canRetry && job.canRetry()
6676
- },
6677
- {
6678
- label: "Clone Job",
6679
- action: "clone-job",
6680
- icon: "bi-copy"
6681
- },
6682
- { separator: true },
6683
- {
6684
- label: "Export Job",
6685
- action: "export-job",
6686
- icon: "bi-download"
6687
- }
6688
- ],
6689
- filters: [
6690
- {
6691
- key: "func",
6692
- label: "Function",
6693
- type: "text"
6694
- }
6695
- ],
6696
- batchActions: [
6697
- {
6698
- label: "Cancel Selected",
6699
- action: "batch-cancel",
6700
- icon: "bi-x-circle"
6701
- },
6702
- {
6703
- label: "Retry Selected",
6704
- action: "batch-retry",
6705
- icon: "bi-arrow-clockwise"
6706
- },
6707
- {
6708
- label: "Export Selected",
6709
- action: "batch-export",
6710
- icon: "bi-download"
6711
- }
6712
- ],
6580
+ className: "job-metrics-modal-view",
6713
6581
  ...options
6714
6582
  });
6583
+ this.template = `
6584
+ <div data-container="job-metrics-modal-chart" style="min-height:320px;"></div>
6585
+ `;
6715
6586
  }
6716
- async onItemViewJobDetails(job) {
6717
- if (job) {
6718
- await JobDetailsView.show(job);
6719
- }
6720
- }
6721
- async onItemCancelJob(job) {
6722
- const confirmed = await Dialog$1.showConfirm("Are you sure you want to cancel this job?");
6723
- if (job && confirmed) {
6724
- try {
6725
- const result = await job.cancel();
6726
- if (result.success) {
6727
- this.getApp().toast.success("Job cancelled successfully");
6728
- await this.collection.fetch();
6729
- } else {
6730
- this.getApp().toast.error(result.data?.error || "Failed to cancel job");
6731
- }
6732
- } catch (error) {
6733
- this.getApp().toast.error("Error cancelling job: " + error.message);
6734
- }
6735
- }
6736
- }
6737
- async onItemRetryJob(job) {
6738
- if (job) {
6739
- const result = await Dialog$1.showForm({
6740
- title: "Retry Job",
6741
- formConfig: JobForms.retry
6742
- });
6743
- if (result) {
6744
- try {
6745
- const retryResult = await job.retry(result.delay || 0);
6746
- if (retryResult.success) {
6747
- this.getApp().toast.success("Job queued for retry");
6748
- await this.collection.fetch();
6749
- } else {
6750
- this.getApp().toast.error(retryResult.data?.error || "Failed to retry job");
6751
- }
6752
- } catch (error) {
6753
- this.getApp().toast.error("Error retrying job: " + error.message);
6754
- }
6755
- }
6756
- }
6757
- }
6758
- async onItemCloneJob(job) {
6759
- if (job) {
6760
- const payload = job.getPayload();
6761
- const result = await Dialog$1.showForm({
6762
- title: "Clone Job",
6763
- formConfig: {
6764
- ...JobForms.clone,
6765
- fields: JobForms.clone.fields.map((field) => {
6766
- if (field.name === "payload") {
6767
- field.value = JSON.stringify(payload, null, 2);
6768
- } else if (field.name === "channel") {
6769
- field.value = job.get("channel");
6770
- }
6771
- return field;
6772
- })
6773
- }
6774
- });
6775
- if (result) {
6776
- try {
6777
- let newPayload = {};
6778
- if (result.payload) {
6779
- newPayload = JSON.parse(result.payload);
6780
- }
6781
- const cloneData = {
6782
- payload: newPayload,
6783
- channel: result.channel || job.get("channel"),
6784
- delay: result.delay || 0
6785
- };
6786
- const cloneResult = await job.cloneJob(cloneData);
6787
- if (cloneResult.success) {
6788
- this.getApp().toast.success("Job cloned successfully");
6789
- await this.collection.fetch();
6790
- } else {
6791
- this.getApp().toast.error(cloneResult.data?.error || "Failed to clone job");
6792
- }
6793
- } catch (error) {
6794
- this.getApp().toast.error("Error cloning job: " + error.message);
6795
- }
6796
- }
6797
- }
6798
- }
6799
- }
6800
- class RunnersTable extends TableView {
6801
- constructor(options = {}) {
6802
- super({
6803
- Collection: JobRunnerList,
6804
- options: {
6805
- searchable: true,
6806
- sortable: true,
6807
- paginated: true,
6808
- size: 10,
6809
- ...options.options
6587
+ async onInit() {
6588
+ this.chart = new MetricsChart({
6589
+ containerId: "job-metrics-modal-chart",
6590
+ title: '<i class="bi bi-graph-up me-2"></i> Job Channel Metrics',
6591
+ endpoint: "/api/metrics/fetch",
6592
+ height: 320,
6593
+ granularity: "hours",
6594
+ category: "jobs_channels",
6595
+ account: "global",
6596
+ chartType: "bar",
6597
+ showDateRange: true,
6598
+ yAxis: {
6599
+ label: "Count",
6600
+ beginAtZero: true
6810
6601
  },
6811
- columns: [
6812
- {
6813
- key: "runner_id",
6814
- label: "Runner ID",
6815
- formatter: "truncate_middle(16)",
6816
- sortable: true
6817
- },
6818
- {
6819
- key: "alive",
6820
- label: "Status",
6821
- formatter: (value) => {
6822
- const isAlive = value === true;
6823
- const badgeClass = isAlive ? "bg-success" : "bg-danger";
6824
- const icon = isAlive ? "bi-check-circle-fill" : "bi-x-octagon-fill";
6825
- const text = isAlive ? "ALIVE" : "DEAD";
6826
- return `<span class="badge ${badgeClass}"><i class="${icon} me-1"></i>${text}</span>`;
6827
- },
6828
- sortable: true,
6829
- filter: {
6830
- type: "select",
6831
- options: [
6832
- { value: true, label: "Alive" },
6833
- { value: false, label: "Dead" }
6834
- ]
6835
- }
6836
- },
6837
- {
6838
- key: "channels",
6839
- label: "Channels",
6840
- formatter: (channels) => {
6841
- if (!channels || !channels.length) return "None";
6842
- return channels.map((c2) => `<span class="badge bg-secondary me-1">${c2}</span>`).join("");
6843
- },
6844
- sortable: false
6845
- },
6846
- {
6847
- key: "jobs_processed",
6848
- label: "Processed",
6849
- sortable: true
6850
- },
6851
- {
6852
- key: "jobs_failed",
6853
- label: "Failed",
6854
- formatter: (value) => {
6855
- const badgeClass = value > 0 ? "bg-danger" : "bg-success";
6856
- return `<span class="badge ${badgeClass}">${value}</span>`;
6857
- },
6858
- sortable: true
6859
- },
6860
- {
6861
- key: "last_heartbeat",
6862
- label: "Last Heartbeat",
6863
- formatter: (value) => {
6864
- if (!value) return "Never";
6865
- const heartbeatTime = new Date(value);
6866
- const now = /* @__PURE__ */ new Date();
6867
- const diffMs = now - heartbeatTime;
6868
- const diffSeconds = Math.floor(diffMs / 1e3);
6869
- if (diffSeconds < 60) return `${diffSeconds}s ago`;
6870
- if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`;
6871
- return `${Math.floor(diffSeconds / 3600)}h ago`;
6872
- },
6873
- sortable: true
6874
- },
6875
- {
6876
- key: "started",
6877
- label: "Uptime",
6878
- formatter: (value) => {
6879
- if (!value) return "Unknown";
6880
- const startTime = new Date(value);
6881
- const now = /* @__PURE__ */ new Date();
6882
- const diffMs = now - startTime;
6883
- const diffSeconds = Math.floor(diffMs / 1e3);
6884
- if (diffSeconds < 60) return `${diffSeconds}s`;
6885
- if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m`;
6886
- if (diffSeconds < 86400) return `${Math.floor(diffSeconds / 3600)}h`;
6887
- return `${Math.floor(diffSeconds / 86400)}d`;
6888
- },
6889
- sortable: true
6890
- }
6891
- ],
6892
- contextMenu: [
6893
- {
6894
- label: "Ping Runner",
6895
- action: "ping-runner",
6896
- icon: "bi-wifi"
6897
- },
6898
- {
6899
- label: "View Details",
6900
- action: "view-runner-details",
6901
- icon: "bi-info-circle"
6902
- },
6903
- { separator: true },
6904
- {
6905
- label: "Pause Runner",
6906
- action: "pause-runner",
6907
- icon: "bi-pause-circle",
6908
- condition: (runner) => runner.get("alive") === true
6909
- },
6910
- {
6911
- label: "Resume Runner",
6912
- action: "resume-runner",
6913
- icon: "bi-play-circle",
6914
- condition: (runner) => runner.get("alive") !== true
6915
- },
6916
- { separator: true },
6917
- {
6918
- label: "Shutdown Runner",
6919
- action: "shutdown-runner",
6920
- icon: "bi-power",
6921
- danger: true,
6922
- condition: (runner) => runner.get("alive") === true
6923
- }
6924
- ],
6925
- ...options
6926
- });
6927
- }
6928
- async onActionPingRunner(event, element) {
6929
- const runnerId = element.getAttribute("data-id");
6930
- const runner = this.collection.get(runnerId);
6931
- if (runner) {
6932
- try {
6933
- const result = await runner.ping();
6934
- if (result.success) {
6935
- this.getApp().toast.success("Runner ping successful");
6936
- await this.collection.fetch();
6937
- } else {
6938
- this.getApp().toast.error(result.data?.error || "Runner ping failed");
6939
- }
6940
- } catch (error) {
6941
- this.getApp().toast.error("Error pinging runner: " + error.message);
6942
- }
6943
- }
6944
- }
6945
- async onActionShutdownRunner(event, element) {
6946
- const runnerId = element.getAttribute("data-id");
6947
- const runner = this.collection.get(runnerId);
6948
- if (runner && confirm("Are you sure you want to shutdown this runner?")) {
6949
- try {
6950
- const result = await runner.shutdown(true);
6951
- if (result.success) {
6952
- this.getApp().toast.success("Runner shutdown initiated");
6953
- await this.collection.fetch();
6954
- } else {
6955
- this.getApp().toast.error(result.data?.error || "Failed to shutdown runner");
6956
- }
6957
- } catch (error) {
6958
- this.getApp().toast.error("Error shutting down runner: " + error.message);
6602
+ tooltip: {
6603
+ y: "number:0"
6959
6604
  }
6960
- }
6961
- }
6962
- }
6963
- class ScheduledJobsTable extends TableView {
6964
- constructor(options = {}) {
6965
- super({
6966
- Collection: JobList,
6967
- collectionParams: { status: "pending" },
6968
- hideActivePillNames: ["status"],
6969
- options: {
6970
- searchable: true,
6971
- sortable: true,
6972
- paginated: true,
6973
- size: 10,
6974
- ...options.options
6975
- },
6976
- columns: [
6977
- {
6978
- key: "id",
6979
- label: "Job ID",
6980
- formatter: "truncate_middle(12)",
6981
- sortable: true
6982
- },
6983
- {
6984
- key: "func",
6985
- label: "Function",
6986
- sortable: true
6987
- },
6988
- {
6989
- key: "channel",
6990
- label: "Channel",
6991
- formatter: "badge",
6992
- sortable: true
6993
- },
6994
- {
6995
- key: "run_at",
6996
- label: "Scheduled For",
6997
- formatter: "datetime",
6998
- sortable: true
6999
- },
7000
- {
7001
- key: "created",
7002
- label: "Created",
7003
- formatter: "datetime",
7004
- sortable: true
7005
- },
7006
- {
7007
- key: "expires_at",
7008
- label: "Expires At",
7009
- formatter: "datetime",
7010
- sortable: true
7011
- }
7012
- ],
7013
- ...options
7014
6605
  });
6606
+ this.addChild(this.chart);
7015
6607
  }
7016
6608
  }
7017
6609
  class JobsAdminPage extends Page {
@@ -7035,7 +6627,7 @@ class JobsAdminPage extends Page {
7035
6627
  <p class="text-muted mb-0">{{pageSubtitle}}</p>
7036
6628
  <small class="text-info">
7037
6629
  <i class="bi bi-arrow-clockwise me-1"></i>
7038
- Auto-refresh: {{refreshRateSeconds}}s | Last updated: {{lastUpdated}}
6630
+ Auto-refresh: {{refreshRateLabel}} | Last updated: {{lastUpdated}}
7039
6631
  </small>
7040
6632
  </div>
7041
6633
  <div class="btn-group" role="group">
@@ -7080,28 +6672,106 @@ class JobsAdminPage extends Page {
7080
6672
  </div>
7081
6673
  </div>
7082
6674
 
7083
- <!-- Job Stats -->
7084
6675
  <div data-container="job-stats"></div>
7085
6676
 
7086
- <!-- Job Health -->
6677
+ <div class="row mb-4 g-3 align-items-stretch">
6678
+ <div class="col-lg-6" data-container="jobs-published-chart"></div>
6679
+ <div class="col-lg-6 position-relative">
6680
+ <div data-container="jobs-failed-chart"></div>
6681
+ <button class="btn btn-link btn-sm text-decoration-none position-absolute top-0 end-0"
6682
+ data-action="open-job-metrics-modal">
6683
+ <i class="bi bi-graph-up"></i> Channel Metrics
6684
+ </button>
6685
+ </div>
6686
+ </div>
6687
+
7087
6688
  <div class="row">
7088
- <div class="col-12">
6689
+ <div class="col-xxl-6 col-lg-6 mb-4">
7089
6690
  <div data-container="job-health"></div>
7090
6691
  </div>
7091
- <div class="col-12">
7092
- <div class="mb-3" data-container="job-metrics"></div>
6692
+ <div class="col-xxl-6 col-lg-6 mb-4">
6693
+ <div class="card shadow-sm h-100">
6694
+ <div class="card-header d-flex justify-content-between align-items-center">
6695
+ <h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Job Runners</h5>
6696
+ <small class="text-muted">Heartbeat &amp; status</small>
6697
+ </div>
6698
+ <div class="card-body p-0" data-container="runner-table"></div>
6699
+ </div>
6700
+ </div>
6701
+ </div>
6702
+
6703
+ <div class="row">
6704
+ <div class="col-xl-6 mb-4">
6705
+ <div class="card shadow-sm h-100">
6706
+ <div class="card-header d-flex justify-content-between align-items-center">
6707
+ <h5 class="mb-0"><i class="bi bi-play-circle me-2"></i>Running Jobs</h5>
6708
+ <small class="text-muted">Currently executing</small>
6709
+ </div>
6710
+ <div class="card-body p-0" data-container="running-jobs-table"></div>
6711
+ </div>
6712
+ </div>
6713
+ <div class="col-xl-6 mb-4">
6714
+ <div class="card shadow-sm h-100">
6715
+ <div class="card-header d-flex justify-content-between align-items-center">
6716
+ <h5 class="mb-0"><i class="bi bi-hourglass-split me-2"></i>Pending Jobs</h5>
6717
+ <small class="text-muted">Waiting in queue</small>
6718
+ </div>
6719
+ <div class="card-body p-0" data-container="pending-jobs-table"></div>
6720
+ </div>
6721
+ </div>
6722
+ </div>
6723
+
6724
+ <div class="row">
6725
+ <div class="col-xl-6 mb-4">
6726
+ <div class="card shadow-sm h-100">
6727
+ <div class="card-header">
6728
+ <h5 class="mb-0"><i class="bi bi-calendar-event me-2"></i>Scheduled Jobs</h5>
6729
+ </div>
6730
+ <div class="card-body p-0" data-container="scheduled-jobs-table"></div>
6731
+ </div>
6732
+ </div>
6733
+ <div class="col-xl-6 mb-4">
6734
+ <div class="card shadow-sm h-100">
6735
+ <div class="card-header d-flex justify-content-between align-items-center">
6736
+ <h5 class="mb-0"><i class="bi bi-bug me-2"></i>Failed Jobs</h5>
6737
+ <small class="text-muted">Latest errors</small>
6738
+ </div>
6739
+ <div class="card-body p-0" data-container="failed-jobs-table"></div>
6740
+ </div>
7093
6741
  </div>
7094
6742
  </div>
7095
6743
 
7096
- <!-- Job Tables -->
7097
- <div class="card border shadow">
7098
- <div class="card-header">
7099
- <h5 class="card-title mb-0">
7100
- <i class="bi bi-list-task me-2"></i>Job Management
7101
- </h5>
6744
+ <div class="card shadow-sm mb-5">
6745
+ <div class="card-header d-flex justify-content-between align-items-center">
6746
+ <h5 class="mb-0"><i class="bi bi-tools me-2"></i>Operations</h5>
6747
+ <button class="btn btn-outline-secondary btn-sm" data-action="view-all-jobs">
6748
+ <i class="bi bi-table"></i> View All Jobs
6749
+ </button>
7102
6750
  </div>
7103
6751
  <div class="card-body">
7104
- <div data-container="job-tables"></div>
6752
+ <div class="d-flex flex-wrap gap-2">
6753
+ <button class="btn btn-outline-primary" data-action="run-simple-job">
6754
+ <i class="bi bi-play-circle me-2"></i>Run Simple Job
6755
+ </button>
6756
+ <button class="btn btn-outline-primary" data-action="run-test-jobs">
6757
+ <i class="bi bi-robot me-2"></i>Run Test Jobs
6758
+ </button>
6759
+ <button class="btn btn-outline-warning" data-action="clear-stuck">
6760
+ <i class="bi bi-wrench me-2"></i>Clear Stuck
6761
+ </button>
6762
+ <button class="btn btn-outline-warning" data-action="clear-channel">
6763
+ <i class="bi bi-eraser me-2"></i>Clear Channel
6764
+ </button>
6765
+ <button class="btn btn-outline-danger" data-action="purge-jobs">
6766
+ <i class="bi bi-trash me-2"></i>Purge Jobs
6767
+ </button>
6768
+ <button class="btn btn-outline-info" data-action="cleanup-consumers">
6769
+ <i class="bi bi-people me-2"></i>Cleanup Consumers
6770
+ </button>
6771
+ <button class="btn btn-outline-secondary" data-action="runner-broadcast">
6772
+ <i class="bi bi-wifi me-2"></i>Broadcast Command
6773
+ </button>
6774
+ </div>
7105
6775
  </div>
7106
6776
  </div>
7107
6777
  </div>
@@ -7119,36 +6789,308 @@ class JobsAdminPage extends Page {
7119
6789
  model: this.jobStats
7120
6790
  });
7121
6791
  this.addChild(this.jobHealthView);
7122
- this.jobTablesView = new TabView({
7123
- containerId: "job-tables",
7124
- tabs: {
7125
- "Jobs": new JobsTable(),
7126
- "Runners": new RunnersTable(),
7127
- "Scheduled": new ScheduledJobsTable()
6792
+ this.jobsPublishedChart = new MetricsMiniChartWidget({
6793
+ containerId: "jobs-published-chart",
6794
+ icon: "bi bi-upload",
6795
+ title: "Jobs Published",
6796
+ subtitle: "{{now_value}} {{now_label}}",
6797
+ granularity: "days",
6798
+ slugs: ["jobs.published"],
6799
+ account: "global",
6800
+ chartType: "line",
6801
+ height: 90,
6802
+ showSettings: true,
6803
+ showTrending: true,
6804
+ showDateRange: false
6805
+ });
6806
+ this.addChild(this.jobsPublishedChart);
6807
+ this.jobsFailedChart = new MetricsMiniChartWidget({
6808
+ containerId: "jobs-failed-chart",
6809
+ icon: "bi bi-exclamation-octagon",
6810
+ title: "Jobs Failed",
6811
+ subtitle: "{{now_value}} {{now_label}}",
6812
+ granularity: "days",
6813
+ slugs: ["jobs.failed"],
6814
+ account: "global",
6815
+ chartType: "line",
6816
+ height: 90,
6817
+ showSettings: true,
6818
+ showTrending: true,
6819
+ showDateRange: false
6820
+ });
6821
+ this.addChild(this.jobsFailedChart);
6822
+ this.runningJobsTable = new TableView({
6823
+ containerId: "running-jobs-table",
6824
+ Collection: JobList,
6825
+ collectionParams: {
6826
+ size: 5,
6827
+ sort: "-created",
6828
+ status: "running"
7128
6829
  },
7129
- activeTab: "Jobs"
6830
+ searchable: true,
6831
+ filterable: false,
6832
+ paginated: true,
6833
+ itemView: JobDetailsView,
6834
+ hideActivePills: ["status"],
6835
+ viewDialogOptions: {
6836
+ title: "Job Details",
6837
+ size: "xl",
6838
+ scrollable: true
6839
+ },
6840
+ tableOptions: {
6841
+ striped: false,
6842
+ hover: true,
6843
+ size: "sm"
6844
+ },
6845
+ columns: [
6846
+ {
6847
+ key: "id",
6848
+ label: "Job",
6849
+ template: `
6850
+ <div class="fw-semibold font-monospace">{{model.id|truncate_middle(12)}}</div>
6851
+ <div class="text-muted small">{{model.channel}} &middot; {{model.func|truncate_middle(28)|default('n/a')}}</div>
6852
+ `
6853
+ },
6854
+ {
6855
+ key: "runner_id",
6856
+ label: "Runner",
6857
+ template: `
6858
+ <span class="font-monospace">{{model.runner_id|truncate_middle(12)|default('n/a')}}</span>
6859
+ `
6860
+ },
6861
+ {
6862
+ key: "status",
6863
+ label: "State",
6864
+ formatter: (value, context) => {
6865
+ const job = context.row;
6866
+ const badgeClass = job.getStatusBadgeClass ? job.getStatusBadgeClass() : "bg-secondary";
6867
+ const icon = job.getStatusIcon ? job.getStatusIcon() : "bi-question";
6868
+ return `<span class="badge ${badgeClass}"><i class="${icon} me-1"></i>${value.toUpperCase()}</span>`;
6869
+ }
6870
+ },
6871
+ {
6872
+ key: "created",
6873
+ label: "Started",
6874
+ formatter: "datetime"
6875
+ }
6876
+ ]
7130
6877
  });
7131
- this.addChild(this.jobTablesView);
7132
- this.jobMetricsChart = new MetricsChart({
7133
- title: `<i class="bi bi-graph-up me-2"></i> Job Metrics`,
7134
- endpoint: "/api/metrics/fetch",
7135
- height: 100,
7136
- granularity: "hours",
7137
- category: "jobs_channels",
7138
- account: "global",
7139
- chartType: "bar",
7140
- showDateRange: false,
7141
- yAxis: {
7142
- label: "Count",
7143
- beginAtZero: true
6878
+ this.addChild(this.runningJobsTable);
6879
+ this.pendingJobsTable = new TableView({
6880
+ containerId: "pending-jobs-table",
6881
+ Collection: JobList,
6882
+ collectionParams: {
6883
+ size: 5,
6884
+ sort: "-created",
6885
+ status: "pending",
6886
+ run_at__isnull: true
7144
6887
  },
7145
- tooltip: {
7146
- y: "number"
6888
+ searchable: true,
6889
+ filterable: false,
6890
+ paginated: true,
6891
+ selectable: true,
6892
+ batchBarLocation: "top",
6893
+ batchActions: [
6894
+ {
6895
+ icon: "bi-x-circle-fill",
6896
+ label: "Cancel Jobs",
6897
+ action: "cancel-jobs"
6898
+ }
6899
+ ],
6900
+ itemView: JobDetailsView,
6901
+ hideActivePills: ["status"],
6902
+ viewDialogOptions: {
6903
+ title: "Job Details",
6904
+ size: "xl",
6905
+ scrollable: true
6906
+ },
6907
+ tableOptions: {
6908
+ striped: false,
6909
+ hover: true,
6910
+ size: "sm"
7147
6911
  },
7148
- containerId: "job-metrics"
6912
+ columns: [
6913
+ {
6914
+ key: "id",
6915
+ label: "Job",
6916
+ template: `
6917
+ <div class="fw-semibold font-monospace">{{model.id|truncate_middle(12)}}</div>
6918
+ <div class="text-muted small">{{model.channel}} &middot; {{model.func|truncate_middle(28)|default('n/a')}}</div>
6919
+ `
6920
+ },
6921
+ {
6922
+ key: "priority",
6923
+ label: "Priority",
6924
+ formatter: (value = 0) => {
6925
+ const badge = value >= 8 ? "bg-danger" : value >= 5 ? "bg-warning" : "bg-secondary";
6926
+ return `<span class="badge ${badge}">${value}</span>`;
6927
+ }
6928
+ },
6929
+ {
6930
+ key: "modified",
6931
+ label: "Queued",
6932
+ formatter: "relative"
6933
+ }
6934
+ ]
6935
+ });
6936
+ this.pendingJobsTable.on("action:batch-cancel-jobs", async (action, event, element) => {
6937
+ const items = this.pendingJobsTable.getSelectedItems();
6938
+ await Promise.all(items.map((item) => item.model.cancel()));
6939
+ this.getApp().toast.success("Jobs cancelled successfully");
6940
+ this.pendingJobsTable.collection.fetch();
6941
+ });
6942
+ this.addChild(this.pendingJobsTable);
6943
+ this.failedJobsTable = new TableView({
6944
+ containerId: "failed-jobs-table",
6945
+ Collection: JobList,
6946
+ collectionParams: {
6947
+ size: 5,
6948
+ sort: "-finished_at",
6949
+ status: "failed"
6950
+ },
6951
+ searchable: true,
6952
+ filterable: false,
6953
+ paginated: true,
6954
+ itemView: JobDetailsView,
6955
+ viewDialogOptions: {
6956
+ title: "Job Details",
6957
+ size: "xl",
6958
+ scrollable: true
6959
+ },
6960
+ hideActivePills: ["status"],
6961
+ tableOptions: {
6962
+ striped: false,
6963
+ hover: true,
6964
+ size: "sm"
6965
+ },
6966
+ columns: [
6967
+ {
6968
+ key: "id",
6969
+ label: "Job",
6970
+ template: `
6971
+ <div class="fw-semibold font-monospace">{{model.id|truncate_middle(12)}}</div>
6972
+ <div class="text-muted small">{{model.channel}} &middot; {{model.func|truncate_middle(28)|default('n/a')}}</div>
6973
+ `
6974
+ },
6975
+ {
6976
+ key: "last_error",
6977
+ label: "Error",
6978
+ template: `
6979
+ <div class="text-danger small">{{model.last_error|truncate(80)|default('Unknown error')}}</div>
6980
+ `
6981
+ },
6982
+ {
6983
+ key: "modified",
6984
+ label: "Failed",
6985
+ formatter: "relative"
6986
+ }
6987
+ ]
6988
+ });
6989
+ this.addChild(this.failedJobsTable);
6990
+ this.scheduledJobsTable = new TableView({
6991
+ containerId: "scheduled-jobs-table",
6992
+ Collection: JobList,
6993
+ collectionParams: {
6994
+ size: 5,
6995
+ sort: "run_at",
6996
+ run_at__isnull: false,
6997
+ status: "pending"
6998
+ },
6999
+ searchable: true,
7000
+ filterable: false,
7001
+ paginated: true,
7002
+ itemView: JobDetailsView,
7003
+ hideActivePills: ["status"],
7004
+ viewDialogOptions: {
7005
+ title: "Job Details",
7006
+ size: "xl",
7007
+ scrollable: true
7008
+ },
7009
+ tableOptions: {
7010
+ striped: false,
7011
+ hover: true,
7012
+ size: "sm"
7013
+ },
7014
+ columns: [
7015
+ {
7016
+ key: "id",
7017
+ label: "Job",
7018
+ formatter: "truncate_middle(12)"
7019
+ },
7020
+ {
7021
+ key: "run_at",
7022
+ label: "Scheduled For",
7023
+ formatter: "datetime"
7024
+ },
7025
+ {
7026
+ key: "channel",
7027
+ label: "Channel",
7028
+ formatter: "badge"
7029
+ }
7030
+ ],
7031
+ selectable: true,
7032
+ batchBarLocation: "top",
7033
+ batchActions: [
7034
+ {
7035
+ icon: "bi-x-circle-fill",
7036
+ label: "Cancel Jobs",
7037
+ action: "cancel-jobs"
7038
+ }
7039
+ ]
7040
+ });
7041
+ this.scheduledJobsTable.on("action:batch-cancel-jobs", async (action, event, element) => {
7042
+ const items = this.scheduledJobsTable.getSelectedItems();
7043
+ await Promise.all(items.map((item) => item.model.cancel()));
7044
+ this.getApp().toast.success("Jobs cancelled successfully");
7045
+ this.scheduledJobsTable.collection.fetch();
7149
7046
  });
7150
- this.addChild(this.jobMetricsChart);
7151
- await this.jobStats.fetch();
7047
+ this.addChild(this.scheduledJobsTable);
7048
+ this.runnersTable = new TableView({
7049
+ containerId: "runner-table",
7050
+ Collection: JobRunnerList,
7051
+ searchable: true,
7052
+ filterable: false,
7053
+ paginated: true,
7054
+ tableOptions: {
7055
+ striped: false,
7056
+ hover: true,
7057
+ size: "sm"
7058
+ },
7059
+ columns: [
7060
+ {
7061
+ key: "runner_id",
7062
+ label: "Runner",
7063
+ formatter: "truncate_middle(16)"
7064
+ },
7065
+ {
7066
+ key: "alive",
7067
+ label: "Status",
7068
+ formatter: (value) => {
7069
+ const isAlive = value === true;
7070
+ const badgeClass = isAlive ? "bg-success" : "bg-danger";
7071
+ const icon = isAlive ? "bi-check-circle-fill" : "bi-x-octagon-fill";
7072
+ const text = isAlive ? "ALIVE" : "DEAD";
7073
+ return `<span class="badge ${badgeClass}"><i class="${icon} me-1"></i>${text}</span>`;
7074
+ }
7075
+ },
7076
+ {
7077
+ key: "last_heartbeat",
7078
+ label: "Heartbeat",
7079
+ formatter: (value) => {
7080
+ if (!value) return "Never";
7081
+ const heartbeatTime = new Date(value);
7082
+ const now = /* @__PURE__ */ new Date();
7083
+ const diffMs = now - heartbeatTime;
7084
+ const diffSeconds = Math.floor(diffMs / 1e3);
7085
+ if (diffSeconds < 60) return `${diffSeconds}s ago`;
7086
+ if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`;
7087
+ return `${Math.floor(diffSeconds / 3600)}h ago`;
7088
+ }
7089
+ }
7090
+ ]
7091
+ });
7092
+ this.addChild(this.runnersTable);
7093
+ await this.refreshData();
7152
7094
  }
7153
7095
  // Auto-refresh management
7154
7096
  startAutoRefresh() {
@@ -7163,14 +7105,31 @@ class JobsAdminPage extends Page {
7163
7105
  }
7164
7106
  async refreshData() {
7165
7107
  try {
7166
- await this.jobStats.fetch();
7167
- const activeTab = this.jobTablesView?.getActiveTab();
7168
- if (activeTab) {
7169
- const activeTable = this.jobTablesView.getTab(activeTab);
7170
- if (activeTable?.collection?.fetch) {
7171
- await activeTable.collection.fetch();
7172
- }
7108
+ const tasks = [
7109
+ this.jobStats.fetch()
7110
+ ];
7111
+ if (this.jobsPublishedChart) {
7112
+ tasks.push(this.jobsPublishedChart.refresh());
7113
+ }
7114
+ if (this.jobsFailedChart) {
7115
+ tasks.push(this.jobsFailedChart.refresh());
7116
+ }
7117
+ if (this.runningJobsTable?.collection?.fetch) {
7118
+ tasks.push(this.runningJobsTable.collection.fetch());
7119
+ }
7120
+ if (this.pendingJobsTable?.collection?.fetch) {
7121
+ tasks.push(this.pendingJobsTable.collection.fetch());
7173
7122
  }
7123
+ if (this.failedJobsTable?.collection?.fetch) {
7124
+ tasks.push(this.failedJobsTable.collection.fetch());
7125
+ }
7126
+ if (this.scheduledJobsTable?.collection?.fetch) {
7127
+ tasks.push(this.scheduledJobsTable.collection.fetch());
7128
+ }
7129
+ if (this.runnersTable?.collection?.fetch) {
7130
+ tasks.push(this.runnersTable.collection.fetch());
7131
+ }
7132
+ await Promise.all(tasks);
7174
7133
  this.lastUpdated = (/* @__PURE__ */ new Date()).toLocaleString();
7175
7134
  this.updateHeaderTimestamp();
7176
7135
  } catch (error) {
@@ -7180,15 +7139,19 @@ class JobsAdminPage extends Page {
7180
7139
  updateHeaderTimestamp() {
7181
7140
  const timestampElement = this.element?.querySelector(".text-info");
7182
7141
  if (timestampElement) {
7142
+ const rateLabel = this.refreshRate === 0 ? "Off" : `${this.refreshRate / 1e3}s`;
7183
7143
  timestampElement.innerHTML = `
7184
7144
  <i class="bi bi-arrow-clockwise me-1"></i>
7185
- Auto-refresh: ${this.refreshRate / 1e3}s | Last updated: ${this.lastUpdated}
7145
+ Auto-refresh: ${rateLabel} | Last updated: ${this.lastUpdated}
7186
7146
  `;
7187
7147
  }
7188
7148
  }
7189
7149
  get refreshRateSeconds() {
7190
7150
  return this.refreshRate / 1e3;
7191
7151
  }
7152
+ get refreshRateLabel() {
7153
+ return this.refreshRate === 0 ? "Off" : `${this.refreshRateSeconds}s`;
7154
+ }
7192
7155
  // Action handlers
7193
7156
  async onActionRefreshAll(event, element) {
7194
7157
  try {
@@ -7196,7 +7159,6 @@ class JobsAdminPage extends Page {
7196
7159
  icon?.classList.add("spinning");
7197
7160
  element.disabled = true;
7198
7161
  await this.refreshData();
7199
- await this.render();
7200
7162
  } catch (error) {
7201
7163
  console.error("Failed to refresh jobs dashboard:", error);
7202
7164
  } finally {
@@ -7412,6 +7374,26 @@ class JobsAdminPage extends Page {
7412
7374
  getHealth() {
7413
7375
  return this.jobHealthView?.health || {};
7414
7376
  }
7377
+ async onActionOpenJobMetricsModal() {
7378
+ const modalView = new JobMetricsModalView();
7379
+ await Dialog$1.showDialog({
7380
+ title: '<i class="bi bi-graph-up me-2"></i>Job Channel Metrics',
7381
+ body: modalView,
7382
+ size: "xl",
7383
+ scrollable: true,
7384
+ buttons: [
7385
+ { text: "Close", class: "btn-secondary", dismiss: true }
7386
+ ]
7387
+ });
7388
+ }
7389
+ async onActionViewAllJobs() {
7390
+ const router = this.getApp()?.router;
7391
+ if (router) {
7392
+ router.navigateTo("/admin/jobs/table");
7393
+ } else {
7394
+ this.getApp()?.toast?.info("Router unavailable.");
7395
+ }
7396
+ }
7415
7397
  }
7416
7398
  class TaskDetailsView extends View {
7417
7399
  constructor(options = {}) {