web-mojo 2.1.827 → 2.1.917

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 (67) 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 +604 -62
  4. package/dist/admin.es.js.map +1 -1
  5. package/dist/auth.cjs.js +1 -1
  6. package/dist/auth.cjs.js.map +1 -1
  7. package/dist/auth.es.js +3 -3
  8. package/dist/auth.es.js.map +1 -1
  9. package/dist/charts.cjs.js +1 -1
  10. package/dist/charts.es.js +2 -2
  11. package/dist/chunks/{ChatView-DJK49g8O.js → ChatView-D9hJ2u6c.js} +372 -53
  12. package/dist/chunks/ChatView-D9hJ2u6c.js.map +1 -0
  13. package/dist/chunks/ChatView-u-UdWa-P.js +2 -0
  14. package/dist/chunks/ChatView-u-UdWa-P.js.map +1 -0
  15. package/dist/chunks/{ContextMenu-CRwv6-gc.js → ContextMenu-BlmrYFlZ.js} +3 -2
  16. package/dist/chunks/{ContextMenu-CRwv6-gc.js.map → ContextMenu-BlmrYFlZ.js.map} +1 -1
  17. package/dist/chunks/{ContextMenu-K8sVV2Sf.js → ContextMenu-CfXiUGrB.js} +2 -2
  18. package/dist/chunks/{ContextMenu-K8sVV2Sf.js.map → ContextMenu-CfXiUGrB.js.map} +1 -1
  19. package/dist/chunks/{DataView-3iqbZ_4y.js → DataView-BDUCD0q0.js} +11 -21
  20. package/dist/chunks/DataView-BDUCD0q0.js.map +1 -0
  21. package/dist/chunks/DataView-CorHtzkO.js +2 -0
  22. package/dist/chunks/DataView-CorHtzkO.js.map +1 -0
  23. package/dist/chunks/{Dialog-Bup1WdQn.js → Dialog-D9qIh5Jb.js} +5 -5
  24. package/dist/chunks/{Dialog-Bup1WdQn.js.map → Dialog-D9qIh5Jb.js.map} +1 -1
  25. package/dist/chunks/{Dialog-CEEzQOVl.js → Dialog-ifYDY_7S.js} +2 -2
  26. package/dist/chunks/{Dialog-CEEzQOVl.js.map → Dialog-ifYDY_7S.js.map} +1 -1
  27. package/dist/chunks/{FormView-C4Po3Q3z.js → FormView-Bq74K6dj.js} +4 -4
  28. package/dist/chunks/FormView-Bq74K6dj.js.map +1 -0
  29. package/dist/chunks/FormView-Dtzh5qLB.js +3 -0
  30. package/dist/chunks/FormView-Dtzh5qLB.js.map +1 -0
  31. package/dist/chunks/{MetricsMiniChartWidget-CxTnMgp6.js → MetricsMiniChartWidget-BHLlrFJf.js} +2 -2
  32. package/dist/chunks/{MetricsMiniChartWidget-CxTnMgp6.js.map → MetricsMiniChartWidget-BHLlrFJf.js.map} +1 -1
  33. package/dist/chunks/{MetricsMiniChartWidget-CK6K9Kh6.js → MetricsMiniChartWidget-CoapKHw0.js} +3 -3
  34. package/dist/chunks/{MetricsMiniChartWidget-CK6K9Kh6.js.map → MetricsMiniChartWidget-CoapKHw0.js.map} +1 -1
  35. package/dist/chunks/{PDFViewer-DgXB4rR9.js → PDFViewer-BXcYgXKx.js} +3 -3
  36. package/dist/chunks/{PDFViewer-DgXB4rR9.js.map → PDFViewer-BXcYgXKx.js.map} +1 -1
  37. package/dist/chunks/{PDFViewer-BvBw9BNk.js → PDFViewer-DxoRBrX9.js} +2 -2
  38. package/dist/chunks/{PDFViewer-BvBw9BNk.js.map → PDFViewer-DxoRBrX9.js.map} +1 -1
  39. package/dist/chunks/{Page-DaptCfWp.js → Page-CFzSUQLz.js} +2 -2
  40. package/dist/chunks/{Page-DaptCfWp.js.map → Page-CFzSUQLz.js.map} +1 -1
  41. package/dist/chunks/{Page-BJxsE5Os.js → Page-Cw4qW3aV.js} +2 -2
  42. package/dist/chunks/{Page-BJxsE5Os.js.map → Page-Cw4qW3aV.js.map} +1 -1
  43. package/dist/chunks/{TopNav-BjJojcF7.js → TopNav-DaX7k8M8.js} +5 -5
  44. package/dist/chunks/{TopNav-BjJojcF7.js.map → TopNav-DaX7k8M8.js.map} +1 -1
  45. package/dist/chunks/{TopNav-BkpUjJB2.js → TopNav-DgPBfS1e.js} +2 -2
  46. package/dist/chunks/{TopNav-BkpUjJB2.js.map → TopNav-DgPBfS1e.js.map} +1 -1
  47. package/dist/chunks/{WebApp-9L0mkLAO.js → WebApp-DHNZIzfN.js} +2 -2
  48. package/dist/chunks/{WebApp-9L0mkLAO.js.map → WebApp-DHNZIzfN.js.map} +1 -1
  49. package/dist/chunks/{WebApp-Cd21W4Hv.js → WebApp-MzDCFzar.js} +13 -13
  50. package/dist/chunks/{WebApp-Cd21W4Hv.js.map → WebApp-MzDCFzar.js.map} +1 -1
  51. package/dist/docit.cjs.js +1 -1
  52. package/dist/docit.es.js +5 -5
  53. package/dist/index.cjs.js +1 -1
  54. package/dist/index.cjs.js.map +1 -1
  55. package/dist/index.es.js +58 -52
  56. package/dist/lightbox.cjs.js +1 -1
  57. package/dist/lightbox.es.js +4 -4
  58. package/package.json +1 -1
  59. package/dist/chunks/ChatView-CM0ATZID.js +0 -2
  60. package/dist/chunks/ChatView-CM0ATZID.js.map +0 -1
  61. package/dist/chunks/ChatView-DJK49g8O.js.map +0 -1
  62. package/dist/chunks/DataView-3iqbZ_4y.js.map +0 -1
  63. package/dist/chunks/DataView-DoKRJHii.js +0 -2
  64. package/dist/chunks/DataView-DoKRJHii.js.map +0 -1
  65. package/dist/chunks/FormView-BEIw_LH9.js +0 -3
  66. package/dist/chunks/FormView-BEIw_LH9.js.map +0 -1
  67. package/dist/chunks/FormView-C4Po3Q3z.js.map +0 -1
package/dist/admin.es.js CHANGED
@@ -1,13 +1,13 @@
1
- import { P as Page } from "./chunks/Page-DaptCfWp.js";
2
- import { V as View, h as MOJOUtils } from "./chunks/WebApp-Cd21W4Hv.js";
3
- import { B, b, a, c, e, f, W } from "./chunks/WebApp-Cd21W4Hv.js";
4
- import Dialog$1 from "./chunks/Dialog-Bup1WdQn.js";
5
- import { M as MetricsChart, c as MetricsMiniChartWidget, P as PieChart } from "./chunks/MetricsMiniChartWidget-CK6K9Kh6.js";
6
- import { b as TablePage, k as EmailDomainForms, j as EmailDomainList, E as EmailDomain, n as MailboxForms, m as MailboxList, l as Mailbox, r as EmailTemplate, d as TabView, t as EmailTemplateForms, s as EmailTemplateList, I as IncidentEvent, B as IncidentEventForms, A as IncidentEventList, w as FileManagerForms, v as FileManagerList, x as File, T as TableView, z as FileForms, y as FileList, ap as GeoLocatedIP, aq as GeoLocatedIPList, aa as MemberList, a9 as LogList, as as TicketList, G as IncidentList, Y as IncidentStats, R as IncidentHistoryList, Q as IncidentHistory, D as Incident, C as ChatView, H as IncidentForms, Z as Job, a3 as JobEventList, a1 as JobLogList, $ as JobForms, _ as JobList, a6 as JobRunnerList, a4 as JobsEngineStats, a7 as JobRunnerForms, a5 as JobRunner, a8 as Log, M as Member, ab as MemberForms, ac as MetricsPermission, ae as MetricsForms, ad as MetricsPermissionList, an as PushConfigForms, ak as PushConfigList, am as PushDeliveryList, ag as PushDeviceList, ao as PushTemplateForms, ai as PushTemplateList, U as RuleSet, X as RuleList, V as RuleSetList, i as S3BucketForms, h as S3BucketList, o as SentMessage, p as SentMessageList, au as TicketNoteList, at as TicketNote, ar as Ticket, av as TicketForms, aw as TicketCategories } from "./chunks/ChatView-DJK49g8O.js";
7
- import DataView from "./chunks/DataView-3iqbZ_4y.js";
8
- import { b as ContextMenu, C as Collection, a as Group, G as GroupList, c as GroupForms, g as UserDevice, j as UserDeviceLocationList, h as UserDeviceList, U as User, f as UserDataView, e as UserForms, d as UserList } from "./chunks/ContextMenu-CRwv6-gc.js";
9
- import { L as LightboxGallery, P as PDFViewer } from "./chunks/PDFViewer-DgXB4rR9.js";
10
- import { a as applyFileDropMixin, F as FormView } from "./chunks/FormView-C4Po3Q3z.js";
1
+ import { P as Page } from "./chunks/Page-CFzSUQLz.js";
2
+ import { V as View, h as MOJOUtils } from "./chunks/WebApp-MzDCFzar.js";
3
+ import { B, b, a, c, e, f, W } from "./chunks/WebApp-MzDCFzar.js";
4
+ import Dialog$1 from "./chunks/Dialog-D9qIh5Jb.js";
5
+ import { M as MetricsChart, c as MetricsMiniChartWidget, P as PieChart } from "./chunks/MetricsMiniChartWidget-CoapKHw0.js";
6
+ import { b as TablePage, k as EmailDomainForms, j as EmailDomainList, E as EmailDomain, n as MailboxForms, m as MailboxList, l as Mailbox, r as EmailTemplate, d as TabView, t as EmailTemplateForms, s as EmailTemplateList, I as IncidentEvent, B as IncidentEventForms, A as IncidentEventList, w as FileManagerForms, v as FileManagerList, x as File, T as TableView, z as FileForms, y as FileList, av as GeoLocatedIP, aw as GeoLocatedIPList, ag as MemberList, af as LogList, ay as TicketList, G as IncidentList, _ as IncidentStats, R as IncidentHistoryList, Q as IncidentHistory, D as Incident, C as ChatView, H as IncidentForms, a3 as Job, a9 as JobEventList, a7 as JobLogList, a5 as JobForms, a4 as JobList, ac as JobRunnerList, aa as JobsEngineStats, ad as JobRunnerForms, ab as JobRunner, ae as Log, M as Member, ah as MemberForms, ai as MetricsPermission, ak as MetricsForms, aj as MetricsPermissionList, at as PushConfigForms, aq as PushConfigList, as as PushDeliveryList, am as PushDeviceList, au as PushTemplateForms, ao as PushTemplateList, U as RuleSet, a0 as MatchByOptions, $ as BundleByOptions, Y as RuleList, V as RuleSetList, i as S3BucketForms, h as S3BucketList, o as SentMessage, p as SentMessageList, aA as TicketNoteList, az as TicketNote, ax as Ticket, aB as TicketForms, aC as TicketCategories } from "./chunks/ChatView-D9hJ2u6c.js";
7
+ import DataView from "./chunks/DataView-BDUCD0q0.js";
8
+ import { b as ContextMenu, C as Collection, a as Group, G as GroupList, c as GroupForms, g as UserDevice, j as UserDeviceLocationList, h as UserDeviceList, U as User, f as UserDataView, e as UserForms, d as UserList } from "./chunks/ContextMenu-BlmrYFlZ.js";
9
+ import { L as LightboxGallery, P as PDFViewer } from "./chunks/PDFViewer-BXcYgXKx.js";
10
+ import { a as applyFileDropMixin, F as FormView } from "./chunks/FormView-Bq74K6dj.js";
11
11
  class AdminHeaderView extends View {
12
12
  constructor(options = {}) {
13
13
  super({
@@ -831,6 +831,129 @@ class EmailTemplateTablePage extends TablePage {
831
831
  });
832
832
  }
833
833
  }
834
+ class StackTraceView extends View {
835
+ constructor(options = {}) {
836
+ super({
837
+ className: "stack-trace-view",
838
+ ...options
839
+ });
840
+ this.stackTrace = options.stackTrace || "";
841
+ this.template = `
842
+ <div class="stack-trace-container p-3">
843
+ <style>
844
+ .stack-trace-line {
845
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
846
+ font-size: 13px;
847
+ line-height: 1.6;
848
+ padding: 4px 8px;
849
+ margin: 0;
850
+ border-left: 3px solid transparent;
851
+ }
852
+ .stack-trace-line:hover {
853
+ background-color: rgba(0, 0, 0, 0.05);
854
+ }
855
+ .stack-trace-error {
856
+ color: #dc3545;
857
+ font-weight: 600;
858
+ border-left-color: #dc3545;
859
+ background-color: rgba(220, 53, 69, 0.05);
860
+ }
861
+ .stack-trace-file {
862
+ color: #0d6efd;
863
+ font-weight: 500;
864
+ border-left-color: #0d6efd;
865
+ }
866
+ .stack-trace-function {
867
+ color: #6610f2;
868
+ font-weight: 500;
869
+ }
870
+ .stack-trace-location {
871
+ color: #6c757d;
872
+ font-size: 12px;
873
+ }
874
+ .stack-trace-line-number {
875
+ color: #fd7e14;
876
+ font-weight: 600;
877
+ }
878
+ .stack-trace-context {
879
+ color: #495057;
880
+ background-color: rgba(0, 0, 0, 0.02);
881
+ }
882
+ .stack-trace-container {
883
+ background-color: #f8f9fa;
884
+ border: 1px solid #dee2e6;
885
+ border-radius: 0.375rem;
886
+ max-height: 600px;
887
+ overflow-y: auto;
888
+ }
889
+ </style>
890
+ <div class="stack-trace-content">
891
+ {{{formattedStackTrace}}}
892
+ </div>
893
+ </div>
894
+ `;
895
+ }
896
+ async onBeforeRender() {
897
+ this.formattedStackTrace = this.formatStackTrace(this.stackTrace);
898
+ }
899
+ formatStackTrace(stackTrace) {
900
+ if (!stackTrace) {
901
+ return '<div class="text-muted p-3">No stack trace available</div>';
902
+ }
903
+ const traceStr = typeof stackTrace === "string" ? stackTrace : JSON.stringify(stackTrace, null, 2);
904
+ const lines = traceStr.split("\n");
905
+ let html = "";
906
+ lines.forEach((line, index) => {
907
+ if (!line.trim()) {
908
+ html += '<div class="stack-trace-line">&nbsp;</div>';
909
+ return;
910
+ }
911
+ if (index === 0 && (line.includes("Error:") || line.includes("Exception:"))) {
912
+ html += `<div class="stack-trace-line stack-trace-error">${this.escapeHtml(line)}</div>`;
913
+ return;
914
+ }
915
+ const filePattern = /(.+?)\s*\(([^:]+):(\d+):(\d+)\)/;
916
+ const simpleFilePattern = /^\s*at\s+([^:]+):(\d+):(\d+)/;
917
+ let match = line.match(filePattern);
918
+ if (match) {
919
+ const [, funcName, filePath, lineNum, colNum] = match;
920
+ html += `<div class="stack-trace-line stack-trace-file">
921
+ <span class="stack-trace-function">${this.escapeHtml(funcName.trim())}</span>
922
+ <span class="stack-trace-location"> (${this.escapeHtml(filePath)}:<span class="stack-trace-line-number">${lineNum}</span>:${colNum})</span>
923
+ </div>`;
924
+ return;
925
+ }
926
+ match = line.match(simpleFilePattern);
927
+ if (match) {
928
+ const [, filePath, lineNum, colNum] = match;
929
+ html += `<div class="stack-trace-line stack-trace-file">
930
+ <span class="stack-trace-location">at ${this.escapeHtml(filePath)}:<span class="stack-trace-line-number">${lineNum}</span>:${colNum}</span>
931
+ </div>`;
932
+ return;
933
+ }
934
+ const pythonPattern = /File\s+"([^"]+)",\s+line\s+(\d+),\s+in\s+(.+)/;
935
+ match = line.match(pythonPattern);
936
+ if (match) {
937
+ const [, filePath, lineNum, funcName] = match;
938
+ html += `<div class="stack-trace-line stack-trace-file">
939
+ <span class="stack-trace-location">File "${this.escapeHtml(filePath)}", line <span class="stack-trace-line-number">${lineNum}</span>, in </span>
940
+ <span class="stack-trace-function">${this.escapeHtml(funcName)}</span>
941
+ </div>`;
942
+ return;
943
+ }
944
+ if (line.trim().startsWith("at ")) {
945
+ html += `<div class="stack-trace-line stack-trace-file">${this.escapeHtml(line)}</div>`;
946
+ return;
947
+ }
948
+ html += `<div class="stack-trace-line stack-trace-context">${this.escapeHtml(line)}</div>`;
949
+ });
950
+ return html;
951
+ }
952
+ updateStackTrace(newStackTrace) {
953
+ this.stackTrace = newStackTrace;
954
+ this.render();
955
+ }
956
+ }
834
957
  class EventView extends View {
835
958
  constructor(options = {}) {
836
959
  super({
@@ -886,20 +1009,20 @@ class EventView extends View {
886
1009
  { name: "details", label: "Details", columns: 12 }
887
1010
  ]
888
1011
  });
889
- this.metadataView = new View({
890
- model: this.model,
891
- template: `<pre class="bg-light p-3 border rounded"><code>{{{model.metadata|json}}}</code></pre>`
892
- });
893
1012
  const tabs = { "Overview": this.overviewView };
894
- if (this.model.get("metadata") && Object.keys(this.model.get("metadata")).length > 0) {
895
- tabs["Metadata"] = this.metadataView;
1013
+ const metadata = this.model.get("metadata") || {};
1014
+ if (metadata.stack_trace) {
1015
+ this.stackTraceView = new StackTraceView({
1016
+ stackTrace: metadata.stack_trace
1017
+ });
1018
+ tabs["Stack Trace"] = this.stackTraceView;
896
1019
  }
897
- if (this.model.get("metadata.stack_trace")) {
898
- this.stackTraceView = new View({
1020
+ if (Object.keys(metadata).length > 0) {
1021
+ this.metadataView = new View({
899
1022
  model: this.model,
900
- template: `<pre class="bg-dark text-white p-3 border rounded">{{{model.metadata.stack_trace}}}</pre>`
1023
+ template: `<pre class="bg-light p-3 border rounded"><code>{{{model.metadata|json}}}</code></pre>`
901
1024
  });
902
- tabs["Stack Trace"] = this.stackTraceView;
1025
+ tabs["Metadata"] = this.metadataView;
903
1026
  }
904
1027
  this.tabView = new TabView({
905
1028
  containerId: "event-tabs",
@@ -1467,7 +1590,7 @@ class FileTablePage extends TablePage {
1467
1590
  */
1468
1591
  async onActionAdd(event, element) {
1469
1592
  event.preventDefault();
1470
- const Dialog2 = (await import("./chunks/Dialog-Bup1WdQn.js")).default;
1593
+ const Dialog2 = (await import("./chunks/Dialog-D9qIh5Jb.js")).default;
1471
1594
  const formData = await Dialog2.showForm({
1472
1595
  title: "Upload File",
1473
1596
  size: "md",
@@ -1563,6 +1686,185 @@ class FileTablePage extends TablePage {
1563
1686
  }
1564
1687
  }
1565
1688
  applyFileDropMixin(FileTablePage);
1689
+ class MapView extends View {
1690
+ constructor(options = {}) {
1691
+ super({
1692
+ className: "map-view",
1693
+ ...options
1694
+ });
1695
+ this.markers = options.markers || [];
1696
+ this.center = options.center || null;
1697
+ this.zoom = options.zoom || 13;
1698
+ this.height = options.height || 400;
1699
+ this.showZoomControl = options.showZoomControl !== false;
1700
+ this.tileLayer = options.tileLayer || "osm";
1701
+ this.map = null;
1702
+ this.leafletMarkers = [];
1703
+ this.template = `
1704
+ <div class="map-container">
1705
+ <div id="map-{{id}}" style="height: {{height}}px; width: 100%; border-radius: 0.375rem; border: 1px solid #dee2e6;"></div>
1706
+ </div>
1707
+ `;
1708
+ }
1709
+ async onAfterRender() {
1710
+ await this.loadLeaflet();
1711
+ await this.initializeMap();
1712
+ }
1713
+ async loadLeaflet() {
1714
+ if (window.L) return;
1715
+ const cssLoaded = new Promise((resolve) => {
1716
+ const link = document.createElement("link");
1717
+ link.rel = "stylesheet";
1718
+ link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
1719
+ link.onload = resolve;
1720
+ link.onerror = resolve;
1721
+ document.head.appendChild(link);
1722
+ });
1723
+ const jsLoaded = new Promise((resolve, reject) => {
1724
+ const script = document.createElement("script");
1725
+ script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
1726
+ script.onload = resolve;
1727
+ script.onerror = reject;
1728
+ document.head.appendChild(script);
1729
+ });
1730
+ await Promise.all([cssLoaded, jsLoaded]);
1731
+ }
1732
+ getTileLayerUrl() {
1733
+ const tileLayers = {
1734
+ // Standard street maps
1735
+ osm: {
1736
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
1737
+ attribution: "© OpenStreetMap contributors",
1738
+ maxZoom: 19
1739
+ },
1740
+ // Satellite imagery
1741
+ satellite: {
1742
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
1743
+ attribution: "© Esri",
1744
+ maxZoom: 19
1745
+ },
1746
+ // Terrain and topographic
1747
+ terrain: {
1748
+ url: "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
1749
+ attribution: "© OpenTopoMap contributors",
1750
+ maxZoom: 17
1751
+ },
1752
+ // Dark mode styles
1753
+ dark: {
1754
+ url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
1755
+ attribution: "© OpenStreetMap contributors © CARTO",
1756
+ maxZoom: 20
1757
+ },
1758
+ // Light/minimal styles
1759
+ light: {
1760
+ url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
1761
+ attribution: "© OpenStreetMap contributors © CARTO",
1762
+ maxZoom: 20
1763
+ },
1764
+ // Watercolor artistic style
1765
+ watercolor: {
1766
+ url: "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg",
1767
+ attribution: "© Stadia Maps © Stamen Design © OpenStreetMap contributors",
1768
+ maxZoom: 16
1769
+ },
1770
+ // Black and white
1771
+ bw: {
1772
+ url: "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png",
1773
+ attribution: "© OpenStreetMap contributors © CARTO",
1774
+ maxZoom: 20
1775
+ },
1776
+ // Streets with labels
1777
+ streets: {
1778
+ url: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
1779
+ attribution: "© OpenStreetMap contributors © CARTO",
1780
+ maxZoom: 20
1781
+ }
1782
+ };
1783
+ return tileLayers[this.tileLayer] || tileLayers.osm;
1784
+ }
1785
+ async initializeMap() {
1786
+ const mapElement = this.element.querySelector(`#map-${this.id}`);
1787
+ if (!mapElement || !window.L) return;
1788
+ let mapCenter = this.center;
1789
+ if (!mapCenter && this.markers.length > 0) {
1790
+ mapCenter = [this.markers[0].lat, this.markers[0].lng];
1791
+ }
1792
+ if (!mapCenter) {
1793
+ mapCenter = [0, 0];
1794
+ }
1795
+ this.map = window.L.map(mapElement, {
1796
+ center: mapCenter,
1797
+ zoom: this.zoom,
1798
+ zoomControl: this.showZoomControl
1799
+ });
1800
+ const tileConfig = this.getTileLayerUrl();
1801
+ window.L.tileLayer(tileConfig.url, {
1802
+ attribution: tileConfig.attribution,
1803
+ maxZoom: tileConfig.maxZoom
1804
+ }).addTo(this.map);
1805
+ this.addMarkers(this.markers);
1806
+ if (this.markers.length > 1) {
1807
+ this.fitBounds();
1808
+ }
1809
+ setTimeout(() => {
1810
+ if (this.map) {
1811
+ this.map.invalidateSize();
1812
+ }
1813
+ }, 300);
1814
+ }
1815
+ addMarkers(markers) {
1816
+ if (!this.map || !Array.isArray(markers)) return;
1817
+ markers.forEach((markerData) => {
1818
+ const { lat, lng, popup, icon } = markerData;
1819
+ if (!lat || !lng) return;
1820
+ const markerOptions = {};
1821
+ if (icon) {
1822
+ markerOptions.icon = window.L.icon(icon);
1823
+ }
1824
+ const marker = window.L.marker([lat, lng], markerOptions).addTo(this.map);
1825
+ if (popup) {
1826
+ marker.bindPopup(popup);
1827
+ }
1828
+ this.leafletMarkers.push(marker);
1829
+ });
1830
+ }
1831
+ fitBounds() {
1832
+ if (!this.map || this.leafletMarkers.length === 0) return;
1833
+ const group = new window.L.featureGroup(this.leafletMarkers);
1834
+ this.map.fitBounds(group.getBounds().pad(0.1));
1835
+ }
1836
+ updateMarkers(newMarkers) {
1837
+ this.clearMarkers();
1838
+ this.markers = newMarkers;
1839
+ this.addMarkers(newMarkers);
1840
+ if (newMarkers.length > 1) {
1841
+ this.fitBounds();
1842
+ } else if (newMarkers.length === 1) {
1843
+ this.map.setView([newMarkers[0].lat, newMarkers[0].lng], this.zoom);
1844
+ }
1845
+ }
1846
+ clearMarkers() {
1847
+ this.leafletMarkers.forEach((marker) => {
1848
+ this.map.removeLayer(marker);
1849
+ });
1850
+ this.leafletMarkers = [];
1851
+ }
1852
+ setView(lat, lng, zoom = null) {
1853
+ if (!this.map) return;
1854
+ this.map.setView([lat, lng], zoom || this.zoom);
1855
+ }
1856
+ setZoom(zoom) {
1857
+ if (!this.map) return;
1858
+ this.map.setZoom(zoom);
1859
+ }
1860
+ async onBeforeDestroy() {
1861
+ if (this.map) {
1862
+ this.map.remove();
1863
+ this.map = null;
1864
+ }
1865
+ await super.onBeforeDestroy();
1866
+ }
1867
+ }
1566
1868
  class GeoIPView extends View {
1567
1869
  constructor(options = {}) {
1568
1870
  super({
@@ -1597,36 +1899,99 @@ class GeoIPView extends View {
1597
1899
  </div>
1598
1900
  </div>
1599
1901
 
1600
- <!-- Body -->
1601
- <div data-container="geoip-data-view"></div>
1902
+ <!-- Tabs -->
1903
+ <div data-container="geoip-tabs"></div>
1602
1904
  </div>
1603
1905
  `;
1604
1906
  }
1605
1907
  async onInit() {
1606
- this.dataView = new DataView({
1607
- containerId: "geoip-data-view",
1908
+ this.detailsView = new DataView({
1608
1909
  model: this.model,
1609
- className: "p-3 border rounded",
1910
+ className: "p-3",
1610
1911
  showEmptyValues: true,
1611
1912
  emptyValueText: "—",
1612
1913
  columns: 2,
1613
1914
  fields: [
1614
- { name: "id", label: "ID" },
1615
- { name: "subnet", label: "Subnet" },
1616
- { name: "country_code", label: "Country Code" },
1617
- { name: "region", label: "Region" },
1618
- { name: "city", label: "City" },
1619
- { name: "postal_code", label: "Postal Code" },
1620
- { name: "latitude", label: "Latitude" },
1621
- { name: "longitude", label: "Longitude" },
1622
- { name: "timezone", label: "Timezone" },
1623
- { name: "created", label: "Created", format: "datetime" },
1624
- { name: "modified", label: "Last Modified", format: "datetime" },
1625
- { name: "expires_at", label: "Expires", format: "datetime" }
1915
+ { name: "ip_address", label: "IP Address", cols: 4 },
1916
+ { name: "subnet", label: "Subnet", cols: 4 },
1917
+ { name: "country_name", label: "Country", cols: 4 },
1918
+ { name: "country_code", label: "Country Code", cols: 4 },
1919
+ { name: "region", label: "Region", cols: 4 },
1920
+ { name: "city", label: "City", cols: 4 },
1921
+ { name: "postal_code", label: "Postal Code", cols: 4 },
1922
+ { name: "timezone", label: "Timezone", cols: 4 },
1923
+ { name: "latitude", label: "Latitude", cols: 4 },
1924
+ { name: "longitude", label: "Longitude", cols: 4 }
1626
1925
  ]
1627
1926
  });
1628
- this.addChild(this.dataView);
1927
+ this.securityView = new DataView({
1928
+ model: this.model,
1929
+ className: "p-3",
1930
+ showEmptyValues: true,
1931
+ emptyValueText: "—",
1932
+ columns: 2,
1933
+ fields: [
1934
+ { name: "threat_level", label: "Threat Level", cols: 4 },
1935
+ { name: "is_tor", label: "TOR Exit Node", cols: 4 },
1936
+ { name: "is_vpn", label: "VPN", formatter: "yesnoicon", cols: 4 },
1937
+ { name: "is_proxy", label: "Proxy", formatter: "yesnoicon", cols: 4 },
1938
+ { name: "is_cloud", label: "Cloud Provider", formatter: "yesnoicon", cols: 4 },
1939
+ { name: "is_datacenter", label: "Datacenter", formatter: "yesnoicon", cols: 4 },
1940
+ { name: "asn", label: "ASN", cols: 4 },
1941
+ { name: "asn_org", label: "ASN Organization", cols: 4 },
1942
+ { name: "isp", label: "ISP", cols: 4 },
1943
+ { name: "connection_type", label: "Connection Type", cols: 6 }
1944
+ ]
1945
+ });
1946
+ this.metadataView = new DataView({
1947
+ model: this.model,
1948
+ className: "p-3",
1949
+ showEmptyValues: true,
1950
+ emptyValueText: "—",
1951
+ columns: 2,
1952
+ fields: [
1953
+ { name: "id", label: "Record ID", cols: 6 },
1954
+ { name: "provider", label: "Data Provider", formatter: "capitalize", cols: 6 },
1955
+ { name: "created", label: "Created", formatter: "datetime", cols: 6 },
1956
+ { name: "modified", label: "Last Modified", formatter: "datetime", cols: 6 },
1957
+ { name: "expires_at", label: "Expires", formatter: "datetime", cols: 12 }
1958
+ ]
1959
+ });
1960
+ const tabs = {
1961
+ "Location": this.detailsView,
1962
+ "Security": this.securityView,
1963
+ "Metadata": this.metadataView
1964
+ };
1965
+ if (this.hasCoordinates) {
1966
+ const lat = this.model.get("latitude");
1967
+ const lng = this.model.get("longitude");
1968
+ const city = this.model.get("city") || "Unknown";
1969
+ const region = this.model.get("region") || "";
1970
+ const country = this.model.get("country_name") || "";
1971
+ const locationStr = [city, region, country].filter(Boolean).join(", ");
1972
+ this.mapView = new MapView({
1973
+ markers: [{
1974
+ lat,
1975
+ lng,
1976
+ popup: `<strong>${this.model.get("ip_address")}</strong><br>${locationStr}`
1977
+ }],
1978
+ tileLayer: "light",
1979
+ zoom: 4,
1980
+ height: 450
1981
+ });
1982
+ tabs["Map"] = this.mapView;
1983
+ }
1984
+ this.tabView = new TabView({
1985
+ containerId: "geoip-tabs",
1986
+ tabs,
1987
+ activeTab: this.hasCoordinates ? "Map" : "Location"
1988
+ });
1989
+ this.addChild(this.tabView);
1629
1990
  const menuItems = [
1991
+ { label: "Edit Location", action: "edit-location", icon: "bi-geo-alt" },
1992
+ { label: "Edit Security", action: "edit-security", icon: "bi-shield-lock" },
1993
+ { label: "Edit Network", action: "edit-network", icon: "bi-diagram-3" },
1994
+ { type: "divider" },
1630
1995
  { label: "Refresh Geolocation", action: "refresh-geoip", icon: "bi-arrow-clockwise" }
1631
1996
  ];
1632
1997
  if (this.hasCoordinates) {
@@ -1651,8 +2016,41 @@ class GeoIPView extends View {
1651
2016
  });
1652
2017
  this.addChild(geoIPMenu);
1653
2018
  }
2019
+ async onActionEditLocation() {
2020
+ const resp = await Dialog$1.showModelForm({
2021
+ title: `Edit Location - ${this.model.get("ip_address")}`,
2022
+ model: this.model,
2023
+ formConfig: GeoLocatedIP.EDIT_LOCATION_FORM
2024
+ });
2025
+ if (resp) {
2026
+ await this.render();
2027
+ this.getApp()?.toast?.success("Location updated successfully");
2028
+ }
2029
+ }
2030
+ async onActionEditSecurity() {
2031
+ const resp = await Dialog$1.showModelForm({
2032
+ title: `Edit Security - ${this.model.get("ip_address")}`,
2033
+ model: this.model,
2034
+ formConfig: GeoLocatedIP.EDIT_SECURITY_FORM
2035
+ });
2036
+ if (resp) {
2037
+ await this.render();
2038
+ this.getApp()?.toast?.success("Security settings updated successfully");
2039
+ }
2040
+ }
2041
+ async onActionEditNetwork() {
2042
+ const resp = await Dialog$1.showModelForm({
2043
+ title: `Edit Network - ${this.model.get("ip_address")}`,
2044
+ model: this.model,
2045
+ formConfig: GeoLocatedIP.EDIT_NETWORK_FORM
2046
+ });
2047
+ if (resp) {
2048
+ await this.render();
2049
+ this.getApp()?.toast?.success("Network information updated successfully");
2050
+ }
2051
+ }
1654
2052
  async onActionRefreshGeoip() {
1655
- console.log("Refreshing GeoIP for:", this.model.get("ip_address"));
2053
+ await this.model.save({ refresh: true });
1656
2054
  this.getApp()?.toast?.info("Refresh request sent for " + this.model.get("ip_address"));
1657
2055
  }
1658
2056
  async onActionViewOnMap() {
@@ -1703,20 +2101,19 @@ class GeoLocatedIPTablePage extends TablePage {
1703
2101
  pageName: "GeoIP Cache",
1704
2102
  router: "admin/system/geoip",
1705
2103
  Collection: GeoLocatedIPList,
1706
- itemViewClass: GeoIPView,
2104
+ itemView: GeoIPView,
1707
2105
  viewDialogOptions: {
1708
2106
  header: false,
1709
- size: "lg"
2107
+ size: "xl"
1710
2108
  },
1711
2109
  // Column definitions
1712
2110
  columns: [
1713
- { key: "id", label: "ID", width: "70px", sortable: true, class: "text-muted" },
1714
2111
  { key: "ip_address", label: "IP Address", sortable: true },
1715
2112
  { key: "city", label: "City", sortable: true, formatter: "default('—')" },
1716
2113
  { key: "region", label: "Region", sortable: true, formatter: "default('—')" },
1717
2114
  { key: "country_name", label: "Country", sortable: true, formatter: "default('—')" },
1718
- { key: "provider", label: "Provider", sortable: true },
1719
- { key: "is_expired", label: "Expired", formatter: "boolean|badge" }
2115
+ { key: "isp", label: "ISP", sortable: true, formatter: "default('—')" },
2116
+ { key: "threat_level", label: "Threat", formatter: "default('—')" }
1720
2117
  ],
1721
2118
  // Table features
1722
2119
  selectable: true,
@@ -1724,6 +2121,9 @@ class GeoLocatedIPTablePage extends TablePage {
1724
2121
  sortable: true,
1725
2122
  filterable: true,
1726
2123
  paginated: true,
2124
+ // Actions
2125
+ // actions: ['view', 'edit', 'delete'],
2126
+ clickAction: "view",
1727
2127
  // Toolbar
1728
2128
  showRefresh: true,
1729
2129
  showAdd: false,
@@ -2365,13 +2765,39 @@ class IncidentView extends View {
2365
2765
  { name: "details", label: "Details", columns: 12, format: "pre" }
2366
2766
  ]
2367
2767
  });
2768
+ const eventsCollection = new IncidentEventList({
2769
+ params: { incident: this.model.get("id") }
2770
+ });
2771
+ this.eventsView = new TableView({
2772
+ collection: eventsCollection,
2773
+ hideActivePillNames: ["incident"],
2774
+ columns: [
2775
+ { key: "id", label: "ID", width: "70px", sortable: true },
2776
+ { key: "created", label: "Date", formatter: "datetime", sortable: true, width: "180px" },
2777
+ { key: "category", label: "Category", formatter: "badge", sortable: true },
2778
+ { key: "title", label: "Title", sortable: true },
2779
+ { key: "level", label: "Level", sortable: true, width: "80px" }
2780
+ ],
2781
+ showAdd: false,
2782
+ actions: ["view"],
2783
+ paginated: true,
2784
+ size: 10
2785
+ });
2368
2786
  const adapter = new IncidentHistoryAdapter(this.model.get("id"));
2369
2787
  this.historyView = new ChatView({ adapter });
2370
2788
  const tabs = {
2371
2789
  "Overview": this.overviewView,
2790
+ "Events": this.eventsView,
2372
2791
  "History & Comments": this.historyView
2373
2792
  };
2374
- if (this.model.get("metadata") && Object.keys(this.model.get("metadata")).length > 0) {
2793
+ const metadata = this.model.get("metadata") || {};
2794
+ if (metadata.stack_trace) {
2795
+ this.stackTraceView = new StackTraceView({
2796
+ stackTrace: metadata.stack_trace
2797
+ });
2798
+ tabs["Stack Trace"] = this.stackTraceView;
2799
+ }
2800
+ if (Object.keys(metadata).length > 0) {
2375
2801
  this.metadataView = new View({
2376
2802
  model: this.model,
2377
2803
  template: `<pre class="bg-light p-3 border rounded"><code>{{{model.metadata|json}}}</code></pre>`
@@ -2507,7 +2933,7 @@ class IncidentTablePage extends TablePage {
2507
2933
  { label: "Resolve", icon: "bi bi-check-circle", action: "resolve" },
2508
2934
  { label: "Pause", icon: "bi bi-pause-circle", action: "pause" },
2509
2935
  { label: "Ignore", icon: "bi bi-x-circle", action: "ignore" },
2510
- { label: "Change Status", icon: "bi bi-arrow-repeat", action: "status" }
2936
+ { label: "Merge", icon: "bi bi-merge", action: "merge" }
2511
2937
  ],
2512
2938
  // Table display options
2513
2939
  tableOptions: {
@@ -2554,6 +2980,29 @@ class IncidentTablePage extends TablePage {
2554
2980
  await Promise.all(selected.map((item) => item.model.save({ status: "ignored" })));
2555
2981
  this.tableView.collection.fetch();
2556
2982
  }
2983
+ async onActionBatchMerge(_event, _element) {
2984
+ const selected = this.tableView.getSelectedItems();
2985
+ if (!selected.length) return;
2986
+ const app = this.getApp();
2987
+ const result = await app.showForm({
2988
+ title: `Merge ${selected.length} incidents`,
2989
+ fields: [
2990
+ {
2991
+ name: "merge",
2992
+ type: "select",
2993
+ label: "Select Parent Incident",
2994
+ options: selected.map((item) => ({ value: item.model.id, label: item.model.id })),
2995
+ required: true
2996
+ }
2997
+ ]
2998
+ });
2999
+ if (!result) return;
3000
+ const parentModel = selected.find((item) => item.model.id == result.merge)?.model;
3001
+ if (!parentModel) return;
3002
+ const mergeIds = selected.map((item) => item.model.id).filter((id) => id != result.merge);
3003
+ await parentModel.save({ merge: mergeIds });
3004
+ this.tableView.collection.fetch();
3005
+ }
2557
3006
  }
2558
3007
  class JobStatsView extends View {
2559
3008
  constructor(options = {}) {
@@ -5013,20 +5462,36 @@ class RuleSetView extends View {
5013
5462
  `;
5014
5463
  }
5015
5464
  async onInit() {
5465
+ const matchByValue = this.model.get("match_by");
5466
+ const matchByOption = MatchByOptions.find((opt) => opt.value === matchByValue);
5467
+ const matchByLabel = matchByOption ? matchByOption.label : String(matchByValue);
5468
+ const bundleByValue = this.model.get("bundle_by");
5469
+ const bundleByOption = BundleByOptions.find((opt) => opt.value === bundleByValue);
5470
+ const bundleByLabel = bundleByOption ? bundleByOption.label : String(bundleByValue);
5016
5471
  this.configView = new DataView({
5017
5472
  model: this.model,
5018
5473
  className: "p-3",
5019
5474
  columns: 2,
5020
5475
  fields: [
5021
- { name: "id", label: "RuleSet ID" },
5022
- { name: "name", label: "Name" },
5023
- { name: "category", label: "Category" },
5024
- { name: "priority", label: "Priority" },
5025
- { name: "match_by", label: "Match Logic", format: (v) => v === 0 ? "ALL" : "ANY" },
5026
- { name: "bundle_by", label: "Bundle By" },
5027
- { name: "bundle_minutes", label: "Bundle Minutes" },
5028
- { name: "handler", label: "Handler" },
5029
- { name: "is_active", label: "Status", format: "boolean" }
5476
+ { name: "id", label: "RuleSet ID", cols: 6 },
5477
+ { name: "priority", label: "Priority", cols: 6 },
5478
+ { name: "name", label: "Name", cols: 12 },
5479
+ { name: "category", label: "Category", formatter: "badge", cols: 6 },
5480
+ { name: "is_active", label: "Status", formatter: "boolean", cols: 6 },
5481
+ {
5482
+ name: "match_by",
5483
+ label: "Match Logic",
5484
+ template: matchByLabel,
5485
+ cols: 12
5486
+ },
5487
+ {
5488
+ name: "bundle_by",
5489
+ label: "Bundle By",
5490
+ template: bundleByLabel,
5491
+ cols: 12
5492
+ },
5493
+ { name: "bundle_minutes", label: "Bundle Minutes", cols: 6 },
5494
+ { name: "handler", label: "Handler", cols: 12 }
5030
5495
  ]
5031
5496
  });
5032
5497
  const rulesCollection = new RuleList({
@@ -5038,12 +5503,23 @@ class RuleSetView extends View {
5038
5503
  { key: "id", label: "ID", width: "70px" },
5039
5504
  { key: "name", label: "Name" },
5040
5505
  { key: "field_name", label: "Field" },
5041
- { key: "comparator", label: "Comparator" },
5506
+ { key: "comparator", label: "Comparator", width: "120px" },
5042
5507
  { key: "value", label: "Value" },
5043
- { key: "value_type", label: "Type" }
5508
+ { key: "value_type", label: "Type", width: "100px" }
5044
5509
  ],
5045
5510
  showAdd: true,
5046
- actions: ["edit", "delete"]
5511
+ clickAction: "edit",
5512
+ actions: ["edit", "delete"],
5513
+ contextMenu: [
5514
+ { label: "Edit Rule", action: "edit", icon: "bi-pencil" },
5515
+ { label: "Duplicate Rule", action: "duplicate", icon: "bi-files" },
5516
+ { divider: true },
5517
+ { label: "Delete Rule", action: "delete", icon: "bi-trash", danger: true }
5518
+ ],
5519
+ // Pass the parent ID so new rules get associated with this ruleset
5520
+ defaultData: {
5521
+ parent: this.model.get("id")
5522
+ }
5047
5523
  });
5048
5524
  this.tabView = new TabView({
5049
5525
  containerId: "ruleset-tabs",
@@ -5069,6 +5545,73 @@ class RuleSetView extends View {
5069
5545
  });
5070
5546
  this.addChild(contextMenu);
5071
5547
  }
5548
+ /**
5549
+ * Action handler: Edit RuleSet
5550
+ */
5551
+ async onActionEditRuleset() {
5552
+ const resp = await Dialog$1.showModelForm({
5553
+ title: `Edit RuleSet - ${this.model.get("name")}`,
5554
+ model: this.model,
5555
+ formConfig: RuleSet.EDIT_FORM
5556
+ });
5557
+ if (resp) {
5558
+ await this.render();
5559
+ }
5560
+ }
5561
+ /**
5562
+ * Action handler: Disable/Enable RuleSet
5563
+ */
5564
+ async onActionDisableRuleset() {
5565
+ const isActive = this.model.get("is_active");
5566
+ const newStatus = !isActive;
5567
+ try {
5568
+ this.model.set("is_active", newStatus);
5569
+ await this.model.save();
5570
+ await this.render();
5571
+ Dialog$1.showToast({
5572
+ message: `RuleSet ${newStatus ? "enabled" : "disabled"} successfully`,
5573
+ type: "success"
5574
+ });
5575
+ } catch (error) {
5576
+ Dialog$1.showToast({
5577
+ message: `Failed to update RuleSet: ${error.message}`,
5578
+ type: "error"
5579
+ });
5580
+ }
5581
+ }
5582
+ /**
5583
+ * Action handler: Delete RuleSet
5584
+ */
5585
+ async onActionDeleteRuleset() {
5586
+ const confirmed = await Dialog$1.confirm({
5587
+ title: "Delete RuleSet",
5588
+ message: `Are you sure you want to delete the ruleset "${this.model.get("name")}"? This action cannot be undone.`,
5589
+ confirmText: "Delete",
5590
+ confirmClass: "btn-danger"
5591
+ });
5592
+ if (confirmed) {
5593
+ try {
5594
+ await this.model.destroy();
5595
+ Dialog$1.showToast({
5596
+ message: "RuleSet deleted successfully",
5597
+ type: "success"
5598
+ });
5599
+ const dialog = this.element?.closest(".modal");
5600
+ if (dialog) {
5601
+ const bsModal = bootstrap.Modal.getInstance(dialog);
5602
+ if (bsModal) {
5603
+ bsModal.hide();
5604
+ }
5605
+ }
5606
+ this.emit("ruleset:deleted", { model: this.model });
5607
+ } catch (error) {
5608
+ Dialog$1.showToast({
5609
+ message: `Failed to delete RuleSet: ${error.message}`,
5610
+ type: "error"
5611
+ });
5612
+ }
5613
+ }
5614
+ }
5072
5615
  }
5073
5616
  RuleSetView.VIEW_CLASS = RuleSetView;
5074
5617
  class RuleSetTablePage extends TablePage {
@@ -5079,7 +5622,7 @@ class RuleSetTablePage extends TablePage {
5079
5622
  pageName: "Rule Engine",
5080
5623
  router: "admin/rulesets",
5081
5624
  Collection: RuleSetList,
5082
- itemViewClass: RuleSetView,
5625
+ itemView: RuleSetView,
5083
5626
  viewDialogOptions: {
5084
5627
  header: false,
5085
5628
  size: "xl"
@@ -5089,8 +5632,7 @@ class RuleSetTablePage extends TablePage {
5089
5632
  { key: "name", label: "Name", sortable: true },
5090
5633
  { key: "category", label: "Category", sortable: true, formatter: "badge" },
5091
5634
  { key: "priority", label: "Priority", sortable: true },
5092
- { key: "match_by", label: "Match Logic", formatter: (v) => v === 0 ? "ALL" : "ANY" },
5093
- { key: "is_active", label: "Status", formatter: "boolean|badge('Active:success,Inactive:secondary')" }
5635
+ { key: "match_by", label: "Match Logic", formatter: (v) => v === 0 ? "ALL" : "ANY" }
5094
5636
  ],
5095
5637
  selectable: true,
5096
5638
  searchable: true,