web-mojo 2.1.1096 → 2.1.1098
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +27 -12
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +2 -2
- package/dist/chunks/ChatView-BCtENksD.js +2 -0
- package/dist/chunks/ChatView-BCtENksD.js.map +1 -0
- package/dist/chunks/{ChatView-CwKqeYLf.js → ChatView-CYJPSylg.js} +16 -1
- package/dist/chunks/ChatView-CYJPSylg.js.map +1 -0
- package/dist/chunks/{MetricsMiniChartWidget-qRKrRyZi.js → MetricsMiniChartWidget-B-DkwhEe.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-qRKrRyZi.js.map → MetricsMiniChartWidget-B-DkwhEe.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-Df7x1Zgc.js → MetricsMiniChartWidget-Dt_tkyLi.js} +4 -4
- package/dist/chunks/{MetricsMiniChartWidget-Df7x1Zgc.js.map → MetricsMiniChartWidget-Dt_tkyLi.js.map} +1 -1
- package/dist/chunks/{version-ovutOXGy.js → version-BSojz3ia.js} +4 -4
- package/dist/chunks/{version-ovutOXGy.js.map → version-BSojz3ia.js.map} +1 -1
- package/dist/chunks/{version-CJQdhA14.js → version-Cf0LAYex.js} +2 -2
- package/dist/chunks/{version-CJQdhA14.js.map → version-Cf0LAYex.js.map} +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +3 -3
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/package.json +1 -1
- package/dist/chunks/ChatView-Bx3GEAk-.js +0 -2
- package/dist/chunks/ChatView-Bx3GEAk-.js.map +0 -1
- package/dist/chunks/ChatView-CwKqeYLf.js.map +0 -1
package/dist/admin.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./chunks/ContextMenu-DWau8gXS.js"),t=require("./chunks/Rest-BpDyhFfG.js");require("./chunks/WebSocketClient-E08hfP5f.js");const a=require("./chunks/Dialog-ua-xN2r0.js"),s=require("./chunks/MetricsMiniChartWidget-qRKrRyZi.js"),i=require("./chunks/ChatView-Bx3GEAk-.js"),n=require("./chunks/DataView-DESqBxT-.js"),l=require("./chunks/Collection-B64LJ92k.js"),o=require("./chunks/PDFViewer-etF76Hp4.js"),r=require("./chunks/FormView-Cd20wDM9.js"),d=require("./map.cjs.js"),c=require("./chunks/version-CJQdhA14.js");class AdminHeaderView extends t.View{constructor(e={}){super({title:"Dashboard",...e,headerActions:[{label:"Export",icon:"bi-download",action:"export",buttonClass:"btn-primary"}],className:"admin-header-section"}),this.stats={user_activity_day:0,total_users:0,group_activity_day:0,total_groups:0,api_calls:0,apiChange:"",incidents:0,incidentsChange:""},this.prepareStatsForTemplate()}async getTemplate(){return'\n <div class="admin-stats-header mb-4">\n <div class="row">\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="user_activity_day"></div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="group_activity_day"></div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="api_activity_day"></div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="incident_activity_day"></div>\n </div>\n </div>\n </div>\n '}async onInit(){this.userActivity=new s.MetricsMiniChartWidget({icon:"bi bi-people fs-2",title:"User Activity",subtitle:"{{now_value}}",background:"#5388D6",textColor:"#FFFFFF",granularity:"days",trendRange:4,trendOffset:0,slugs:["user_activity_day"],account:"global",chartType:"bar",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"user_activity_day"}),this.addChild(this.userActivity),this.groupActivity=new s.MetricsMiniChartWidget({icon:"bi bi-collection fs-2",title:"Group Activity",subtitle:"{{now_value}}",background:"#1f6a7a",textColor:"#FFFFFF",granularity:"days",trendRange:4,trendOffset:0,slugs:["group_activity_day"],account:"global",chartType:"bar",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"group_activity_day"}),this.addChild(this.groupActivity),this.apiActivity=new s.MetricsMiniChartWidget({icon:"bi bi-graph-up fs-2",title:"API Requests",subtitle:"{{now_value}}",background:"#50A079",textColor:"#FFFFFF",endpoint:"/api/metrics/fetch",trendRange:4,trendOffset:0,granularity:"days",slugs:["api_calls"],account:"global",chartType:"line",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"api_activity_day"}),this.addChild(this.apiActivity),this.incidentActivity=new s.MetricsMiniChartWidget({icon:"bi bi-exclamation-triangle fs-2",title:"Incidents",subtitle:"{{now_value}}",background:"#B14545",textColor:"#FFFFFF",endpoint:"/api/metrics/fetch",trendRange:4,trendOffset:0,granularity:"days",slugs:["incidents"],account:"incident",chartType:"line",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"incident_activity_day"}),this.addChild(this.incidentActivity)}async onBeforeRender(){await Promise.all([this.loadStats(),this.loadValues()])}prepareStatsForTemplate(){}async loadValues(){try{const e=await this.getApp().rest.GET("/api/metrics/value/get",{slugs:["total_users","total_groups"],account:"global"});e.success&&e.data.status&&Object.assign(this.stats,e.data.data)}catch(e){console.error("Failed to load admin stats:",e)}}async loadStats(){try{const e=await this.getApp().rest.GET("/api/metrics/series",{slugs:["user_created","user_activity_day","incidents","api_calls","api_errors","group_activity_day"],account:"global",granularity:"days"});e.success&&e.data.status&&(Object.assign(this.stats,e.data.data),this.prepareStatsForTemplate())}catch(e){console.error("Failed to load admin stats:",e)}}}class AdminDashboardPage extends e.Page{constructor(e={}){super({...e,title:"Admin Dashboard",className:"admin-dashboard-page"}),this.pageTitle="Admin Dashboard",this.pageSubtitle="System monitoring and metrics overview"}async getTemplate(){return'\n <div class="admin-dashboard-container container-lg">\n \x3c!-- Page Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-2">\n <div>\n <p class="text-muted mb-0">{{pageSubtitle}}</p>\n <small class="text-info">\n <i class="bi bi-shield-check me-1"></i>\n Real-time system metrics and performance monitoring\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Charts">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button type="button" class="btn btn-outline-primary btn-sm"\n data-action="export-metrics" title="Export Metrics Data">\n <i class="bi bi-download"></i> Export\n </button>\n <button type="button" class="btn btn-outline-warning btn-sm"\n data-action="view-alerts" title="View System Alerts">\n <i class="bi bi-bell"></i> Alerts\n </button>\n </div>\n </div>\n\n \x3c!-- Stats Header --\x3e\n <div data-container="admin-header"></div>\n <div data-container="example-chart"></div>\n \x3c!-- Charts Section --\x3e\n <div class="row">\n \x3c!-- Full Width API Metrics Chart --\x3e\n <div class="col-12 mb-4">\n <div data-container="api-metrics-chart"></div>\n </div>\n </div>\n\n <div class="row">\n \x3c!-- System Events Chart --\x3e\n <div class="col-xl-6 col-lg-6 mb-4">\n <div data-container="system-events-chart"></div>\n </div>\n\n \x3c!-- System Incidents Chart --\x3e\n <div class="col-xl-6 col-lg-6 mb-4">\n <div data-container="system-incidents-chart"></div>\n </div>\n </div>\n\n \x3c!-- System Status Footer --\x3e\n <div class="row">\n <div class="col-12">\n <div class="alert alert-success border-0" role="alert">\n <div class="d-flex align-items-center">\n <i class="bi bi-check-circle-fill me-2"></i>\n <div>\n <strong>System Status:</strong> All systems operational.\n Last updated: <span class="text-muted">{{lastUpdated}}</span>\n </div>\n <div class="ms-auto">\n <button class="btn btn-sm btn-outline-success" data-action="view-system-status">\n <i class="bi bi-info-circle"></i> Details\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async onInit(){this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString(),this.headerView=new AdminHeaderView({containerId:"admin-header"}),this.addChild(this.headerView),this.apiMetricsChart=new s.MetricsChart({title:'<i class="bi bi-graph-up me-2"></i> API Metrics',endpoint:"/api/metrics/fetch",height:250,granularity:"hours",slugs:["api_calls","api_errors"],account:"global",chartType:"line",showDateRange:!1,yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"api-metrics-chart"}),this.addChild(this.apiMetricsChart),this.systemEventsChart=new s.MetricsChart({title:'<i class="bi bi-activity me-2"></i> System Events',endpoint:"/api/metrics/fetch",granularity:"hours",slugs:["incident_events"],account:"incident",chartType:"line",showDateRange:!1,showMetricsFilter:!1,height:250,colors:["rgba(32, 201, 151, 0.8)"],yAxis:{label:"Events",beginAtZero:!0},tooltip:{y:"number"},containerId:"system-events-chart"}),this.addChild(this.systemEventsChart),this.systemIncidentsChart=new s.MetricsChart({title:'<i class="bi bi-exclamation-triangle me-2"></i> System Incidents',endpoint:"/api/metrics/fetch",granularity:"hours",slugs:["incidents"],account:"incident",chartType:"line",showDateRange:!1,showMetricsFilter:!1,height:250,colors:["rgba(255, 193, 7, 0.8)"],yAxis:{label:"Incidents",beginAtZero:!0},tooltip:{y:"number"},containerId:"system-incidents-chart"}),this.addChild(this.systemIncidentsChart)}async onActionRefreshAll(e,t){try{const e=t.querySelector("i");e?.classList.add("bi-spin"),t.disabled=!0;const a=[this.headerView?.loadStats(),this.apiMetricsChart?.refresh(),this.systemEventsChart?.refresh(),this.systemIncidentsChart?.refresh()].filter(Boolean);await Promise.allSettled(a),this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString();const s=this.getApp()?.events;s&&s.emit("admin:dashboard-refreshed",{page:this,timestamp:this.lastUpdated})}catch(a){console.error("Failed to refresh dashboard:",a);const e=this.element.querySelector(".alert-success");e&&(e.className="alert alert-danger border-0",e.innerHTML='\n <div class="d-flex align-items-center">\n <i class="bi bi-exclamation-triangle-fill me-2"></i>\n <div>\n <strong>Error:</strong> Failed to refresh dashboard data.\n </div>\n </div>\n ',setTimeout(()=>{e.className="alert alert-success border-0",e.innerHTML=`\n <div class="d-flex align-items-center">\n <i class="bi bi-check-circle-fill me-2"></i>\n <div>\n <strong>System Status:</strong> All systems operational.\n Last updated: <span class="text-muted">${this.lastUpdated}</span>\n </div>\n </div>\n `},5e3))}finally{const e=t.querySelector("i");e?.classList.remove("bi-spin"),t.disabled=!1}}async onActionExportMetrics(e,t){try{await(this.apiMetricsChart?.export("png")),await(this.systemEventsChart?.export("png")),await(this.systemIncidentsChart?.export("png"));const e=this.getApp()?.events;e&&e.emit("admin:metrics-exported",{page:this,charts:["api-metrics","system-events","system-incidents"]})}catch(a){console.error("Failed to export metrics:",a)}}async onActionViewAlerts(e,t){const a=this.getApp()?.router;a&&a.navigateTo("/admin/alerts")}async onActionViewSystemStatus(e,t){const a=this.getApp()?.router;a&&a.navigateTo("/admin/system-status")}async refreshDashboard(){return this.onActionRefreshAll(null,null,{disabled:!1,querySelector:()=>null})}getCharts(){return{apiMetrics:this.apiMetricsChart,systemEvents:this.systemEventsChart,systemIncidents:this.systemIncidentsChart}}getStats(){return this.headerView?.stats||{}}}class EmailDomainTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_domains",pageName:"Email Domains",router:"admin/email/domains",Collection:i.EmailDomainList,formCreate:i.EmailDomainForms.create,formEdit:i.EmailDomainForms.edit,columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"name",label:"Domain",sortable:!0},{key:"region",label:"Region",sortable:!0,formatter:"default('—')"},{key:"receiving_enabled",label:"Receiving",formatter:"boolean|badge"},{key:"can_send",label:"Send Verified",formatter:"boolean|badge"},{key:"can_recv",label:"Recv Verified",formatter:"boolean|badge"},{key:"created",label:"Created",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No domains found. Click "Add Domain" to get started.',contextMenu:[{icon:"bi-shield-check",action:"edit-aws-creds",label:"Edit AWS Credentials"},{icon:"bi-rocket-takeoff",action:"onboard",label:"Onboard"},{icon:"bi-shield-check",action:"audit",label:"Audit"},{icon:"bi-arrow-repeat",action:"reconcile",label:"Reconcile"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionEditAwsCreds(e,t){const s=this.collection.get(t.dataset.id);return await a.Dialog.showModelForm({model:s,formConfig:i.EmailDomainForms.credentials}),!0}async onActionOnboard(e,t){const s=this.collection.get(t.dataset.id),n=new i.EmailDomain({id:s.id}),l=await a.Dialog.showForm(i.EmailDomainForms.onboard);if(l)try{const e=await n.onboard(l);if(!e.success)throw new Error(e.message||"Network error during onboarding");if(!e.data?.status)throw new Error(e.data?.error||"Onboarding failed");this.getApp()?.toast?.success("Domain onboarding completed successfully"),await this.refresh()}catch(o){console.error("Onboard error:",o),this.showError(o.message||"Failed to onboard domain")}}async onActionAudit(e,s){const n=this.collection.get(s.dataset.id),l=new i.EmailDomain({id:n.id});try{const e=await l.audit();if(!e.success)throw new Error(e.message||"Network error during audit");if(!e.data?.status)throw new Error(e.data?.error||"Audit failed");const s=e.data?.data||{};await a.Dialog.showDialog({title:`Audit Report - ${n.name}`,body:new t.View({template:'\n <div>\n <p class="text-muted">Drift report and status:</p>\n <pre class="bg-light p-3 rounded small"><code>{{{data.result|json}}}</code></pre>\n </div>\n ',data:{result:s}}),size:"lg"})}catch(o){console.error("Audit error:",o),this.showError(o.message||"Failed to audit domain")}}async onActionReconcile(e,t){const a=this.collection.get(t.dataset.id),s=new i.EmailDomain({id:a.id});try{const e=await s.reconcile();if(!e.success)throw new Error(e.message||"Network error during reconcile");if(!e.data?.status)throw new Error(e.data?.error||"Reconcile failed");this.getApp()?.toast?.success("Reconcile completed successfully"),await this.refresh()}catch(n){console.error("Reconcile error:",n),this.showError(n.message||"Failed to reconcile domain")}}}class EmailMailboxTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_mailboxes",pageName:"Mailboxes",router:"admin/email/mailboxes",Collection:i.MailboxList,formCreate:i.MailboxForms.create,formEdit:i.MailboxForms.edit,clickAction:"edit",columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"email",label:"Email",sortable:!0},{key:"domain.name",label:"Domain",sortable:!0,formatter:"default('—')"},{key:"allow_inbound",label:"Inbound",formatter:"boolean|badge"},{key:"allow_outbound",label:"Outbound",formatter:"boolean|badge"},{key:"is_system_default",label:"System Default",formatter:"boolean|badge"},{key:"is_domain_default",label:"Domain Default",formatter:"boolean|badge"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No mailboxes found. Click "Add Mailbox" to create one.',contextMenu:[{icon:"bi-envelope",action:"send-email",label:"Send Email"},{icon:"bi-envelope",action:"send-template-email",label:"Send Template Email"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionSendEmail(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showForm({title:"Send Email",fields:[{name:"to",label:"To",type:"email",required:!0},{name:"subject",label:"Subject",type:"text",required:!0},{name:"body_html",label:"Body",type:"textarea",required:!0}]});n.from_email=s.get("email");const l=await i.Mailbox.sendEmail(n);if(l.success)this.getApp().toast.success("Email sent successfully");else{let e="Failed to send email";l.data.details?e=l.data.details:l.data.error&&(e=l.data.error),this.getApp().toast.error(e)}}async onActionSendTemplateEmail(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showForm({title:"Send Email",fields:[{name:"to",label:"To",type:"email",required:!0},{name:"subject",label:"Subject",type:"text",required:!0},{name:"template_name",label:"Template",type:"text",required:!0},{name:"template_context",label:"Context",type:"textarea",required:!0}]});n.from_email=s.get("email");const l=await i.Mailbox.sendEmail(n);if(l.success)this.getApp().toast.success("Email sent successfully");else{let e="Failed to send email";l.data.details?e=l.data.details:l.data.error&&(e=l.data.error),this.getApp().toast.error(e)}}}class EmailTemplateView extends t.View{constructor(e={}){super({className:"email-template-view",...e}),this.model=e.model||new i.EmailTemplate(e.data||{}),this.hasHtml=!!this.model.get("html_template"),this.hasText=!!this.model.get("text_template"),this.hasMetadata=this.model.get("metadata")&&Object.keys(this.model.get("metadata")).length>0,this.template='\n <div class="email-template-view-container p-3">\n \x3c!-- Header --\x3e\n <div class="template-header border-bottom pb-3 mb-3">\n <h4 class="mb-1">{{model.name}}</h4>\n <div class="text-muted">\n <strong>Subject:</strong> {{model.subject_template|default(\'Not set\')}}\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="template-tabs"></div>\n </div>\n '}async onInit(){const e={};this.hasHtml&&(e["HTML Preview"]=new t.View({model:this.model,template:'<div class="email-html-content border rounded p-3" style="height: 500px; overflow-y: auto;">{{model.html_template}}</div>'})),this.hasText&&(e["Text Version"]=new t.View({model:this.model,template:'<pre class="email-text-content p-3 bg-light border rounded" style="white-space: pre-wrap; word-wrap: break-word;">{{model.text_template}}</pre>'})),this.hasMetadata&&(e.Metadata=new t.View({model:this.model,template:'<pre class="email-metadata-content p-3 bg-light border rounded"><code>{{model.metadata|json}}</code></pre>'})),this.tabView=new i.TabView({containerId:"template-tabs",tabs:e,activeTab:Object.keys(e)[0]||""}),this.addChild(this.tabView)}}class EmailTemplateTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_templates",pageName:"Email Templates",router:"admin/email/templates",Collection:i.EmailTemplateList,formCreate:i.EmailTemplateForms.create,formEdit:i.EmailTemplateForms.edit,itemViewClass:EmailTemplateView,clickAction:"edit",viewDialogOptions:{header:!1,size:"xl",scrollable:!0},formDialogConfig:{size:"fullscreen"},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"name",label:"Name",sortable:!0},{key:"created",label:"Created",formatter:"datetime"},{key:"modified",label:"Modified",formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No email templates found. Click "Add Template" to create your first one.',tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class StackTraceView extends t.View{constructor(e={}){super({className:"stack-trace-view",...e}),this.stackTrace=e.stackTrace||"",this.template="\n <div class=\"stack-trace-container p-3\">\n <style>\n .stack-trace-line {\n font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;\n font-size: 13px;\n line-height: 1.6;\n padding: 4px 8px;\n margin: 0;\n border-left: 3px solid transparent;\n }\n .stack-trace-line:hover {\n background-color: rgba(0, 0, 0, 0.05);\n }\n .stack-trace-error {\n color: #dc3545;\n font-weight: 600;\n border-left-color: #dc3545;\n background-color: rgba(220, 53, 69, 0.05);\n }\n .stack-trace-file {\n color: #0d6efd;\n font-weight: 500;\n border-left-color: #0d6efd;\n }\n .stack-trace-function {\n color: #6610f2;\n font-weight: 500;\n }\n .stack-trace-location {\n color: #6c757d;\n font-size: 12px;\n }\n .stack-trace-line-number {\n color: #fd7e14;\n font-weight: 600;\n }\n .stack-trace-context {\n color: #495057;\n background-color: rgba(0, 0, 0, 0.02);\n }\n .stack-trace-container {\n background-color: #f8f9fa;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n max-height: 600px;\n overflow-y: auto;\n }\n </style>\n <div class=\"stack-trace-content\">\n {{{formattedStackTrace}}}\n </div>\n </div>\n "}async onBeforeRender(){this.formattedStackTrace=this.formatStackTrace(this.stackTrace)}formatStackTrace(e){if(!e)return'<div class="text-muted p-3">No stack trace available</div>';const t=("string"==typeof e?e:JSON.stringify(e,null,2)).split("\n");let a="";return t.forEach((e,t)=>{if(!e.trim())return void(a+='<div class="stack-trace-line"> </div>');if(0===t&&(e.includes("Error:")||e.includes("Exception:")))return void(a+=`<div class="stack-trace-line stack-trace-error">${this.escapeHtml(e)}</div>`);let s=e.match(/(.+?)\s*\(([^:]+):(\d+):(\d+)\)/);if(s){const[,e,t,i,n]=s;return void(a+=`<div class="stack-trace-line stack-trace-file">\n <span class="stack-trace-function">${this.escapeHtml(e.trim())}</span>\n <span class="stack-trace-location"> (${this.escapeHtml(t)}:<span class="stack-trace-line-number">${i}</span>:${n})</span>\n </div>`)}if(s=e.match(/^\s*at\s+([^:]+):(\d+):(\d+)/),s){const[,e,t,i]=s;return void(a+=`<div class="stack-trace-line stack-trace-file">\n <span class="stack-trace-location">at ${this.escapeHtml(e)}:<span class="stack-trace-line-number">${t}</span>:${i}</span>\n </div>`)}if(s=e.match(/File\s+"([^"]+)",\s+line\s+(\d+),\s+in\s+(.+)/),s){const[,e,t,i]=s;return void(a+=`<div class="stack-trace-line stack-trace-file">\n <span class="stack-trace-location">File "${this.escapeHtml(e)}", line <span class="stack-trace-line-number">${t}</span>, in </span>\n <span class="stack-trace-function">${this.escapeHtml(i)}</span>\n </div>`)}e.trim().startsWith("at ")?a+=`<div class="stack-trace-line stack-trace-file">${this.escapeHtml(e)}</div>`:a+=`<div class="stack-trace-line stack-trace-context">${this.escapeHtml(e)}</div>`}),a}updateStackTrace(e){this.stackTrace=e,this.render()}}class EventView extends t.View{constructor(e={}){super({className:"event-view",...e}),this.model=e.model||new i.IncidentEvent(e.data||{}),this.eventIcon=this.getIconForEvent(this.model.get("level")),this.template='\n <div class="event-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 {{eventIcon.color}}">\n <i class="bi {{eventIcon.icon}}"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.title|default(\'System Event\')}}</h3>\n <div class="text-muted small">\n Category: {{model.category|capitalize}}\n </div>\n <div class="text-muted small mt-1">\n {{model.created|datetime}} from {{model.source_ip|default(\'Unknown IP\')}}\n </div>\n </div>\n </div>\n <div data-container="event-context-menu"></div>\n </div>\n\n \x3c!-- Body --\x3e\n <div data-container="event-tabs"></div>\n </div>\n '}getIconForEvent(e){return e>=40?{icon:"bi-exclamation-octagon-fill",color:"text-danger"}:e>=30?{icon:"bi-exclamation-triangle-fill",color:"text-warning"}:e>=20?{icon:"bi-info-circle-fill",color:"text-info"}:{icon:"bi-bell-fill",color:"text-secondary"}}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Event ID"},{name:"level",label:"Level"},{name:"hostname",label:"Hostname"},{name:"incident",label:"Incident ID"},{name:"model_name",label:"Related Model"},{name:"model_id",label:"Related Model ID"},{name:"details",label:"Details",columns:12}]});const a={Overview:this.overviewView},s=this.model.get("metadata")||{};s.stack_trace&&(this.stackTraceView=new StackTraceView({stackTrace:s.stack_trace}),a["Stack Trace"]=this.stackTraceView),Object.keys(s).length>0&&(this.metadataView=new t.View({model:this.model,template:'<pre class="bg-light p-3 border rounded"><code>{{{model.metadata|json}}}</code></pre>'}),a.Metadata=this.metadataView),this.tabView=new i.TabView({containerId:"event-tabs",tabs:a,activeTab:"Overview"}),this.addChild(this.tabView);const l=[{label:"View Incident",action:"view-incident",icon:"bi-shield-exclamation",disabled:!this.model.get("incident")},{label:"View Related Model",action:"view-model",icon:"bi-box-arrow-up-right",disabled:!this.model.get("model_id")},{type:"divider"},{label:"Delete Event",action:"delete-event",icon:"bi-trash",danger:!0}],o=new e.ContextMenu({containerId:"event-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:l}});this.addChild(o)}async onActionViewIncident(){this.model.get("incident")}async onActionViewModel(){this.model.get("model_name"),this.model.get("model_id")}async onActionDeleteEvent(){await a.Dialog.confirm("Are you sure you want to delete this event? This action cannot be undone.","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("event:deleted",{model:this.model})}}i.IncidentEvent.VIEW_CLASS=EventView;class EventTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_events",pageName:"System Events",router:"admin/events",Collection:i.IncidentEventList,formEdit:i.IncidentEventForms.edit,itemViewClass:EventView,viewDialogOptions:{header:!1,size:"lg"},defaultQuery:{sort:"-id",category__not:"ossec"},columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"datetime"},{key:"level",label:"Level",sortable:!0,formatter:"badge",filter:{type:"select",options:[{value:"5",label:"Critical"},{value:"4",label:"Warning"},{value:"3",label:"Info"},{value:"2",label:"Debug"},{value:"1",label:"Trace"}]}},{key:"category",label:"Category",sortable:!0,formatter:"badge",filter:{type:"text"}},{key:"title",label:"Title",sortable:!0,formatter:"truncate(50)"},{key:"source_ip",label:"Source IP",sortable:!0,filter:{type:"text"}},{key:"model_name",label:"Related Model",sortable:!0}],filters:[{key:"category__not",label:"Not Category",filter:{type:"text"}}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No events found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class FileManagerTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_file_managers",pageName:"Manage Storage Backends",router:"admin/file-managers",Collection:i.FileManagerList,formCreate:i.FileManagerForms.create,formEdit:i.FileManagerForms.edit,columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"name",label:"Name",formatter:"default('Unnamed Backend')"},{key:"backend_url",label:"Backend URL",sortable:!0},{key:"is_default",label:"Default",formatter:"boolean|badge"},{key:"is_active",label:"Active",formatter:"boolean|badge"},{key:"backend_type",label:"Type",formatter:"default('Unknown')"},{key:"created",label:"Created",formatter:"epoch|datetime"}],contextMenu:[{icon:"bi-pencil",action:"edit",label:"Edit Name"},{icon:"bi-shield",action:"edit-credentials",label:"Edit Credentials"},{icon:"bi-person",action:"edit-owners",label:"Edit Owners"},{divider:!0},{icon:"bi-copy",action:"clone",label:"Clone Manager"},{divider:!0},{icon:"bi-check",action:"test-connection",label:"Test Connection"},{icon:"bi-question-circle",action:"check-cors",label:"Check CORS"},{icon:"bi-wrench",action:"fix-cors",label:"Fix CORS"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No storage backends found. Click "Add Storage Backend" to configure your first backend.',batchBarLocation:"top",batchActions:[{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Activate",icon:"bi bi-check-circle",action:"batch-activate"},{label:"Deactivate",icon:"bi bi-x-circle",action:"batch-deactivate"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionEditOwners(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showModelForm({title:"Edit Owners",model:s,fields:i.FileManagerForms.owners.fields});if(!n)return!0;n.success?this.getApp().toast.success("Owners Updated successfully"):this.getApp().toast.error("Owners update failed")}async onActionCheckCors(e,t){const s=this.collection.get(t.dataset.id),i=await s.save({check_cors:!0});return i.success&&i.data.status?await a.Dialog.showData({title:`Audit Report - ${s._.name}`,data:i.data,size:"lg"}):this.getApp().toast.error("Connection test failed"),!0}async onActionTestConnection(e,t){const a=this.collection.get(t.dataset.id),s=await a.save({test_connection:!0});return s.success&&s.data.status?this.getApp().toast.success("Connection test successful"):this.getApp().toast.error("Connection test failed"),!0}async onActionEditCredentials(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showModelForm({title:"Edit Credentials",model:s,fields:i.FileManagerForms.credentials.fields});return!n||(n.success&&n.data.status?this.getApp().toast.success("Credentials updated successfully"):this.getApp().toast.error("Failed to update credentials"),!0)}async onActionClone(e,t){if(!(await a.Dialog.showConfirm({title:"Clone File Manager",message:"This will create a clone with the same credentials."})))return!0;const s=this.collection.get(t.dataset.id),i=await s.save({clone:!0});return i.success&&i.data.status?(this.getApp().toast.success("Connection cloned successfully"),this.collection.fetch()):this.getApp().toast.error("Failed to clone connection"),!0}}class FileView extends t.View{constructor(e={}){super({className:"file-view",...e}),this.model=e.model||new i.File(e.data||{}),this.isImage="image"===this.model.get("category");const t=this.model.get("renditions")||{};this.renditionsCollection=new l.Collection(Object.values(t)),this.template='\n <div class="file-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Thumbnail & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="file-thumbnail" style="width: 80px; height: 80px;">\n {{#isImage}}\n <a href="{{model.url}}" target="_blank" title="View original file">\n <img src="{{model.renditions.thumbnail.url|default(model.url)}}" class="img-fluid rounded" style="width: 80px; height: 80px; object-fit: cover;">\n </a>\n {{/isImage}}\n {{^isImage}}\n <div class="avatar-placeholder rounded bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi bi-file-earmark-text text-secondary" style="font-size: 40px;"></i>\n </div>\n {{/isImage}}\n </div>\n <div>\n <h3 class="mb-1" style="word-break: break-all;">{{model.filename|truncate(40)}}</h3>\n <div class="text-muted small">\n <span><i class="bi bi-hdd"></i> {{model.file_size|filesize}}</span>\n <span class="mx-2">|</span>\n <span>{{model.content_type}}</span>\n </div>\n <div class="text-muted small mt-1">\n Uploaded: {{model.created|datetime}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2 justify-content-end">\n <span class="badge {{model.upload_status|badge}}">{{model.upload_status|capitalize}}</span>\n </div>\n <div class="text-muted small mt-1">\n Public: {{{model.is_public|yesnoicon}}}\n </div>\n </div>\n <div data-container="file-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="file-tabs"></div>\n </div>\n '}async onInit(){this.infoView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"ID"},{name:"filename",label:"Filename"},{name:"storage_filename",label:"Storage Filename"},{name:"content_type",label:"Content Type"},{name:"file_size",label:"File Size",format:"filesize"},{name:"category",label:"Category"},{name:"upload_status",label:"Status",format:"badge"},{name:"created",label:"Created",format:"datetime"},{name:"modified",label:"Modified",format:"datetime"},{name:"user.display_name",label:"Uploaded By"},{name:"file_manager.name",label:"Storage Backend"},{name:"storage_file_path",label:"Storage Path"},{name:"url",label:"Public URL",format:"url"},{name:"is_public",label:"Is Public",format:"boolean"}]}),this.renditionsView=new i.TableView({collection:this.renditionsCollection,columns:[{key:"role",label:"Role",formatter:"badge"},{key:"filename",label:"Filename",formatter:"truncate(40)"},{key:"file_size",label:"Size",formatter:"filesize"},{key:"content_type",label:"Content Type"},{key:"actions",label:"Actions",template:'\n <a href="{{url}}" target="_blank" class="btn btn-sm btn-outline-primary" title="View">\n <i class="bi bi-eye"></i>\n </a>\n <a href="{{url}}" download="{{filename}}" class="btn btn-sm btn-outline-secondary" title="Download">\n <i class="bi bi-download"></i>\n </a>\n '}]});const t={Info:this.infoView};t.Renditions=this.renditionsView,this.tabView=new i.TabView({tabs:t,activeTab:"Info",containerId:"file-tabs"}),this.addChild(this.tabView);const a=new e.ContextMenu({containerId:"file-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View",action:"view-file",icon:"bi-eye"},{label:"Download",action:"download-file",icon:"bi bi-download"},{label:"Edit Details",action:"edit-file",icon:"bi bi-pencil"},{type:"divider"},this.model.get("is_public")?{label:"Make Private",action:"make-private",icon:"bi bi-lock"}:{label:"Make Public",action:"make-public",icon:"bi bi-unlock"},{type:"divider"},{label:"Delete File",action:"delete-file",icon:"bi bi-trash",danger:!0}]}});this.addChild(a)}async onActionViewFile(){const e=this.model.get("content_type"),t=this.model.get("url");if(e.startsWith("image/")){const e=this.model.get("renditions")||{},a=[{src:t,alt:"Original"},...Object.values(e).map(e=>({src:e.url,alt:e.role}))];o.LightboxGallery.show(a,{fitToScreen:!1})}else"application/pdf"===e?o.PDFViewer.showDialog(t,{title:this.model.get("filename")}):window.open(t,"_blank")}async onActionDownloadFile(){const e=this.model.get("url");if(e){const t=document.createElement("a");t.href=e,t.download=this.model.get("filename"),document.body.appendChild(t),t.click(),document.body.removeChild(t)}}async onActionEditFile(){await a.Dialog.showModelForm({title:`Edit File - ${this.model.get("filename")}`,model:this.model,formConfig:i.FileForms.edit})&&this.render()}async onActionMakePublic(){await this.model.save({is_public:!0}),this.render()}async onActionMakePrivate(){await this.model.save({is_public:!1}),this.render()}async onActionDeleteFile(){await a.Dialog.confirm(`Are you sure you want to delete the file "${this.model.get("filename")}"? This action cannot be undone.`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("file:deleted",{model:this.model})}}i.File.VIEW_CLASS=FileView;class FileTablePage extends i.TablePage{constructor(e={}){super({name:"admin_files",pageName:"Manage Files",router:"admin/files",Collection:i.FileList,formEdit:i.FileForms.edit,itemViewClass:FileView,onAdd:async e=>{await this.handleFileUpload(e)},viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"filename",label:"Filename"},{key:"content_type",label:"Type",formatter:"default('Unknown')"},{key:"file_size",label:"Size",formatter:"filesize"},{key:"group.name",label:"Group",formatter:"default('No Group')"},{key:"upload_status",label:"Status",formatter:"badge"},{key:"created",label:"Uploaded",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No files found. Click "Add File" to upload your first file.',batchBarLocation:"top",batchActions:[{label:"Download",icon:"bi bi-download",action:"batch-download"},{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Move to Group",icon:"bi bi-folder",action:"batch-move"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},...e}),this.enableFileDrop({acceptedTypes:["*/*"],maxFileSize:104857600,multiple:!1,validateOnDrop:!0})}async handleFileUpload(e){e&&e.preventDefault();const t=document.createElement("input");t.type="file",t.accept="*/*",t.multiple=!1,t.style.display="none",t.addEventListener("change",async e=>{const a=e.target.files[0];if(!a)return;const s=104857600;if(a.size>s)this.showError(`File size (${this._formatFileSize(a.size)}) exceeds maximum (${this._formatFileSize(s)})`);else try{const e=new i.File;let t={};this.options.requiresGroup&&this.getApp().activeGroup&&(t.group=this.getApp().activeGroup.id);const s=e.upload({file:a,name:a.name,description:`File uploaded on ${/* @__PURE__ */(new Date).toLocaleDateString()}`,showToast:!0,onProgress:e=>{e.percentage},onComplete:e=>{this.refresh()},onError:e=>{console.error("Upload failed:",e),this.showError("Upload failed: "+e.message)},...t});await s}catch(n){console.error("Error starting file upload:",n),this.showError("Failed to start file upload: "+n.message)}finally{t.remove()}}),document.body.appendChild(t),t.click()}_formatFileSize(e){if(0===e)return"0 Bytes";const t=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,t)).toFixed(2))+" "+["Bytes","KB","MB","GB"][t]}async onFileDrop(e,t,a){const s=e[0];s.name,s.type,s.size;try{const e=new i.File;let t={};this.options.requiresGroup&&this.getApp().activeGroup&&(t.group=this.getApp().activeGroup.id);const a=e.upload({file:s,name:s.name,description:`File uploaded via drag & drop on ${/* @__PURE__ */(new Date).toLocaleDateString()}`,showToast:!0,onProgress:e=>{e.percentage},onComplete:e=>{this.refresh()},onError:e=>{console.error("Upload failed:",e),this.showError("Upload failed: "+e.message)},...t});await a}catch(n){console.error("Error starting file upload:",n),this.showError("Failed to start file upload: "+n.message)}}}r.applyFileDropMixin(FileTablePage);class GeoIPView extends t.View{constructor(e={}){super({className:"geoip-view",...e}),this.model=e.model||new i.GeoLocatedIP(e.data||{}),this.hasCoordinates=this.model.get("latitude")&&this.model.get("longitude"),this.template='\n <div class="geoip-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi bi-globe-americas"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.ip_address}}</h3>\n <div class="text-muted small">\n {{model.city|default(\'Unknown Location\')}}, {{model.country_name|default(\'Unknown Location\')}}\n </div>\n <div class="text-muted small mt-1">\n ISP: {{model.isp|capitalize}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Risk Summary + Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n \x3c!-- Risk summary --\x3e\n <div class="text-end">\n <div class="d-flex align-items-baseline justify-content-end gap-2">\n <span class="text-muted">Risk:</span>\n <span class="fw-bold fs-4\n {{#model.is_threat}} text-danger {{/model.is_threat}}\n {{#model.is_suspicious}} text-warning {{/model.is_suspicious}}\n {{^model.is_threat}}{{^model.is_suspicious}} text-success {{/model.is_suspicious}}{{/model.is_threat}}\n ">{{#model.threat_level}}{{model.threat_level|capitalize}}{{/model.threat_level}}{{^model.threat_level}}Unknown{{/model.threat_level}}</span>\n </div>\n <div class="mt-1 small d-flex align-items-center justify-content-end gap-2">\n <span class="text-muted">Score:</span>\n <span class="fw-semibold">{{model.risk_score|default(\'—\')}}</span>\n </div>\n <div class="mt-1 d-flex align-items-center justify-content-end gap-2">\n <i class="bi bi-shield-lock {{#model.is_tor}}fs-4 text-success{{/model.is_tor}}{{^model.is_tor}}text-muted{{/model.is_tor}}" data-bs-toggle="tooltip" title="TOR exit"></i>\n <i class="bi bi-shield {{#model.is_vpn}}fs-4 text-success{{/model.is_vpn}}{{^model.is_vpn}}text-muted{{/model.is_vpn}}" data-bs-toggle="tooltip" title="VPN detected"></i>\n <i class="bi bi-cloud {{#model.is_cloud}}fs-4 text-success{{/model.is_cloud}}{{^model.is_cloud}}text-muted{{/model.is_cloud}}" data-bs-toggle="tooltip" title="Cloud provider"></i>\n <i class="bi bi-hdd-stack {{#model.is_datacenter}}fs-4 text-success{{/model.is_datacenter}}{{^model.is_datacenter}}text-muted{{/model.is_datacenter}}" data-bs-toggle="tooltip" title="Datacenter"></i>\n <i class="bi bi-phone {{#model.is_mobile}}fs-4 text-success{{/model.is_mobile}}{{^model.is_mobile}}text-muted{{/model.is_mobile}}" data-bs-toggle="tooltip" title="Mobile connection"></i>\n <i class="bi bi-diagram-3 {{#model.is_proxy}}fs-4 text-success{{/model.is_proxy}}{{^model.is_proxy}}text-muted{{/model.is_proxy}}" data-bs-toggle="tooltip" title="Proxy"></i>\n </div>\n </div>\n \x3c!-- Actions: context menu aligned to top (not vertically centered) --\x3e\n <div class="d-flex align-items-start">\n <div data-container="geoip-context-menu"></div>\n </div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="geoip-tabs"></div>\n </div>\n '}async onInit(){this.detailsView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"ip_address",label:"IP Address",cols:4},{name:"subnet",label:"Subnet",cols:4},{name:"country_name",label:"Country",cols:4},{name:"country_code",label:"Country Code",cols:4},{name:"region",label:"Region",cols:4},{name:"city",label:"City",cols:4},{name:"postal_code",label:"Postal Code",cols:4},{name:"timezone",label:"Timezone",cols:4},{name:"latitude",label:"Latitude",cols:4},{name:"longitude",label:"Longitude",cols:4}]}),this.networkView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"is_tor",label:"TOR Exit Node",formatter:"yesnoicon",cols:4},{name:"is_vpn",label:"VPN",formatter:"yesnoicon",cols:4},{name:"is_proxy",label:"Proxy",formatter:"yesnoicon",cols:4},{name:"is_cloud",label:"Cloud Provider",formatter:"yesnoicon",cols:4},{name:"is_datacenter",label:"Datacenter",formatter:"yesnoicon",cols:4},{name:"is_mobile",label:"Mobile",formatter:"yesnoicon",cols:4},{name:"mobile_carrier",label:"Mobile Carrier",cols:8},{name:"asn",label:"ASN",cols:4},{name:"asn_org",label:"ASN Organization",cols:8},{name:"isp",label:"ISP",cols:12},{name:"connection_type",label:"Connection Type",cols:6}]}),this.riskView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"threat_level",label:"Threat Level",cols:6},{name:"risk_score",label:"Risk Score",cols:6},{name:"is_threat",label:"Threat",formatter:"yesnoicon",cols:6},{name:"is_suspicious",label:"Suspicious",formatter:"yesnoicon",cols:6},{name:"is_known_attacker",label:"Known Attacker",formatter:"yesnoicon",cols:6},{name:"is_known_abuser",label:"Known Abuser",formatter:"yesnoicon",cols:6}]}),this.metadataView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"Record ID",cols:6},{name:"provider",label:"Data Provider",formatter:"capitalize",cols:6},{name:"created",label:"Created",formatter:"datetime",cols:6},{name:"modified",label:"Last Modified",formatter:"datetime",cols:6},{name:"last_seen",label:"Last Seen",formatter:"datetime",cols:6},{name:"expires_at",label:"Expires",formatter:"datetime",cols:6}]});const t=new i.IncidentEventList({params:{size:5,source_ip:this.model.get("ip_address")}});this.eventsView=new i.TableView({collection:t,hideActivePillNames:["source_ip"],columns:[{key:"id",label:"ID",sortable:!0,width:"40px"},{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"150px"},{key:"category|badge",label:"Category"},{key:"title",label:"Title"}]});const a=new i.LogList({params:{size:5,ip:this.model.get("ip_address")}});this.logsView=new i.TableView({collection:a,permissions:"view_logs",hideActivePillNames:["ip"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"log",label:"Log"}]});const s={Location:this.detailsView,Network:this.networkView,"Risk & Reputation":this.riskView,Events:this.eventsView,Logs:this.logsView,Metadata:this.metadataView};if(this.hasCoordinates){const e=this.model.get("latitude"),t=this.model.get("longitude"),a=[this.model.get("city")||"Unknown",this.model.get("region")||"",this.model.get("country_name")||""].filter(Boolean).join(", ");this.mapView=new d.MapView({markers:[{lat:e,lng:t,popup:`<strong>${this.model.get("ip_address")}</strong><br>${a}`}],tileLayer:"light",zoom:4,height:450}),s.Map=this.mapView}this.tabView=new i.TabView({containerId:"geoip-tabs",tabs:s,activeTab:this.hasCoordinates?"Map":"Location"}),this.addChild(this.tabView);const l=[{label:"Edit Location",action:"edit-location",icon:"bi-geo-alt"},{label:"Edit Security",action:"edit-security",icon:"bi-shield-lock"},{label:"Edit Network",action:"edit-network",icon:"bi-diagram-3"},{type:"divider"},{label:"Refresh Geolocation",action:"refresh-geoip",icon:"bi-arrow-clockwise"}];this.hasCoordinates&&l.push({label:"View on Map",action:"view-on-map",icon:"bi-map"}),l.push({type:"divider"},{label:"Delete Record",action:"delete-geoip",icon:"bi-trash",danger:!0});const o=new e.ContextMenu({containerId:"geoip-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:l}});this.addChild(o)}async onAfterRender(){await super.onAfterRender(),window.bootstrap&&window.bootstrap.Tooltip&&this.element&&this.element.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(e=>{const t=window.bootstrap.Tooltip.getInstance(e);t&&"function"==typeof t.dispose&&t.dispose(),new window.bootstrap.Tooltip(e)})}async onActionEditLocation(){await a.Dialog.showModelForm({title:`Edit Location - ${this.model.get("ip_address")}`,model:this.model,formConfig:i.GeoLocatedIP.EDIT_LOCATION_FORM})&&(await this.render(),this.getApp()?.toast?.success("Location updated successfully"))}async onActionEditSecurity(){await a.Dialog.showModelForm({title:`Edit Security - ${this.model.get("ip_address")}`,model:this.model,formConfig:i.GeoLocatedIP.EDIT_SECURITY_FORM})&&(await this.render(),this.getApp()?.toast?.success("Security settings updated successfully"))}async onActionEditNetwork(){await a.Dialog.showModelForm({title:`Edit Network - ${this.model.get("ip_address")}`,model:this.model,formConfig:i.GeoLocatedIP.EDIT_NETWORK_FORM})&&(await this.render(),this.getApp()?.toast?.success("Network information updated successfully"))}async onActionRefreshGeoip(){await this.model.save({refresh:!0}),this.getApp()?.toast?.info("Refresh request sent for "+this.model.get("ip_address"))}async onActionThreatAnalysis(){await this.model.save({threat_analysis:!0}),this.getApp()?.toast?.info("Requesting threat analysis for "+this.model.get("ip_address"))}async onActionViewOnMap(){if(this.hasCoordinates){const e=`https://www.google.com/maps/search/?api=1&query=${this.model.get("latitude")},${this.model.get("longitude")}`;window.open(e,"_blank")}}async onActionDeleteGeoip(){await a.Dialog.confirm(`Are you sure you want to delete the GeoIP record for "${this.model.get("ip_address")}"?`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("geoip:deleted",{model:this.model})}static async show(e){const t=await i.GeoLocatedIP.lookup(e);if(t){const e=new GeoIPView({model:t}),s=new a.Dialog({header:!1,size:"lg",body:e,buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}]});return await s.render(!0,document.body),s.show(),s}return a.Dialog.alert({message:`Could not find geolocation data for IP: ${e}`,type:"warning"}),null}}i.GeoLocatedIP.VIEW_CLASS=GeoIPView;class GeoLocatedIPTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_system_geoip",pageName:"GeoIP Cache",router:"admin/system/geoip",Collection:i.GeoLocatedIPList,itemView:GeoIPView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"ip_address",label:"IP Address",sortable:!0},{key:"city",label:"City",sortable:!0,formatter:"default('—')"},{key:"region",label:"Region",sortable:!0,formatter:"default('—')"},{key:"country_name",label:"Country",sortable:!0,formatter:"default('—')"},{key:"isp",label:"ISP",sortable:!0,formatter:"default('—')"},{key:"threat_level",label:"Threat",formatter:"default('—')"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,clickAction:"view",showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:"No GeoIP records found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},tableViewOptions:{addButtonLabel:"Lookup IP",onAdd:e=>{e.preventDefault(),this.onLookup()}}})}async onLookup(){const e=await this.getApp().showForm({title:"Lookup IP",fields:[{name:"ip",type:"text",required:!0}]});if(e&&e.ip){const t=await i.GeoLocatedIP.lookup(e.ip);t&&this.tableView._onRowView({model:t})}}}class GroupView extends t.View{constructor(t={}){super({className:"group-view",...t}),this.model=t.model||new e.Group(t.data||{}),this.tabView=null,this.membersView=null,this.childrenView=null,this.logsView=null,this.template='\n <div class="group-view-container">\n \x3c!-- Group Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n {{#model.avatar}}\n {{{model.avatar|avatar(\'md\',\'rounded\')}}}\n {{/model.avatar}}\n {{^model.avatar}}\n <div class="avatar-placeholder rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi bi-collection text-secondary" style="font-size: 40px;"></i>\n </div>\n {{/model.avatar}}\n <div>\n <h3 class="mb-1">{{model.name|truncate(32)|default(\'Unnamed Group\')}}</h3>\n <div class="text-muted small">\n <span>ID: {{model.id}}</span>\n <span class="mx-2">|</span>\n <span>Kind: {{model.kind|capitalize}}</span>\n {{#model.metadata.timezone}}\n <span class="mx-2">|</span>\n <span><i class="bi bi-clock"></i> {{model.metadata.timezone}}</span>\n {{/model.metadata.timezone}}\n </div>\n {{#model.parent}}\n <div class="text-muted small mt-2">\n <div>Parent: <a href="#" data-action="view-parent" data-id="{{model.parent.id}}">{{model.parent.name|truncate(32)}}</a></div>\n <div>ID: {{model.parent.id}} | Kind: {{model.parent.kind|capitalize}}</div>\n </div>\n {{/model.parent}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <i class="bi bi-circle-fill fs-8 {{model.is_active|boolean(\'text-success\',\'text-secondary\')}}"></i>\n <span>{{model.is_active|boolean(\'Active\',\'Inactive\')}}</span>\n </div>\n {{#model.last_activity}}\n <div class="text-muted small mt-1">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n </div>\n <div data-container="group-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="group-tabs"></div>\n </div>\n '}async onInit(){const t=new i.MemberList({params:{group:this.model.get("id"),size:5}});this.membersView=new i.TableView({collection:t,hideActivePillNames:["group"],columns:[{key:"user.display_name",label:"User",sortable:!0},{key:"user.email",label:"Email",sortable:!0},{key:"created",label:"Date Joined",formatter:"date",sortable:!0}],showAdd:!0,addButtonLabel:"Invite",onAdd:async e=>{this.onInviteClick(e)}});const a=new e.GroupList({params:{parent:this.model.get("id"),size:5}});this.childrenView=new i.TableView({collection:a,hideActivePillNames:["parent"],columns:[{key:"name",label:"Name",sortable:!0},{key:"kind",label:"Kind",formatter:"badge"},{key:"created",label:"Created",formatter:"date",sortable:!0}],toolbarButtons:[{label:"Add Multiple",icon:"bi bi-plus-circle",action:"add-multiple",className:"btn-success"}]});const s=new i.LogList({params:{size:5,model_name:"account.Group",model_id:this.model.get("id")}});this.logsView=new i.TableView({collection:s,permissions:"view_logs",hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime"},{key:"level",label:"Level",sortable:!0,formatter:"badge"},{key:"kind",label:"Kind"},{key:"log",label:"Log"}]}),this.tabView=new i.TabView({tabs:{Members:this.membersView,Children:this.childrenView,Logs:this.logsView},activeTab:"Members",containerId:"group-tabs"}),this.addChild(this.tabView);const n=new e.ContextMenu({containerId:"group-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Group",action:"edit-group",icon:"bi-pencil"},{label:"Add Member",action:"add-member",icon:"bi-person-plus"},{label:"Add Child Group",action:"add-child-group",icon:"bi-diagram-3"},{type:"divider"},this.model.get("is_active")?{label:"Deactivate Group",action:"deactivate-group",icon:"bi-x-circle"}:{label:"Activate Group",action:"activate-group",icon:"bi-check-circle"}]}});this.addChild(n)}async onActionEditGroup(){await a.Dialog.showModelForm({title:`Edit Group - ${this.model.get("name")}`,model:this.model,size:"lg",formConfig:e.GroupForms.detailed})&&this.render()}async onActionAddMember(){this.model.id}async onActionAddChildGroup(){this.model.id}async onActionDeactivateGroup(){this.model.id}async onActionActivateGroup(){this.model.id}async onActionViewParent(e,t){const a=t.dataset.id;this.emit("view-parent-group",{groupId:a})}async onInviteClick(e){e.preventDefault(),e.stopPropagation();const t=this.getApp(),a=await t.showForm({title:"Invite User To "+this.model.get("name"),fields:[{type:"email",name:"email",label:"Email",required:!0,columns:12}]});if(a&&a.email){t.showLoading();const e=await t.rest.POST("/api/group/member/invite",{group:this.model.id,email:a.email});t.hideLoading(),e.success?(t.toast.success("User invited successfully"),this.membersView.collection.fetch()):t.toast.error("Failed to invite user")}}async onActionAddMultiple(){const t=await a.Dialog.showForm({title:"Select Members",fields:[{name:"group_ids",type:"collectionmultiselect",required:!0,Collection:e.GroupList,labelField:"name",itemTemplate:'\n <div class="ms-2">\n <div class="fs-7">{{model.name}}</div>\n <div class="fs-8 text-muted">{{model.kind}}</div>\n </div>\n ',valueField:"id",enableSearch:!0,searchPlaceholder:"Search groups...",defaultParams:{is_active:!0,size:100,group:this.model.id}}]});t&&(console.warn(t),this.getApp().toast.warning("This is only for testing"))}}e.Group.VIEW_CLASS=GroupView;class GroupTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_groups",pageName:"Manage Groups",router:"admin/groups",Collection:e.GroupList,formCreate:e.GroupForms.create,formEdit:e.GroupForms.edit,itemViewClass:GroupView,viewDialogOptions:{header:!1},defaultQuery:{sort:"-id",is_active:1},columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"name",label:"Display Name"},{key:"kind|badge",label:"Kind",filter:{type:"select",options:e.Group.GroupKindOptions}},{key:"is_active|yesnoicon",label:"Enabled",visibility:"lg"},{key:"parent.name",label:"Parent",formatter:"default('-')",visibility:"md",class:"text-muted fs-8"},{key:"created",label:"Created",className:"text-muted fs-8",formatter:"epoch|datetime",visibility:"lg"},{key:"last_activity",label:"Activity",className:"text-muted fs-8",formatter:"relative",visibility:"lg"}],filters:[{key:"is_active",label:"Active",type:"select",options:[{label:"Active",value:!0},{label:"Inactive",value:!1}]}],contextMenu:[{icon:"bi-pencil",action:"edit",label:"Edit Group"},{icon:"bi-bullseye",action:"make-active",label:"Make Active Group"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No groups found. Click "Add Group" to create your first one.',batchBarLocation:"top",batchActions:[{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Activate",icon:"bi bi-check-circle",action:"batch-activate"},{label:"Deactivate",icon:"bi bi-x-circle",action:"batch-deactivate"},{label:"Move",icon:"bi bi-arrow-right",action:"batch-move"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}onActionMakeActive(e,t){const a=this.collection.get(t.dataset.id);this.getApp().setActiveGroup(a)}}class IncidentDashboardHeader extends t.View{constructor(e={}){super({...e,className:"incident-dashboard-header"}),this.stats={tickets:{new:0,open:0,paused:0},incidents:{new:0,open:0,paused:0,recent:0},events:{recent:0,warnings:0,critical:0}},this.setModel(new i.IncidentStats)}async getTemplate(){return'\n <div class="row">\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Open Incidents</h6>\n <h3 class="mb-1 fw-bold">{{model.incidents.open}}</h3>\n <span class="badge bg-danger-subtle text-danger">{{model.incidents.new}} New</span>\n </div>\n <div class="text-danger">\n <i class="bi bi-exclamation-triangle fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Open Tickets</h6>\n <h3 class="mb-1 fw-bold">{{model.tickets.open}}</h3>\n <span class="badge bg-warning-subtle text-warning">{{model.tickets.new}} New</span>\n </div>\n <div class="text-warning">\n <i class="bi bi-ticket-perforated fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Recent Events</h6>\n <h3 class="mb-1 fw-bold">{{model.events.recent}}</h3>\n <span class="badge bg-info-subtle text-info">{{model.events.critical}} Critical</span>\n </div>\n <div class="text-info">\n <i class="bi bi-activity fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Recent Incidents</h6>\n <h3 class="mb-1 fw-bold">{{model.incidents.recent}}</h3>\n <span class="badge bg-secondary-subtle text-secondary">Last 24h</span>\n </div>\n <div class="text-secondary">\n <i class="bi bi-clock-history fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async onBeforeRender(){await this.model.fetch()}}class IncidentDashboardPage extends e.Page{constructor(e={}){super({...e,title:"Incidents Dashboard",className:"incident-dashboard-page"})}async getTemplate(){return'\n <div class="container-fluid">\n <div class="d-flex justify-content-between align-items-center mb-2">\n <div>\n <p class="text-muted mb-0">Incidents & Tickets Dashboard</p>\n <small class="text-info">\n <i class="bi bi-shield-check me-1"></i>\n Real-time incident and event monitoring\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Charts">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n </div>\n\n <div data-container="header"></div>\n\n <div class="row">\n <div class="col-xl-8 col-lg-7">\n <div class="card shadow mb-4">\n <div class="card-body" data-container="incidents-chart"></div>\n </div>\n </div>\n <div class="col-xl-4 col-lg-5">\n <div class="card shadow mb-4">\n <div class="card-body" data-container="incidents-by-state-chart"></div>\n </div>\n </div>\n </div>\n\n <div class="row">\n <div class="col-lg-6 mb-4" data-container="my-tickets-table"></div>\n <div class="col-lg-6 mb-4" data-container="high-priority-incidents-table"></div>\n </div>\n </div>\n '}async onInit(){this.header=new IncidentDashboardHeader({containerId:"header"}),this.addChild(this.header),this.systemIncidentsChart=new s.MetricsChart({title:'<i class="bi bi-exclamation-triangle me-2"></i> System Incidents',endpoint:"/api/metrics/fetch",granularity:"hours",slugs:["incidents"],account:"incident",chartType:"line",showDateRange:!1,showMetricsFilter:!1,height:250,colors:["rgba(255, 193, 7, 0.8)"],yAxis:{label:"Incidents",beginAtZero:!0},tooltip:{y:"number"},containerId:"incidents-chart"}),this.addChild(this.systemIncidentsChart);const e=new i.TicketList({params:{assignee:this.getApp().activeUser.id,status:"open"}});this.myTicketsTable=new i.TableView({containerId:"my-tickets-table",title:"My Open Tickets",collection:e,columns:[{key:"id",label:"ID"},{key:"title",label:"Title"},{key:"priority",label:"Priority"}]}),this.addChild(this.myTicketsTable);const t=new i.IncidentList({params:{priority__gte:8,state:"open"}});this.highPriorityIncidentsTable=new i.TableView({containerId:"high-priority-incidents-table",title:"Recent High-Priority Incidents",collection:t,columns:[{key:"id",label:"ID"},{key:"title",label:"Title"},{key:"state",label:"State",formatter:"badge"}]}),this.addChild(this.highPriorityIncidentsTable)}async onActionRefreshAll(e,t){const a=t.querySelector("i");a.classList.add("bi-spin"),t.disabled=!0,await Promise.all([this.header.statsModel.fetch(),this.systemIncidentsChart.refresh(),this.myTicketsTable.collection.fetch(),this.highPriorityIncidentsTable.collection.fetch()]),a.classList.remove("bi-spin"),t.disabled=!1}}class IncidentHistoryAdapter{constructor(e){this.incidentId=e,this.collection=new i.IncidentHistoryList({params:{incident:this.incidentId}})}async fetch(){return await this.collection.fetch(),this.collection.models.map(e=>this.transform(e))}transform(e){return{id:e.get("id"),type:"comment"===e.get("kind")?"user_comment":"system_event",author:{name:e.get("by.display_name")||"System",avatarUrl:e.get("by.avatar.url")},timestamp:e.get("created"),content:e.get("note"),attachments:[]}}async addNote(e){const t=new i.IncidentHistory({incident:this.incidentId,note:e.text,kind:"comment"}),a=await t.save();return a.success&&await this.collection.fetch(),a}}class IncidentView extends t.View{constructor(e={}){super({className:"incident-view",...e}),this.model=e.model||new i.Incident(e.data||{}),this.incidentIcon=this.getIconForIncident(this.model.get("state")),this.template='\n <div class="incident-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 {{incidentIcon.color}}">\n <i class="bi {{incidentIcon.icon}}"></i>\n </div>\n <div>\n <h3 class="mb-1">Incident #{{model.id}}</h3>\n <div class="text-muted small">\n Category: {{model.category|capitalize}}\n </div>\n <div class="text-muted small mt-1">\n Created: {{model.created|datetime}}\n </div>\n </div>\n </div>\n <div class="d-flex align-items-center gap-4">\n <div class="text-end">\n <div>State: <span class="badge bg-primary">{{model.state|capitalize}}</span></div>\n <div class="text-muted small mt-1">Priority: {{model.priority}}</div>\n </div>\n <div data-container="incident-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="incident-tabs"></div>\n </div>\n '}getIconForIncident(e){const t=e?.toLowerCase();return"resolved"===t||"closed"===t?{icon:"bi-check-circle-fill",color:"text-success"}:"new"===t||"opened"===t?{icon:"bi-exclamation-triangle-fill",color:"text-danger"}:"paused"===t||"ignore"===t?{icon:"bi-pause-circle-fill",color:"text-warning"}:{icon:"bi-shield-exclamation",color:"text-secondary"}}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Incident ID"},{name:"state",label:"State",format:"badge"},{name:"priority",label:"Priority"},{name:"category",label:"Category"},{name:"model_name",label:"Related Model"},{name:"model_id",label:"Related Model ID"},{name:"details",label:"Details",columns:12,format:"pre"}]});const a=new i.IncidentEventList({params:{incident:this.model.get("id")}});this.eventsView=new i.TableView({collection:a,hideActivePillNames:["incident"],columns:[{key:"id",label:"ID",width:"70px",sortable:!0},{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"180px"},{key:"category",label:"Category",formatter:"badge",sortable:!0},{key:"title",label:"Title",sortable:!0},{key:"level",label:"Level",sortable:!0,width:"80px"}],showAdd:!1,actions:["view"],paginated:!0,size:10});const s=new IncidentHistoryAdapter(this.model.get("id"));this.historyView=new i.ChatView({adapter:s});const l={Overview:this.overviewView,Events:this.eventsView,"History & Comments":this.historyView},o=this.model.get("metadata")||{};o.stack_trace&&(this.stackTraceView=new StackTraceView({stackTrace:o.stack_trace}),l["Stack Trace"]=this.stackTraceView),Object.keys(o).length>0&&(this.metadataView=new t.View({model:this.model,template:'<pre class="bg-light p-3 border rounded"><code>{{{model.metadata|json}}}</code></pre>'}),l.Metadata=this.metadataView),this.tabView=new i.TabView({containerId:"incident-tabs",tabs:l,activeTab:"Overview"}),this.addChild(this.tabView);const r=new e.ContextMenu({containerId:"incident-context-menu",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Incident",action:"edit-incident",icon:"bi-pencil"},{label:"Resolve",action:"resolve-incident",icon:"bi-check-circle"},{type:"divider"},{label:"Delete Incident",action:"delete-incident",icon:"bi-trash",danger:!0}]}});this.addChild(r)}async onActionEditIncident(){await a.Dialog.showModelForm({title:`Edit Incident #${this.model.id}`,model:this.model,formConfig:i.IncidentForms.edit})&&this.render()}async onActionResolveIncident(){await this.model.save({state:"resolved"}),this.render(),this.emit("incident:updated",{model:this.model})}async onActionDeleteIncident(){await a.Dialog.confirm("Are you sure you want to delete this incident?","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("incident:deleted",{model:this.model})}}i.Incident.VIEW_CLASS=IncidentView;class IncidentTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_incidents",pageName:"Manage Incidents",router:"admin/incidents",Collection:i.IncidentList,formCreate:i.IncidentForms.create,formEdit:i.IncidentForms.edit,itemViewClass:IncidentView,viewDialogOptions:{header:!1,size:"xl"},defaultQuery:{sort:"-id",status:"new"},columns:[{key:"id",label:"ID",width:"60px",sortable:!0,class:"text-muted"},{key:"status",label:"Status",filter:{type:"select",options:["new","open","paused","resolved","qa","ignored"]}},{key:"created",label:"Created",formatter:"epoch|datetime"},{key:"category",label:"Category",sortable:!0,formatter:"default('General')",filter:{type:"text"}},{key:"priority",label:"Priority",filter:{type:"text"}},{key:"title",label:"title",formatter:"truncate(100)|default('No description')"}],filters:[{key:"category__not",label:"Not Category",filter:{type:"text"}}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No incidents found. Click "Add Incident" to create your first incident.',batchBarLocation:"top",batchActions:[{label:"Open",icon:"bi bi-folder2-open",action:"open"},{label:"Resolve",icon:"bi bi-check-circle",action:"resolve"},{label:"Pause",icon:"bi bi-pause-circle",action:"pause"},{label:"Ignore",icon:"bi bi-x-circle",action:"ignore"},{label:"Merge",icon:"bi bi-merge",action:"merge"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionBatchResolve(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to close ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"resolved"}))),this.tableView.collection.fetch())}async onActionBatchOpen(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to open ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"open"}))),this.tableView.collection.fetch())}async onActionBatchPause(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to pause ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"paused"}))),this.tableView.collection.fetch())}async onActionBatchIgnore(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to ignore ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"ignored"}))),this.tableView.collection.fetch())}async onActionBatchMerge(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp(),i=await s.showForm({title:`Merge ${a.length} incidents`,fields:[{name:"merge",type:"select",label:"Select Parent Incident",options:a.map(e=>({value:e.model.id,label:e.model.id})),required:!0}]});if(!i)return;const n=a.find(e=>e.model.id==i.merge)?.model;if(!n)return;const l=a.map(e=>e.model.id).filter(e=>e!=i.merge);await n.save({merge:l}),this.tableView.collection.fetch()}}class JobStatsView extends t.View{constructor(e={}){super({className:"job-stats-section",...e}),this.stats={pending:0,running:0,completed:0,failed:0,scheduled:0},this.template='\n <div class="job-stats-header mb-4">\n <div class="row">\n <div class="col-xl-2 col-lg-4 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Pending</h6>\n <h3 class="mb-1 fw-bold">{{stats.pending}}</h3>\n <span class="badge bg-primary-subtle text-primary">\n <i class="bi bi-hourglass"></i> Queued\n </span>\n </div>\n <div class="text-primary">\n <i class="bi bi-clock fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-2 col-lg-4 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Running</h6>\n <h3 class="mb-1 fw-bold">{{stats.running}}</h3>\n <span class="badge bg-success-subtle text-success">\n <i class="bi bi-arrow-repeat"></i> Active\n </span>\n </div>\n <div class="text-success">\n <i class="bi bi-play-circle fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-2 col-lg-4 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Scheduled</h6>\n <h3 class="mb-1 fw-bold">{{stats.scheduled}}</h3>\n <span class="badge bg-warning-subtle text-warning">\n <i class="bi bi-calendar-event"></i> Planned\n </span>\n </div>\n <div class="text-warning">\n <i class="bi bi-calendar3 fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Completed</h6>\n <h3 class="mb-1 fw-bold">{{stats.completed}}</h3>\n <span class="badge bg-info-subtle text-info">\n <i class="bi bi-check-circle"></i> Done\n </span>\n </div>\n <div class="text-info">\n <i class="bi bi-check-square fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Failed</h6>\n <h3 class="mb-1 fw-bold">{{stats.failed}}</h3>\n <span class="badge bg-danger-subtle text-danger">\n <i class="bi bi-x-octagon"></i> Errors\n </span>\n </div>\n <div class="text-danger">\n <i class="bi bi-exclamation-triangle fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}_onModelChange(){this.loadStats(),this.isMounted()&&this.render()}async loadStats(){this.stats=this.model.attributes.totals}}class JobHealthView extends t.View{constructor(e={}){super({className:"job-health-section",...e}),this.health={status:"unknown",runners:{active:0,total:0},channels:[]},this.template='\n <div class="job-health-header mb-4">\n <div class="card border-0 shadow">\n <div class="card-body">\n <div class="row align-items-center">\n <div class="col-md-6">\n <div class="d-flex align-items-center">\n <div class="health-indicator me-3">\n <i class="bi bi-circle-fill fs-4 {{healthStatusClass}}"></i>\n </div>\n <div>\n <h5 class="mb-1">System Health: {{health.overall_status|capitalize}}</h5>\n <small class="text-muted d-block">\n Workers: {{health.runners.active}}/{{health.runners.total}} active\n </small>\n <small class="text-muted d-block">\n Scheduler:\n <span class="{{schedulerStatusClass}} fw-bold">\n <i class="bi {{schedulerIcon}} me-1"></i>{{schedulerStatusText}}\n </span>\n </small>\n </div>\n </div>\n </div>\n <div class="col-md-6">\n <div class="d-flex justify-content-end">\n <div class="btn-group">\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-health">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button class="btn btn-sm btn-outline-secondary" data-action="system-settings">\n <i class="bi bi-gear"></i> Settings\n </button>\n </div>\n </div>\n </div>\n </div>\n {{#health.channelsArray.length}}\n <div class="row mt-3">\n <div class="col-12">\n <small class="text-muted d-block mb-2">Channel Status:</small>\n <div class="d-flex flex-wrap gap-2">\n {{#health.channelsArray}}\n <span class="badge {{statusBadgeClass}} d-flex align-items-center">\n <i class="bi {{statusIcon}} me-1"></i>\n {{channel}} ({{queued}} queued, {{inflight}} inflight)\n </span>\n {{/health.channelsArray}}\n </div>\n </div>\n </div>\n {{/health.channelsArray.length}}\n </div>\n </div>\n </div>\n '}_onModelChange(){this.loadHealth(),this.isMounted()&&this.render()}async loadHealth(){if(!this.model._.totals)return;const e=this.model.attributes;let t="healthy";0===e.totals.runners_active?t="critical":e.scheduler.active||(t="warning"),this.health={overall_status:t,channels:e.channels,runners:{active:e.totals.runners_active,total:e.runners.length},totals:e.totals,scheduler:e.scheduler},this.healthStatusClass=this.getHealthStatusClass(this.health.overall_status),this.setupChannelDisplay(),this.setupSchedulerDisplay()}setupChannelDisplay(){this.health.channels&&(this.health.channelsArray=Object.values(this.health.channels).map(e=>{let t="healthy";const a=(e.queued_count||0)+(e.inflight_count||0);return a>50&&(t="warning"),(a>100||0===e.runners)&&(t="critical"),{...e,status:t,statusBadgeClass:this.getChannelBadgeClass(t),statusIcon:this.getChannelIcon(t),queued:e.queued_count||0,inflight:e.inflight_count||0,pending:e.queued_count||0,running:e.inflight_count||0}}))}setupSchedulerDisplay(){if(!this.health.scheduler)return this.schedulerStatusText="Unknown",this.schedulerStatusClass="text-muted",void(this.schedulerIcon="bi-question-circle-fill");this.health.scheduler.active?(this.schedulerStatusText="Running",this.schedulerStatusClass="text-success",this.schedulerIcon="bi-check-circle-fill"):(this.schedulerStatusText="Stopped",this.schedulerStatusClass="text-danger",this.schedulerIcon="bi-x-octagon-fill")}getHealthStatusClass(e){return{healthy:"text-success",warning:"text-warning",critical:"text-danger",unknown:"text-muted"}[e]||"text-muted"}getChannelBadgeClass(e){return{healthy:"bg-success",warning:"bg-warning",critical:"bg-danger"}[e]||"bg-secondary"}getChannelIcon(e){return{healthy:"bi-check-circle-fill",warning:"bi-exclamation-triangle-fill",critical:"bi-x-octagon-fill"}[e]||"bi-dash-circle-fill"}async onActionRefreshHealth(e,t){try{t.disabled=!0,await this.model.fetch()}catch(a){console.error("Failed to refresh health:",a)}finally{t.disabled=!1}}async onActionSystemSettings(){await a.Dialog.showAlert({title:"System Settings",message:"System settings interface coming soon!",type:"info"})}}class JobDetailsView extends t.View{constructor(e={}){super({className:"job-details-view",...e}),this.model=e.model||new i.Job(e.data||{}),this.tabView=null,this.overviewView=null,this.payloadView=null,this.eventsView=null,this.logsView=null,this.autoRefreshInterval=null,this.template='\n <div class="job-details-container">\n \x3c!-- Job Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="avatar-placeholder rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi {{model.statusIcon}} text-secondary" style="font-size: 40px;"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.func|truncate(32)|default(\'Unknown Function\')}}</h3>\n <div class="text-muted small">\n <span>ID: {{model.id}}</span>\n <span class="mx-2">|</span>\n <span>Channel: <span class="badge bg-primary">{{model.channel}}</span></span>\n {{#model.runner_id}}\n <span class="mx-2">|</span>\n <span>Runner: {{model.runner_id|truncate(16)}}</span>\n {{/model.runner_id}}\n </div>\n <div class="text-muted small mt-2">\n <div>Created: {{model.created|datetime}}</div>\n {{#model.started_at}}\n <div>Started: {{model.started_at|datetime}}</div>\n {{/model.started_at}}\n {{#model.finished_at}}\n <div>Finished: {{model.finished_at|datetime}}</div>\n {{/model.finished_at}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <span class="badge {{model.statusBadgeClass}} fs-6">\n <i class="bi {{model.statusIcon}}"></i> {{model.status|uppercase}}\n </span>\n {{#model.cancel_requested}}\n <span class="badge bg-warning ms-1">\n <i class="bi bi-exclamation-triangle"></i> Cancel Requested\n </span>\n {{/model.cancel_requested}}\n </div>\n {{#model.formattedDuration}}\n <div class="text-muted small mt-1">Duration: {{model.formattedDuration}}</div>\n {{/model.formattedDuration}}\n </div>\n <div data-container="job-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="job-details-tabs"></div>\n </div>\n '}async onInit(){this.overviewView=new t.View({template:'\n <div class="job-overview-tab">\n <div class="card border-0 bg-light mb-3">\n <div class="card-body">\n <div class="row">\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Job ID</label>\n <div class="font-monospace">{{model.id}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Function</label>\n <div class="font-monospace">{{model.func}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Channel</label>\n <div>\n <span class="badge bg-primary">{{model.channel}}</span>\n </div>\n </div>\n {{#model.runner_id}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Runner</label>\n <div class="font-monospace small">{{model.runner_id}}</div>\n </div>\n {{/model.runner_id}}\n </div>\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Status</label>\n <div>\n <span class="badge {{model.statusBadgeClass}} fs-6">\n <i class="bi {{model.statusIcon}}"></i> {{model.status|uppercase}}\n </span>\n {{#model.cancel_requested}}\n <span class="badge bg-warning ms-1">\n <i class="bi bi-exclamation-triangle"></i> Cancel Requested\n </span>\n {{/model.cancel_requested}}\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Created</label>\n <div>{{model.created|datetime}}</div>\n </div>\n {{#model.started_at}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Started</label>\n <div>{{model.started_at|datetime}}</div>\n </div>\n {{/model.started_at}}\n {{#model.finished_at}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Finished</label>\n <div>{{model.finished_at|datetime}}</div>\n </div>\n {{/model.finished_at}}\n {{#model.duration_ms}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Duration</label>\n <div>{{model.formattedDuration}}</div>\n </div>\n {{/model.duration_ms}}\n </div>\n </div>\n </div>\n </div>\n </div>\n ',model:this.model}),this.payloadView=new t.View({template:'\n <div class="job-payload-tab">\n <pre class="bg-light p-3 rounded"><code>{{{model.payload|json}}}</code></pre>\n </div>\n ',model:this.model});const a=new i.JobEventList({params:{job:this.model.get("id"),size:10}});this.eventsView=new i.TableView({collection:a,hideActivePillNames:["job"],columns:[{key:"at",label:"Timestamp",formatter:"datetime",sortable:!0},{key:"event",label:"Event",formatter:"badge"},{key:"details|json",label:"Details"}]});const s=new i.JobLogList({params:{job:this.model.get("id"),size:10}});this.logsView=new i.TableView({collection:s,hideActivePillNames:["job"],columns:[{key:"created|datetime",label:"Created",sortable:!0},{key:"kind",label:"Kind",formatter:"badge"},{key:"message",label:"Message"}]}),this.tabView=new i.TabView({tabs:{Overview:this.overviewView,Payload:this.payloadView,Events:this.eventsView,Logs:this.logsView},activeTab:"Overview",containerId:"job-details-tabs"}),this.addChild(this.tabView);const n=[{label:"Refresh",action:"refresh-job",icon:"bi-arrow-clockwise"}];this.model.canCancel&&this.model.canCancel()&&n.push({label:"Cancel Job",action:"cancel-job",icon:"bi-x-circle",class:"text-danger"}),this.model.canRetry&&this.model.canRetry()&&n.push({label:"Retry Job",action:"retry-job",icon:"bi-arrow-repeat",class:"text-primary"});const l=new e.ContextMenu({containerId:"job-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:n}});this.addChild(l),await this.model.fetch({params:{graph:"detail"}})}async onBeforeRender(){await this.prepareJobData()}async prepareJobData(){this.model&&(this.model._.statusBadgeClass=this.model.getStatusBadgeClass?this.model.getStatusBadgeClass():"bg-secondary",this.model._.statusIcon=this.model.getStatusIcon?this.model.getStatusIcon():"bi-question-circle",this.model._.formattedDuration=this.model.getFormattedDuration?this.model.getFormattedDuration():"N/A")}async loadJobDetails(){if(this.model?.get("id"))try{this.model.getDetailedStatus&&await this.model.getDetailedStatus(),await this.prepareJobData()}catch(e){console.error("Failed to load job details:",e)}}async onActionRefreshJob(){await this.model.fetch({params:{graph:"detail"}})}async onActionCancelJob(){if(confirm("Are you sure you want to cancel this job?"))try{const e=await this.model.cancel();e.success?(await this.loadJobDetails(),await this.render(),this.emit("job-cancelled",{job:this.model})):alert("Failed to cancel job: "+(e.data?.error||"Unknown error"))}catch(e){console.error("Failed to cancel job:",e),alert("Failed to cancel job: "+e.message)}}async onActionRetryJob(){const e=await a.Dialog.showForm({title:"Retry Job",formConfig:i.JobForms.retry});if(e)try{const t=await this.model.retry(e.delay||0);t.success?this.emit("job-retried",{job:this.model,newJobId:t.newJobId}):alert("Failed to retry job: "+(t.data?.error||"Unknown error"))}catch(t){console.error("Failed to retry job:",t),alert("Failed to retry job: "+t.message)}}startAutoRefresh(){this.autoRefreshInterval&&clearInterval(this.autoRefreshInterval),this.model?.isActive&&this.model.isActive()&&(this.autoRefreshInterval=setInterval(async()=>{try{await this.loadJobDetails(),this.isMounted()&&await this.render()}catch(e){console.error("Auto-refresh failed:",e)}},5e3))}stopAutoRefresh(){this.autoRefreshInterval&&(clearInterval(this.autoRefreshInterval),this.autoRefreshInterval=null)}async onDestroy(){this.stopAutoRefresh(),await super.onDestroy()}static async show(e,t={}){const s=new JobDetailsView({model:e});return await a.Dialog.showDialog({title:`<i class="bi bi-info-circle me-2"></i>Job Details - ${e.get("id")}`,body:s,size:"xl",scrollable:!0,buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}],onHide:()=>s.stopAutoRefresh(),...t})}}i.Job.VIEW_CLASS=JobDetailsView;class JobsTable extends i.TableView{constructor(e={}){super({Collection:i.JobList,collectionParams:{size:15,sort:"-created"},options:{searchable:!0,sortable:!0,paginated:!0,size:15,...e.options},columns:[{key:"id",label:"Job ID",formatter:"truncate_middle(12)",sortable:!0,filter:{type:"text",placeholder:"Job ID..."}},{key:"status",label:"Status",formatter:(e,t)=>{const a=t.row;return`<span class="badge ${a.getStatusBadgeClass?a.getStatusBadgeClass():"bg-secondary"}"><i class="${a.getStatusIcon?a.getStatusIcon():"bi-question"} me-1"></i>${e.toUpperCase()}</span>`},sortable:!0,filter:{type:"select",options:[{value:"pending",label:"Pending"},{value:"running",label:"Running"},{value:"completed",label:"Completed"},{value:"failed",label:"Failed"},{value:"canceled",label:"Canceled"},{value:"expired",label:"Expired"}]}},{key:"channel",label:"Channel",formatter:"badge",sortable:!0,filter:{type:"text",placeholder:"Channel..."}},{key:"created",label:"Created",formatter:"datetime",sortable:!0,filter:{type:"daterange",label:"Created Date"}},{key:"started_at",label:"Started",formatter:"datetime",sortable:!0},{key:"finished_at",label:"Finished",formatter:"datetime",sortable:!0}],contextMenu:[{label:"View Details",action:"view-job-details",icon:"bi-info-circle"},{label:"View Events",action:"view-job-events",icon:"bi-clock-history"},{separator:!0},{label:"Cancel Job",action:"cancel-job",icon:"bi-x-circle",danger:!0,condition:e=>e.canCancel&&e.canCancel()},{label:"Retry Job",action:"retry-job",icon:"bi-arrow-clockwise",condition:e=>e.canRetry&&e.canRetry()},{label:"Clone Job",action:"clone-job",icon:"bi-copy"},{separator:!0},{label:"Export Job",action:"export-job",icon:"bi-download"}],batchActions:[{label:"Cancel Selected",action:"batch-cancel",icon:"bi-x-circle"},{label:"Retry Selected",action:"batch-retry",icon:"bi-arrow-clockwise"},{label:"Export Selected",action:"batch-export",icon:"bi-download"}],...e})}async onItemViewJobDetails(e){e&&await JobDetailsView.show(e)}async onItemCancelJob(e){const t=await a.Dialog.showConfirm("Are you sure you want to cancel this job?");if(e&&t)try{const t=await e.cancel();t.success?(this.getApp().toast.success("Job cancelled successfully"),await this.collection.fetch()):this.getApp().toast.error(t.data?.error||"Failed to cancel job")}catch(s){this.getApp().toast.error("Error cancelling job: "+s.message)}}async onItemRetryJob(e){if(e){const s=await a.Dialog.showForm({title:"Retry Job",formConfig:i.JobForms.retry});if(s)try{const t=await e.retry(s.delay||0);t.success?(this.getApp().toast.success("Job queued for retry"),await this.collection.fetch()):this.getApp().toast.error(t.data?.error||"Failed to retry job")}catch(t){this.getApp().toast.error("Error retrying job: "+t.message)}}}async onItemCloneJob(e){if(e){const s=e.getPayload(),n=await a.Dialog.showForm({title:"Clone Job",formConfig:{...i.JobForms.clone,fields:i.JobForms.clone.fields.map(t=>("payload"===t.name?t.value=JSON.stringify(s,null,2):"channel"===t.name&&(t.value=e.get("channel")),t))}});if(n)try{let t={};n.payload&&(t=JSON.parse(n.payload));const a={payload:t,channel:n.channel||e.get("channel"),delay:n.delay||0},s=await e.cloneJob(a);s.success?(this.getApp().toast.success("Job cloned successfully"),await this.collection.fetch()):this.getApp().toast.error(s.data?.error||"Failed to clone job")}catch(t){this.getApp().toast.error("Error cloning job: "+t.message)}}}}class RunnersTable extends i.TableView{constructor(e={}){super({Collection:i.JobRunnerList,options:{searchable:!0,sortable:!0,paginated:!0,size:10,...e.options},columns:[{key:"runner_id",label:"Runner ID",formatter:"truncate_middle(16)",sortable:!0},{key:"alive",label:"Status",formatter:e=>{const t=!0===e;return`<span class="badge ${t?"bg-success":"bg-danger"}"><i class="${t?"bi-check-circle-fill":"bi-x-octagon-fill"} me-1"></i>${t?"ALIVE":"DEAD"}</span>`},sortable:!0,filter:{type:"select",options:[{value:!0,label:"Alive"},{value:!1,label:"Dead"}]}},{key:"channels",label:"Channels",formatter:e=>e&&e.length?e.map(e=>`<span class="badge bg-secondary me-1">${e}</span>`).join(""):"None",sortable:!1},{key:"jobs_processed",label:"Processed",sortable:!0},{key:"jobs_failed",label:"Failed",formatter:e=>`<span class="badge ${e>0?"bg-danger":"bg-success"}">${e}</span>`,sortable:!0},{key:"last_heartbeat",label:"Last Heartbeat",formatter:e=>{if(!e)return"Never";const t=new Date(e),a=/* @__PURE__ */new Date-t,s=Math.floor(a/1e3);return s<60?`${s}s ago`:s<3600?`${Math.floor(s/60)}m ago`:`${Math.floor(s/3600)}h ago`},sortable:!0},{key:"started",label:"Uptime",formatter:e=>{if(!e)return"Unknown";const t=new Date(e),a=/* @__PURE__ */new Date-t,s=Math.floor(a/1e3);return s<60?`${s}s`:s<3600?`${Math.floor(s/60)}m`:s<86400?`${Math.floor(s/3600)}h`:`${Math.floor(s/86400)}d`},sortable:!0}],contextMenu:[{label:"Ping Runner",action:"ping-runner",icon:"bi-wifi"},{label:"View Details",action:"view-runner-details",icon:"bi-info-circle"},{separator:!0},{label:"Pause Runner",action:"pause-runner",icon:"bi-pause-circle",condition:e=>!0===e.get("alive")},{label:"Resume Runner",action:"resume-runner",icon:"bi-play-circle",condition:e=>!0!==e.get("alive")},{separator:!0},{label:"Shutdown Runner",action:"shutdown-runner",icon:"bi-power",danger:!0,condition:e=>!0===e.get("alive")}],...e})}async onActionPingRunner(e,t){const a=t.getAttribute("data-id"),s=this.collection.get(a);if(s)try{const e=await s.ping();e.success?(this.getApp().toast.success("Runner ping successful"),await this.collection.fetch()):this.getApp().toast.error(e.data?.error||"Runner ping failed")}catch(i){this.getApp().toast.error("Error pinging runner: "+i.message)}}async onActionShutdownRunner(e,t){const a=t.getAttribute("data-id"),s=this.collection.get(a);if(s&&confirm("Are you sure you want to shutdown this runner?"))try{const e=await s.shutdown(!0);e.success?(this.getApp().toast.success("Runner shutdown initiated"),await this.collection.fetch()):this.getApp().toast.error(e.data?.error||"Failed to shutdown runner")}catch(i){this.getApp().toast.error("Error shutting down runner: "+i.message)}}}class ScheduledJobsTable extends i.TableView{constructor(e={}){super({Collection:i.JobList,collectionParams:{status:"pending"},hideActivePillNames:["status"],options:{searchable:!0,sortable:!0,paginated:!0,size:10,...e.options},columns:[{key:"id",label:"Job ID",formatter:"truncate_middle(12)",sortable:!0},{key:"func",label:"Function",sortable:!0},{key:"channel",label:"Channel",formatter:"badge",sortable:!0},{key:"run_at",label:"Scheduled For",formatter:"datetime",sortable:!0},{key:"created",label:"Created",formatter:"datetime",sortable:!0},{key:"expires_at",label:"Expires At",formatter:"datetime",sortable:!0}],...e})}}class JobsAdminPage extends e.Page{constructor(e={}){super({title:"Jobs Management",className:"jobs-admin-page",...e}),this.pageTitle="Jobs Management",this.pageSubtitle="Async job monitoring and runner management",this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString(),this.autoRefreshInterval=null,this.refreshRate=3e4,this.template='\n <div class="jobs-admin-container">\n \x3c!-- Page Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div>\n <h1 class="h3 mb-1">{{pageTitle}}</h1>\n <p class="text-muted mb-0">{{pageSubtitle}}</p>\n <small class="text-info">\n <i class="bi bi-arrow-clockwise me-1"></i>\n Auto-refresh: {{refreshRateSeconds}}s | Last updated: {{lastUpdated}}\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Data">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button type="button" class="btn btn-outline-primary btn-sm"\n data-action="export-data" title="Export Data">\n <i class="bi bi-download"></i> Export\n </button>\n <div class="dropdown">\n <button class="btn btn-outline-secondary btn-sm dropdown-toggle"\n type="button" data-bs-toggle="dropdown">\n <i class="bi bi-gear"></i> Settings\n </button>\n <ul class="dropdown-menu dropdown-menu-end">\n <li><h6 class="dropdown-header">Auto Refresh</h6></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="5">5 seconds</button></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="10">10 seconds</button></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="30">30 seconds</button></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="0">Off</button></li>\n <li><hr class="dropdown-divider"></li>\n <li><button class="dropdown-item" data-action="runner-broadcast">Broadcast Command</button></li>\n </ul>\n </div>\n <div class="dropdown">\n <button class="btn btn-danger btn-sm dropdown-toggle"\n type="button" data-bs-toggle="dropdown">\n <i class="bi bi-wrench"></i> Manage\n </button>\n <ul class="dropdown-menu dropdown-menu-end">\n <li><button class="dropdown-item" data-action="run-simple-job"><i class="bi bi-play-circle me-2"></i>Run Simple Job</button></li>\n <li><button class="dropdown-item" data-action="run-test-jobs"><i class="bi bi-robot me-2"></i>Run Test Jobs</button></li>\n <li><hr class="dropdown-divider"></li>\n <li><button class="dropdown-item" data-action="clear-stuck"><i class="bi bi-wrench me-2"></i>Clear Stuck Jobs</button></li>\n <li><button class="dropdown-item" data-action="clear-channel"><i class="bi bi-eraser me-2"></i>Clear Channel</button></li>\n <li><button class="dropdown-item" data-action="purge-jobs"><i class="bi bi-trash me-2"></i>Purge Jobs</button></li>\n <li><button class="dropdown-item" data-action="cleanup-consumers"><i class="bi bi-people me-2"></i>Cleanup Consumers</button></li>\n </ul>\n </div>\n </div>\n </div>\n\n \x3c!-- Job Stats --\x3e\n <div data-container="job-stats"></div>\n\n \x3c!-- Job Health --\x3e\n <div class="row">\n <div class="col-12">\n <div data-container="job-health"></div>\n </div>\n <div class="col-12">\n <div class="mb-3" data-container="job-metrics"></div>\n </div>\n </div>\n\n \x3c!-- Job Tables --\x3e\n <div class="card border shadow">\n <div class="card-header">\n <h5 class="card-title mb-0">\n <i class="bi bi-list-task me-2"></i>Job Management\n </h5>\n </div>\n <div class="card-body">\n <div data-container="job-tables"></div>\n </div>\n </div>\n </div>\n '}async onInit(){this.jobStats=new i.JobsEngineStats,this.jobStatsView=new JobStatsView({containerId:"job-stats",model:this.jobStats}),this.addChild(this.jobStatsView),this.jobHealthView=new JobHealthView({containerId:"job-health",model:this.jobStats}),this.addChild(this.jobHealthView),this.jobTablesView=new i.TabView({containerId:"job-tables",tabs:{Jobs:new JobsTable,Runners:new RunnersTable,Scheduled:new ScheduledJobsTable},activeTab:"Jobs"}),this.addChild(this.jobTablesView),this.jobMetricsChart=new s.MetricsChart({title:'<i class="bi bi-graph-up me-2"></i> Job Metrics',endpoint:"/api/metrics/fetch",height:100,granularity:"hours",category:"jobs_channels",account:"global",chartType:"bar",showDateRange:!1,yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"job-metrics"}),this.addChild(this.jobMetricsChart),await this.jobStats.fetch()}startAutoRefresh(){this.autoRefreshInterval&&clearInterval(this.autoRefreshInterval),this.refreshRate>0&&(this.autoRefreshInterval=setInterval(async()=>{await this.refreshData()},this.refreshRate))}async refreshData(){try{await this.jobStats.fetch();const e=this.jobTablesView?.getActiveTab();if(e){const t=this.jobTablesView.getTab(e);t?.collection?.fetch&&await t.collection.fetch()}this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString(),this.updateHeaderTimestamp()}catch(e){console.error("Failed to refresh jobs dashboard:",e)}}updateHeaderTimestamp(){const e=this.element?.querySelector(".text-info");e&&(e.innerHTML=`\n <i class="bi bi-arrow-clockwise me-1"></i>\n Auto-refresh: ${this.refreshRate/1e3}s | Last updated: ${this.lastUpdated}\n `)}get refreshRateSeconds(){return this.refreshRate/1e3}async onActionRefreshAll(e,t){try{const e=t.querySelector("i");e?.classList.add("spinning"),t.disabled=!0,await this.refreshData(),await this.render()}catch(a){console.error("Failed to refresh jobs dashboard:",a)}finally{const e=t.querySelector("i");e?.classList.remove("spinning"),t.disabled=!1}}async onActionSetRefreshRate(e,t){const a=1e3*parseInt(t.getAttribute("data-rate"));this.refreshRate=a,this.startAutoRefresh();const s=0===a?"Off":a/1e3+"s";this.getApp().toast.success(`Auto-refresh set to ${s}`)}async onActionExportData(){await a.Dialog.showAlert({title:"Export Data",message:"Data export functionality coming soon!",type:"info"})}async onActionRunSimpleJob(e,t){await a.Dialog.showConfirm({title:"Run Simple Job",message:"This will run a simple test job to verify the job system is working correctly.",confirmText:"Run Test",confirmClass:"btn-success"})&&await this.executeJobAction(t,()=>i.Job.test(),"Test job started successfully")}async onActionRunTestJobs(e,t){await a.Dialog.showConfirm({title:"Run Test Jobs",message:"This will run a suite of test jobs to verify all job functionalities.",confirmText:"Run Tests",confirmClass:"btn-success"})&&await this.executeJobAction(t,()=>i.Job.tests(),"Test suite started successfully")}async onActionClearStuck(e,t){const s=[{value:"",label:"All Channels"},...(this.jobHealthView?.health?.channelsArray||[]).map(e=>({value:e.channel,label:e.channel}))],n=await a.Dialog.showForm({title:"Clear Stuck Jobs",formConfig:{fields:[{name:"channel",type:"select",label:"Channel",options:s,value:"",help:"Select specific channel or leave empty for all channels"}]}});n&&await this.executeJobAction(t,()=>i.Job.clearStuck(n.channel||null),e=>{const t=e.data.count||0;return`Cleared ${t} stuck job${1!==t?"s":""}${n.channel?` from channel "${n.channel}"`:""}`})}async onActionClearChannel(e,t){const s=(this.jobHealthView?.health?.channelsArray||[]).map(e=>({value:e.channel,label:e.channel})),n=await a.Dialog.showForm({title:"Clear Channel",formConfig:{fields:[{name:"channel",type:"select",label:"Channel",options:s,required:!0,help:"Select the channel to clear."}]}});n&&await this.executeJobAction(t,()=>i.Job.clearChannel(n.channel),`Channel "${n.channel}" cleared successfully.`)}async onActionPurgeJobs(e,t){const s=await a.Dialog.showForm({title:"Purge Old Jobs",formConfig:{fields:[{name:"days_old",type:"number",label:"Days Old",value:30,required:!0,help:"Delete jobs older than this many days."}]}});s&&await this.executeJobAction(t,()=>i.Job.purgeJobs(s.days_old),e=>`Purged ${e.data.count||0} old job(s).`)}async onActionCleanupConsumers(e,t){await a.Dialog.showConfirm({title:"Cleanup Consumers",message:"This will remove stale consumer records from the system. This is generally safe.",confirmText:"Cleanup",confirmClass:"btn-warning"})&&await this.executeJobAction(t,()=>i.Job.cleanConsumers(),e=>`Cleaned up ${e.data.count||0} consumer(s).`)}async onActionRunnerBroadcast(){const e=await a.Dialog.showForm({title:"Broadcast Command to All Runners",formConfig:i.JobRunnerForms.broadcast});if(e)try{const t=await i.JobRunner.broadcast(e.command,{},e.timeout);t.success?(this.getApp().toast.success(`Broadcast command "${e.command}" sent successfully`),await this.refreshData()):this.getApp().toast.error(t.data?.error||"Failed to broadcast command")}catch(t){console.error("Failed to broadcast command:",t),this.getApp().toast.error("Error broadcasting command: "+t.message)}}async executeJobAction(e,t,a){try{e.disabled=!0;const s=e.querySelector("i");s?.classList.add("spinning");const i=await t();if(i.success&&i.data?.status){const e="function"==typeof a?a(i):a;this.getApp().toast.success(e),await this.refreshData()}else this.getApp().toast.error(i.data?.error||"Operation failed")}catch(s){console.error("Job action failed:",s),this.getApp().toast.error("Error: "+s.message)}finally{e.disabled=!1;const t=e.querySelector("i");t?.classList.remove("spinning")}}async onEnter(){this.startAutoRefresh()}async onExit(){this.autoRefreshInterval&&(clearInterval(this.autoRefreshInterval),this.autoRefreshInterval=null)}async refreshDashboard(){return await this.refreshData()}getStats(){return this.jobStatsView?.stats||{}}getHealth(){return this.jobHealthView?.health||{}}}class DeviceView extends t.View{constructor(t={}){super({className:"device-view",...t}),this.model=t.model||new e.UserDevice(t.data||{}),this.deviceInfo=this.model.get("device_info")||{},this.deviceIcon=this.getIconForDevice(this.deviceInfo),this.template='\n <div class="device-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi {{deviceIcon}}"></i>\n </div>\n <div>\n <h3 class="mb-1">\n {{deviceInfo.user_agent.family}} on {{deviceInfo.os.family}}\n </h3>\n <div class="text-muted small">\n DUID: {{model.duid|truncate_middle(32)}}\n </div>\n <div class="text-muted small mt-1">\n User: <a href="#" data-action="view-user">{{model.user.display_name}}</a>\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div class="text-end">\n <div class="text-muted small">Last Seen</div>\n <div>{{model.last_seen|relative}}</div>\n <div class="text-muted small">from {{model.last_ip}}</div>\n </div>\n <div data-container="device-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="device-tabs"></div>\n </div>\n '}getIconForDevice(e){const t=e?.os?.family?.toLowerCase()||"",a=e?.user_agent?.family?.toLowerCase()||"",s=e?.device?.family?.toLowerCase()||"";return a.includes("chrome")?"bi-browser-chrome":a.includes("firefox")?"bi-browser-firefox":a.includes("safari")?"bi-browser-safari":a.includes("edge")?"bi-browser-edge":t.includes("mac")||t.includes("ios")?"bi-apple":t.includes("windows")?"bi-windows":t.includes("android")?"bi-android2":t.includes("linux")?"bi-ubuntu":s.includes("iphone")?"bi-phone":s.includes("ipad")?"bi-tablet":"bi-laptop"}async onInit(){this.infoView=new t.View({model:this.model,className:"p-3",template:'\n <div class="list-group">\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Browser</h6>\n </div>\n <p class="mb-1 fs-5">{{model.device_info.user_agent.family}} {{model.device_info.user_agent.major}}</p>\n </div>\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Operating System</h6>\n </div>\n <p class="mb-1 fs-5">{{model.device_info.os.family}} {{model.device_info.os.major}}.{{model.device_info.os.minor}}</p>\n </div>\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Device</h6>\n </div>\n <p class="mb-1 fs-5">{{model.device_info.device.brand}} {{model.device_info.device.model}}</p>\n </div>\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Full User Agent</h6>\n </div>\n <small class="text-muted" style="word-break: break-all;">{{model.device_info.string}}</small>\n </div>\n </div>\n '});const a=new e.UserDeviceLocationList({params:{user_device:this.model.get("id"),size:10}});this.locationsView=new i.TableView({collection:a,hideActivePillNames:["user_device"],columns:[{key:"ip_address",label:"IP Address",sortable:!0},{key:"geolocation.city",label:"City",formatter:"default('—')"},{key:"geolocation.region",label:"Region",formatter:"default('—')"},{key:"geolocation.country_name",label:"Country",formatter:"default('—')"},{key:"first_seen",label:"First Seen",formatter:"datetime"},{key:"last_seen",label:"Last Seen",formatter:"datetime"}]}),this.tabView=new i.TabView({tabs:{Info:this.infoView,Locations:this.locationsView},activeTab:"Info",containerId:"device-tabs"}),this.addChild(this.tabView);const s=new e.ContextMenu({containerId:"device-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View User",action:"view-user",icon:"bi-person"},{label:"Block Device",action:"block-device",icon:"bi-shield-slash",disabled:!0},{type:"divider"},{label:"Delete Record",action:"delete-device",icon:"bi-trash",danger:!0}]}});this.addChild(s)}async onActionViewUser(){this.model.get("user"),this.emit("view-user",{userId:this.model.get("user")?.id})}async onActionDeleteDevice(){this.model.id}static async show(t){const s=await e.UserDevice.getByDuid(t);if(s){const e=new DeviceView({model:s}),t=new a.Dialog({header:!1,size:"lg",body:e,buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}]});return await t.render(!0,document.body),t.show(),t}return a.Dialog.alert({message:`Could not find device with DUID: ${t}`,type:"warning"}),null}}e.UserDevice.VIEW_CLASS=DeviceView;class LogView extends t.View{constructor(e={}){super({className:"log-view",...e}),this.model=e.model||new i.Log(e.data||{}),this.logIcon=this.getIconForLog(this.model.get("level")),this.template='\n <div class="log-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 {{logIcon.color}}">\n <i class="bi {{logIcon.icon}}"></i>\n </div>\n <div>\n <h4 class="mb-1">\n <span class="badge bg-secondary">{{model.method}}</span> {{model.path}}\n </h4>\n <div class="text-muted small">\n {{model.created|datetime}}\n </div>\n </div>\n </div>\n <div data-container="log-context-menu"></div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="log-tabs"></div>\n </div>\n '}getIconForLog(e){const t=e?.toLowerCase();return"error"===t||"critical"===t?{icon:"bi-x-octagon-fill",color:"text-danger"}:"warning"===t?{icon:"bi-exclamation-triangle-fill",color:"text-warning"}:"info"===t?{icon:"bi-info-circle-fill",color:"text-info"}:{icon:"bi-journal-text",color:"text-secondary"}}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Log ID"},{name:"level",label:"Level",format:"badge"},{name:"kind",label:"Kind"},{name:"ip",label:"IP Address",template:'<a href="#" data-action="view-ip">{{model.ip}}</a>'},{name:"uid",label:"User ID"},{name:"username",label:"Username"},{name:"duid",label:"Device ID",template:'<a href="#" data-action="view-device">{{model.duid|truncate_middle(32)}}</a>'},{name:"model_name",label:"Related Model"},{name:"model_id",label:"Related Model ID"}]});const a=this.model.get("log");let s=a;try{const e=JSON.parse(a);s=JSON.stringify(e,null,2)}catch(o){}this.logContentView=new t.View({template:`\n <div class="position-relative">\n <button class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0 mt-2 me-2" data-action="copy-log">\n <i class="bi bi-clipboard"></i> Copy\n </button>\n <pre class="bg-light p-3 border rounded" style="max-height: 600px; overflow-y: auto;"><code>${s}</code></pre>\n </div>\n `,onActionCopyLog:()=>{navigator.clipboard.writeText(s),this.getApp()?.toast?.success("Log content copied to clipboard.")}}),this.tabView=new i.TabView({containerId:"log-tabs",tabs:{Overview:this.overviewView,"Log Content":this.logContentView},activeTab:"Overview"}),this.addChild(this.tabView);const l=new e.ContextMenu({containerId:"log-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View User",action:"view-user",icon:"bi-person",disabled:!this.model.get("uid")},{label:"View Device",action:"view-device",icon:"bi-phone",disabled:!this.model.get("duid")},{type:"divider"},{label:"Delete Log",action:"delete-log",icon:"bi-trash",danger:!0}]}});this.addChild(l)}async onActionViewIp(e){e.preventDefault();const t=this.model.get("ip");t&&GeoIPView.show(t)}async onActionViewDevice(e){e.preventDefault();const t=this.model.get("duid");t&&DeviceView.show(t)}async onActionViewUser(){this.model.get("uid")}async onActionDeleteLog(){await a.Dialog.confirm("Are you sure you want to delete this log entry? This action cannot be undone.","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("log:deleted",{model:this.model})}}i.Log.VIEW_CLASS=LogView;class LogTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_logs",pageName:"Manage Logs",router:"admin/logs",Collection:i.LogList,itemViewClass:LogView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"created|epoch|datetime",label:"Timestamp",sortable:!0,filter:{type:"daterange"}},{key:"level",label:"Level",sortable:!0,formatter:"badge",filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{key:"method",label:"Method",filter:{type:"text"}},{key:"path",label:"Path",filter:{type:"text"}},{key:"username",label:"User",filter:{type:"text"}},{key:"ip",label:"IP",filter:{type:"text"}},{key:"duid",label:"Browser ID",formatter:"truncate_middle(16)",filter:{type:"text"}}],defaultQuery:{sort:"-created"},selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No log entries found.",batchBarLocation:"top",batchActions:[{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Archive",icon:"bi bi-archive",action:"batch-archive"},{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Mark as Reviewed",icon:"bi bi-check2",action:"batch-reviewed"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class MemberView extends t.View{constructor(e={}){super({className:"member-view",...e}),this.model=e.model||new i.Member(e.data||{}),this.template='\n <div class="member-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-4">\n <div class="d-flex align-items-center gap-3">\n {{{model.user.avatar|avatar(\'md\',\'rounded-circle\')}}}\n <div>\n <h3 class="mb-0">{{model.user.display_name}}</h3>\n <div class="text-muted">Member of <strong>{{model.group.name}}</strong></div>\n </div>\n </div>\n\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div>Role: <span class="badge bg-primary">{{model.role|capitalize}}</span></div>\n <div class="text-muted small mt-1">Status: {{model.status|capitalize}}</div>\n </div>\n <div data-container="member-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="member-tabs"></div>\n </div>\n '}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Membership ID"},{name:"user.id",label:"User ID"},{name:"user.display_name",label:"User Name"},{name:"user.email",label:"User Email"},{name:"group.id",label:"Group ID"},{name:"group.name",label:"Group Name"},{name:"role",label:"Role"},{name:"status",label:"Status"},{name:"created",label:"Date Joined",format:"datetime"}]}),this.tabView=new i.TabView({containerId:"member-tabs",tabs:{Overview:this.overviewView},activeTab:"Overview"}),this.addChild(this.tabView);const t=new e.ContextMenu({containerId:"member-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Membership",action:"edit-membership",icon:"bi-pencil"},{type:"divider"},{label:"View User",action:"view-user",icon:"bi-person"},{label:"View Group",action:"view-group",icon:"bi-diagram-3"},{type:"divider"},{label:"Remove From Group",action:"remove-member",icon:"bi-trash",danger:!0}]}});this.addChild(t)}async onActionEditMembership(){await a.Dialog.showModelForm({title:"Edit Membership",model:this.model,formConfig:i.MemberForms.edit})&&this.render()}async onActionViewUser(){this.model.get("user.id")}async onActionViewGroup(){this.model.get("group.id")}async onActionRemoveMember(){await a.Dialog.confirm(`Are you sure you want to remove ${this.model.get("user.display_name")} from ${this.model.get("group.name")}?`,"Confirm Removal")&&(await this.model.destroy()).success&&this.emit("member:removed",{model:this.model})}}i.Member.VIEW_CLASS=MemberView;class MemberTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_members",pageName:"Manage Members",router:"admin/members",Collection:i.MemberList,formEdit:i.MemberForms.edit,itemViewClass:MemberView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"id",label:"ID",width:"60px",sortable:!0,class:"text-muted"},{key:"user.display_name",label:"User",formatter:"default('Unknown User')"},{key:"user.email",label:"Email",formatter:"default('No Email')"},{key:"group.name",label:"Group",formatter:"default('Unknown Group')"},{key:"role",label:"Role",formatter:"badge"},{key:"status",label:"Status",formatter:"badge"},{key:"created",label:"Added",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No members found. Click "Add Member" to add users to groups.',batchBarLocation:"top",batchActions:[{label:"Remove",icon:"bi bi-person-dash",action:"batch-remove"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Change Role",icon:"bi bi-person-gear",action:"batch-role"},{label:"Activate",icon:"bi bi-check-circle",action:"batch-activate"},{label:"Deactivate",icon:"bi bi-x-circle",action:"batch-deactivate"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class MetricsPermissionsView extends t.View{constructor(e={}){super({className:"metrics-permissions-view",...e}),this.model=e.model||new i.MetricsPermission(e.data||{}),this.template='\n <div class="container p-3">\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div>\n <h3 class="mb-1">Permissions for {{model.account}}</h3>\n </div>\n <div data-container="context-menu"></div>\n </div>\n <div data-container="data-view"></div>\n </div>\n '}async onInit(){this.dataView=new n.default({containerId:"data-view",model:this.model,fields:[{name:"view_permissions",label:"View Permissions",format:"list|badge"},{name:"write_permissions",label:"Write Permissions",format:"list|badge"}]}),this.addChild(this.dataView);const t=new e.ContextMenu({containerId:"context-menu",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit",action:"edit",icon:"bi-pencil"},{label:"Delete",action:"delete",icon:"bi-trash",danger:!0}]}});this.addChild(t)}async onActionEdit(){const e=await a.Dialog.showModelForm({title:`Edit Permissions for ${this.model.get("account")}`,model:this.model,formConfig:i.MetricsForms.edit});e&&(this.model.set(e.data.data),this.render())}async onActionDelete(){await a.Dialog.confirm(`Are you sure you want to delete all permissions for ${this.model.get("account")}?`)&&(await this.model.destroy(),this.emit("deleted",this.model))}}i.MetricsPermission.VIEW_CLASS=MetricsPermissionsView;class MetricsPermissionsTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_metrics_permissions",pageName:"Metrics Permissions",router:"admin/metrics/permissions",Collection:i.MetricsPermissionList,formEdit:i.MetricsForms.edit,itemViewClass:MetricsPermissionsView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"account",label:"Account",sortable:!0},{key:"view_permissions",label:"View Permissions",formatter:"list|badge"},{key:"write_permissions",label:"Write Permissions",formatter:"list|badge"}],selectable:!0,searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No metrics permissions found.",emptyIcon:"bi-bar-chart-line",actions:["view","edit","delete"]}})}}class PushConfigTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_configs",pageName:"Push Configurations",router:"admin/push/configs",Collection:i.PushConfigList,formCreate:i.PushConfigForms.create,formEdit:i.PushConfigForms.edit,columns:[{key:"id",label:"ID",width:"70px"},{key:"name",label:"Name"},{key:"group.name",label:"Group",formatter:"default('Default')"},{key:"fcm_sender_id",label:"Project ID"},{key:"is_active",label:"Active",format:"boolean"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,actions:["edit","delete"],tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No push configurations found.",emptyIcon:"bi-gear"}})}}class PushDashboardPage extends e.Page{constructor(e={}){super({...e,title:"Push Notifications Dashboard",className:"push-dashboard-page"})}async getTemplate(){return'\n <div class="container-fluid">\n <h1 class="h3 mb-4">Push Notifications</h1>\n <div class="row">\n \x3c!-- Stat cards --\x3e\n </div>\n <div class="row">\n <div class="col-xl-8 col-lg-7">\n <div class="card shadow mb-4">\n <div class="card-header">Notifications Over Time</div>\n <div class="card-body" data-container="deliveries-chart"></div>\n </div>\n </div>\n <div class="col-xl-4 col-lg-5">\n <div class="card shadow mb-4">\n <div class="card-header">Delivery Status</div>\n <div class="card-body" data-container="status-chart"></div>\n </div>\n </div>\n </div>\n <div class="row">\n <div class="col-lg-6 mb-4" data-container="recent-deliveries"></div>\n <div class="col-lg-6 mb-4" data-container="failed-deliveries"></div>\n </div>\n </div>\n '}async onInit(){this.deliveriesChart=new s.MetricsChart({containerId:"deliveries-chart",endpoint:"/api/metrics/fetch",slugs:["push_sent","push_failed"],chartType:"line"}),this.addChild(this.deliveriesChart),this.statusChart=new s.PieChart({containerId:"status-chart",endpoint:"/api/account/devices/push/stats"}),this.addChild(this.statusChart),this.recentDeliveries=new i.TableView({containerId:"recent-deliveries",title:"Recent Deliveries",Collection:new i.PushDeliveryList({params:{_sort:"-created",_limit:5}}),columns:[{key:"title",label:"Title"},{key:"status",label:"Status",formatter:"badge"}]}),this.addChild(this.recentDeliveries),this.failedDeliveries=new i.TableView({containerId:"failed-deliveries",title:"Failed Deliveries",Collection:new i.PushDeliveryList({params:{status:"failed",_sort:"-created",_limit:5}}),columns:[{key:"title",label:"Title"},{key:"error_message",label:"Error"}]}),this.addChild(this.failedDeliveries)}}class PushDeliveryView extends t.View{constructor(e={}){super({className:"push-delivery-view",...e}),this.model=e.model}getTemplate(){return'\n <div class="p-3">\n <div class="phone-mockup">\n <div class="phone-screen">\n <div class="notification">\n <div class="notification-header">\n <i class="bi bi-app-indicator"></i>\n <strong>Your App</strong>\n <span class="ms-auto small text-muted">now</span>\n </div>\n <div class="notification-body">\n <div class="fw-bold">{{model.title}}</div>\n <div>{{model.body}}</div>\n </div>\n </div>\n </div>\n </div>\n <div class="mt-3">\n <h5>Delivery Details</h5>\n <p><strong>Status:</strong> <span class="badge {{model.status|badge}}">{{model.status}}</span></p>\n <p><strong>Error:</strong> {{model.error_message|default(\'None\')}}</p>\n </div>\n </div>\n '}}class PushDeliveryTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_deliveries",pageName:"Push Deliveries",router:"admin/push/deliveries",Collection:i.PushDeliveryList,itemViewClass:PushDeliveryView,viewDialogOptions:{header:!1,size:"md"},columns:[{key:"id",label:"ID",width:"70px"},{key:"created",label:"Timestamp",formatter:"datetime"},{key:"user.display_name",label:"User"},{key:"device.device_name",label:"Device"},{key:"title",label:"Title"},{key:"category",label:"Category"},{key:"status",label:"Status",formatter:"badge"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No deliveries found.",emptyIcon:"bi-send",actions:["view"]}})}}class PushDeviceView extends t.View{constructor(e={}){super({className:"push-device-view",...e}),this.model=e.model}getTemplate(){return'\n <div class="p-3">\n <h3>{{model.device_name}}</h3>\n <p class="text-muted">{{model.user.display_name}}</p>\n <div data-container="data-view"></div>\n </div>\n '}onInit(){this.dataView=new n.default({containerId:"data-view",model:this.model,fields:[{name:"platform",label:"Platform",format:"badge"},{name:"push_enabled",label:"Push Enabled",format:"boolean"},{name:"app_version",label:"App Version"},{name:"os_version",label:"OS Version"},{name:"last_seen",label:"Last Seen",format:"datetime"},{name:"push_preferences",label:"Preferences",format:"json"}]}),this.addChild(this.dataView)}}class PushDeviceTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_devices",pageName:"Registered Devices",router:"admin/push/devices",Collection:i.PushDeviceList,itemViewClass:PushDeviceView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"id",label:"ID",width:"70px"},{key:"user.display_name",label:"User"},{key:"device_name",label:"Device Name"},{key:"platform",label:"Platform",formatter:"badge"},{key:"app_version",label:"App Version"},{key:"push_enabled",label:"Push Enabled",format:"boolean"},{key:"last_seen",label:"Last Seen",formatter:"datetime"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No devices found.",emptyIcon:"bi-phone",actions:["view","delete"]}})}}class PushTemplateTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_templates",pageName:"Push Templates",router:"admin/push/templates",Collection:i.PushTemplateList,formCreate:i.PushTemplateForms.create,formEdit:i.PushTemplateForms.edit,columns:[{key:"id",label:"ID",width:"70px"},{key:"name",label:"Name"},{key:"category",label:"Category"},{key:"group.name",label:"Group",formatter:"default('Default')"},{key:"priority",label:"Priority"},{key:"is_active",label:"Active",format:"boolean"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No push templates found.",emptyIcon:"bi-file-earmark-text",actions:["edit","delete"]}})}}class RuleSetView extends t.View{constructor(e={}){super({className:"ruleset-view",...e}),this.model=e.model||new i.RuleSet(e.data||{}),this.template='\n <div class="ruleset-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary"><i class="bi bi-gear-wide-connected"></i></div>\n <div>\n <h3 class="mb-1">{{model.name}}</h3>\n <div class="text-muted small">Category: {{model.category}} | Priority: {{model.priority}}</div>\n </div>\n </div>\n <div data-container="ruleset-context-menu"></div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="ruleset-tabs"></div>\n </div>\n '}async onInit(){const t=this.model.get("match_by"),a=i.MatchByOptions.find(e=>e.value===t),s=a?a.label:String(t),l=this.model.get("bundle_by"),o=i.BundleByOptions.find(e=>e.value===l),r=o?o.label:String(l);this.configView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"RuleSet ID",cols:6},{name:"priority",label:"Priority",cols:6},{name:"name",label:"Name",cols:12},{name:"category",label:"Category",formatter:"badge",cols:6},{name:"is_active",label:"Status",formatter:"boolean",cols:6},{name:"match_by",label:"Match Logic",template:s,cols:12},{name:"bundle_by",label:"Bundle By",template:r,cols:12},{name:"bundle_minutes",label:"Bundle Minutes",cols:6},{name:"handler",label:"Handler",cols:12}]});const d=new i.RuleList({params:{parent:this.model.get("id")}});this.rulesView=new i.TableView({collection:d,hideActivePillNames:["parent"],columns:[{key:"id",label:"ID",width:"70px"},{key:"name",label:"Name"},{key:"field_name",label:"Field"},{key:"comparator",label:"Comparator",width:"120px"},{key:"value",label:"Value"},{key:"value_type",label:"Type",width:"100px"}],showAdd:!0,clickAction:"edit",actions:["edit","delete"],contextMenu:[{label:"Edit Rule",action:"edit",icon:"bi-pencil"},{label:"Duplicate Rule",action:"duplicate",icon:"bi-files"},{divider:!0},{label:"Delete Rule",action:"delete",icon:"bi-trash",danger:!0}],addFormDefaults:{parent:this.model.get("id")}}),this.tabView=new i.TabView({containerId:"ruleset-tabs",tabs:{Configuration:this.configView,Rules:this.rulesView},activeTab:"Configuration"}),this.addChild(this.tabView);const c=new e.ContextMenu({containerId:"ruleset-context-menu",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit RuleSet",action:"edit-ruleset",icon:"bi-pencil"},{label:"Disable",action:"disable-ruleset",icon:"bi-toggle-off"},{type:"divider"},{label:"Delete RuleSet",action:"delete-ruleset",icon:"bi-trash",danger:!0}]}});this.addChild(c)}async onActionEditRuleset(){await a.Dialog.showModelForm({title:`Edit RuleSet - ${this.model.get("name")}`,model:this.model,formConfig:i.RuleSet.EDIT_FORM})&&await this.render()}async onActionDisableRuleset(){const e=!this.model.get("is_active");try{this.model.set("is_active",e),await this.model.save(),await this.render(),a.Dialog.showToast({message:`RuleSet ${e?"enabled":"disabled"} successfully`,type:"success"})}catch(t){a.Dialog.showToast({message:`Failed to update RuleSet: ${t.message}`,type:"error"})}}async onActionDeleteRuleset(){if(await a.Dialog.confirm({title:"Delete RuleSet",message:`Are you sure you want to delete the ruleset "${this.model.get("name")}"? This action cannot be undone.`,confirmText:"Delete",confirmClass:"btn-danger"}))try{await this.model.destroy(),a.Dialog.showToast({message:"RuleSet deleted successfully",type:"success"});const e=this.element?.closest(".modal");if(e){const t=bootstrap.Modal.getInstance(e);t&&t.hide()}this.emit("ruleset:deleted",{model:this.model})}catch(e){a.Dialog.showToast({message:`Failed to delete RuleSet: ${e.message}`,type:"error"})}}}RuleSetView.VIEW_CLASS=RuleSetView;class RuleSetTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_rulesets",pageName:"Rule Engine",router:"admin/rulesets",Collection:i.RuleSetList,itemView:RuleSetView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"name",label:"Name",sortable:!0},{key:"category",label:"Category",sortable:!0,formatter:"badge"},{key:"priority",label:"Priority",sortable:!0},{key:"match_by",label:"Match Logic",formatter:e=>0===e?"ALL":"ANY"}],selectable:!0,searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No rule sets found.",emptyIcon:"bi-gear",actions:["view","edit","delete"]}})}}class S3BucketTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_s3_buckets",pageName:"Manage S3 Buckets",router:"admin/s3-buckets",Collection:i.S3BucketList,formCreate:i.S3BucketForms.create,formEdit:i.S3BucketForms.edit,columns:[{key:"id",label:"ID",width:"60px",sortable:!0,class:"text-muted"},{key:"name",label:"Bucket Name",sortable:!0},{key:"created",label:"Created",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No S3 buckets found. Click "Add S3 Bucket" to create your first bucket.',batchBarLocation:"top",batchActions:[{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Make Public",icon:"bi bi-unlock",action:"batch-public"},{label:"Make Private",icon:"bi bi-lock",action:"batch-private"},{label:"Empty Bucket",icon:"bi bi-bucket",action:"batch-empty"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class EmailView extends t.View{constructor(e={}){super({className:"email-view",...e}),this.model=e.model||new i.SentMessage(e.data||{}),this.hasHtml=!!this.model.get("body_html"),this.hasText=!!this.model.get("body_text"),this.hasContext=this.model.get("template_context")&&Object.keys(this.model.get("template_context")).length>0,this.template='\n <div class="email-view-container p-3">\n \x3c!-- Email Header --\x3e\n <div class="email-header border-bottom pb-3 mb-3">\n <h4 class="mb-2">{{model.subject}}</h4>\n <div class="d-flex justify-content-between align-items-center">\n <div class="d-flex align-items-center">\n <div class="flex-shrink-0">\n <i class="bi bi-person-circle fs-2 text-secondary"></i>\n </div>\n <div class="ms-3">\n <div class="fw-bold">{{model.mailbox.email}}</div>\n <div class="text-muted small">To: {{model.to_addresses|list}}</div>\n </div>\n </div>\n <div class="text-end">\n <div class="text-muted small">{{model.created|datetime}}</div>\n <span class="badge {{model.status|badge}} mt-1">{{model.status|capitalize}}</span>\n </div>\n </div>\n </div>\n\n \x3c!-- Email Body Tabs --\x3e\n <div data-container="email-tabs"></div>\n </div>\n '}async onInit(){const e={};this.hasHtml&&(e.HTML=new t.View({model:this.model,template:'<div class="email-html-content border rounded p-3" style="height: 500px; overflow-y: auto;">{{{model.body_html}}}</div>'})),this.hasText&&(e.Text=new t.View({model:this.model,template:'<pre class="email-text-content p-3 bg-light border rounded" style="white-space: pre-wrap; word-wrap: break-word;">{{model.body_text}}</pre>'})),this.hasContext&&(e.Context=new t.View({model:this.model,template:'<pre class="email-context-content p-3 bg-light border rounded"><code>{{model.template_context|json}}</code></pre>'})),this.tabView=new i.TabView({containerId:"email-tabs",tabs:e,activeTab:this.hasHtml?"HTML":this.hasText?"Text":"Context"}),this.addChild(this.tabView)}}i.SentMessage.VIEW_CLASS=EmailView;class SentMessageTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_sent",pageName:"Sent Messages",router:"admin/email/sent",Collection:i.SentMessageList,itemViewClass:EmailView,viewDialogOptions:{header:!1,size:"xl",scrollable:!0},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"mailbox.email",label:"From",sortable:!0},{key:"to_addresses",label:"To",sortable:!1,formatter:"list"},{key:"subject",label:"Subject",sortable:!0},{key:"status",label:"Status",formatter:"badge"},{key:"status_reason",label:"Reason",formatter:"truncate(80)|default('—')"},{key:"created",label:"Created",formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No sent messages found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class TaskDetailsView extends t.View{constructor(e={}){super({...e,className:"mojo-task-details-view"}),this.task=e.task||null,this.logs=[],this.metrics=null}async getTemplate(){return'\n <div class="mojo-task-details-container">\n {{#task}}\n \x3c!-- Task Overview --\x3e\n <div class="card border-0 bg-light mb-3">\n <div class="card-body">\n <div class="row">\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Task ID</label>\n <div class="font-monospace">{{id}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Function</label>\n <div>{{function}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Channel</label>\n <div>\n <span class="badge bg-primary">{{channel}}</span>\n </div>\n </div>\n </div>\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Status</label>\n <div>\n <span class="badge {{statusBadgeClass}} fs-6">\n <i class="bi {{statusIcon}}"></i> {{status|uppercase}}\n </span>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Created</label>\n <div>{{created|datetime}}</div>\n </div>\n {{#completed_at}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Completed</label>\n <div>{{completed_at|datetime}}</div>\n </div>\n {{/completed_at}}\n {{#expires}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Expires</label>\n <div class="{{expiresClass}}">{{expires|datetime}}</div>\n </div>\n {{/expires}}\n </div>\n </div>\n </div>\n </div>\n\n \x3c!-- Task Data --\x3e\n {{#data}}\n <div class="card mb-3">\n <div class="card-header py-2">\n <h6 class="mb-0">\n <i class="bi bi-database me-2"></i>Task Data\n </h6>\n </div>\n <div class="card-body">\n <pre class="bg-light p-3 rounded mb-0"><code>{{dataFormatted}}</code></pre>\n </div>\n </div>\n {{/data}}\n\n \x3c!-- Error Information --\x3e\n {{#error}}\n <div class="card border-danger mb-3">\n <div class="card-header bg-danger-subtle py-2">\n <h6 class="mb-0 text-danger">\n <i class="bi bi-exclamation-triangle me-2"></i>Error Details\n </h6>\n </div>\n <div class="card-body">\n <div class="alert alert-danger mb-0">\n <strong>Error:</strong> {{error}}\n </div>\n {{#errorDetails}}\n <div class="mt-3">\n <label class="form-label fw-bold small">Stack Trace:</label>\n <pre class="bg-light p-3 rounded small mb-0"><code>{{errorDetails}}</code></pre>\n </div>\n {{/errorDetails}}\n </div>\n </div>\n {{/error}}\n\n \x3c!-- Performance Metrics --\x3e\n {{#metrics}}\n <div class="card mb-3">\n <div class="card-header py-2">\n <h6 class="mb-0">\n <i class="bi bi-speedometer2 me-2"></i>Performance Metrics\n </h6>\n </div>\n <div class="card-body">\n <div class="row">\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-primary">{{executionTime}}ms</div>\n <small class="text-muted">Execution Time</small>\n </div>\n </div>\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-info">{{memoryUsage}}MB</div>\n <small class="text-muted">Memory Usage</small>\n </div>\n </div>\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-warning">{{cpuUsage}}%</div>\n <small class="text-muted">CPU Usage</small>\n </div>\n </div>\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-secondary">{{retryCount}}</div>\n <small class="text-muted">Retry Count</small>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/metrics}}\n\n \x3c!-- Task Logs --\x3e\n <div class="card">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-journal-text me-2"></i>Task Logs\n </h6>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-logs">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body p-0">\n <div class="mojo-task-logs-container" style="max-height: 300px; overflow-y: auto;">\n {{#logs.length}}\n {{#logs}}\n <div class="log-entry p-3 border-bottom">\n <div class="d-flex justify-content-between align-items-start">\n <div class="log-message flex-grow-1">\n <span class="badge bg-{{levelClass}} me-2">{{level|uppercase}}</span>\n {{message}}\n </div>\n <small class="text-muted ms-3">{{timestamp|time}}</small>\n </div>\n </div>\n {{/logs}}\n {{/logs.length}}\n {{^logs.length}}\n <div class="text-center text-muted py-5">\n <i class="bi bi-journal fs-1 opacity-50"></i>\n <p class="mb-0 mt-2">No logs available for this task</p>\n </div>\n {{/logs.length}}\n </div>\n </div>\n </div>\n {{/task}}\n\n {{^task}}\n <div class="alert alert-warning">\n <i class="bi bi-exclamation-triangle me-2"></i>\n No task data available\n </div>\n {{/task}}\n </div>\n '}async onInit(){this.task&&(await this.prepareTaskData(),await this.loadTaskLogs(),await this.loadTaskMetrics())}async prepareTaskData(){this.task&&(this.task.statusBadgeClass=this.getStatusBadgeClass(this.task.status),this.task.statusIcon=this.getStatusIcon(this.task.status),this.task.data&&"object"==typeof this.task.data&&(this.task.dataFormatted=JSON.stringify(this.task.data,null,2)),this.task.expires&&(this.task.expiresClass=1e3*this.task.expires<Date.now()?"text-danger":"text-muted"))}getStatusBadgeClass(e){return{pending:"bg-primary",running:"bg-success",completed:"bg-info",error:"bg-danger",cancelled:"bg-secondary",expired:"bg-warning"}[e]||"bg-secondary"}getStatusIcon(e){return{pending:"bi-hourglass",running:"bi-arrow-repeat",completed:"bi-check-circle",error:"bi-x-octagon",cancelled:"bi-x-circle",expired:"bi-clock"}[e]||"bi-question-circle"}getLogLevelClass(e){return{debug:"secondary",info:"primary",warning:"warning",error:"danger"}[e]||"secondary"}async loadTaskLogs(){if(this.task?.id)try{const e=await this.getApp().rest.GET(`/api/tasks/${this.task.id}/logs`);e.success&&e.data.status?this.logs=e.data.data.map(e=>({...e,levelClass:this.getLogLevelClass(e.level)})):this.logs=[]}catch(e){console.error("Failed to load task logs:",e),this.logs=[]}}async loadTaskMetrics(){if(this.task?.id)try{const e=await this.getApp().rest.GET(`/api/tasks/${this.task.id}/metrics`);e.success&&e.data.status&&(this.metrics=e.data.data)}catch(e){console.error("Failed to load task metrics:",e),this.metrics=null}}async setTask(e){this.task=e,await this.prepareTaskData(),await this.loadTaskLogs(),await this.loadTaskMetrics(),this.isMounted()&&await this.render()}async onActionRefreshLogs(e,t,a){if(this.task?.id)try{a.disabled=!0;const e=a.querySelector("i");e&&e.classList.add("spinning"),await this.loadTaskLogs(),await this.render()}catch(s){console.error("Failed to refresh logs:",s),this.showError("Failed to refresh logs: "+s.message)}finally{a.disabled=!1;const e=a.querySelector("i");e&&e.classList.remove("spinning")}}static async show(e,t={}){const a=new TaskDetailsView({task:e});await a.onInit();const s=[];return["pending","running"].includes(e.status)&&s.push({text:"Cancel Task",class:"btn-outline-danger",action:async()=>{if(confirm("Are you sure you want to cancel this task?"))try{const t=await a.getApp().rest.POST(`/api/tasks/${e.id}/cancel`);if(t.success&&t.data.status)return a.showSuccess("Task cancelled successfully"),{action:"cancelled",task:e};a.showError(t.data.error||"Failed to cancel task")}catch(t){a.showError("Failed to cancel task: "+t.message)}return null}}),"error"===e.status&&s.push({text:"Retry Task",class:"btn-outline-primary",action:async()=>{try{const t=await a.getApp().rest.POST(`/api/tasks/${e.id}/retry`);if(t.success&&t.data.status)return a.showSuccess("Task queued for retry"),{action:"retried",task:e};a.showError(t.data.error||"Failed to retry task")}catch(t){a.showError("Failed to retry task: "+t.message)}return null}}),s.push({text:"Clone Task",class:"btn-outline-info",action:async()=>{try{const t=await a.getApp().rest.POST(`/api/tasks/${e.id}/clone`);if(t.success&&t.data.status)return a.showSuccess("Task cloned successfully"),{action:"cloned",originalTask:e,newTask:t.data.data};a.showError(t.data.error||"Failed to clone task")}catch(t){a.showError("Failed to clone task: "+t.message)}return null}}),s.push({text:"Export",class:"btn-outline-secondary",action:()=>{try{const t={task:e,logs:a.logs,metrics:a.metrics,exported_at:/* @__PURE__ */(new Date).toISOString(),exported_by:"task-management-system"},s=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),i=URL.createObjectURL(s),n=document.createElement("a");return n.href=i,n.download=`task-${e.id}-${Date.now()}.json`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(i),a.showSuccess("Task data exported successfully"),null}catch(t){return a.showError("Failed to export task data"),null}}}),s.push({text:"Close",class:"btn-secondary",dismiss:!0}),await Dialog.showDialog({title:`<i class="bi bi-info-circle me-2"></i>Task Details - ${e.id}`,body:a,size:"lg",scrollable:!0,buttons:s,...t})}}class RunnerDetailsView extends t.View{constructor(e={}){super({...e,className:"mojo-runner-details-view"}),this.runner=e.runner||null,this.currentTasks=[],this.logs=[],this.metrics=null,this.config=null}async getTemplate(){return'\n <div class="mojo-runner-details-container">\n {{#runner}}\n \x3c!-- Runner Overview --\x3e\n <div class="card border-0 bg-light mb-3">\n <div class="card-body">\n <div class="row">\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Hostname</label>\n <div class="h5 mb-0 font-monospace">{{hostname}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Status</label>\n <div>\n <span class="badge {{statusBadgeClass}} fs-6">\n <i class="bi {{statusIcon}}"></i> {{status|uppercase}}\n </span>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Workers</label>\n <div class="h5 mb-0">\n {{#metrics.activeWorkers}}{{metrics.activeWorkers}}/{{/metrics.activeWorkers}}{{max_workers}} workers\n {{#metrics.workerUtilization}}\n <div class="progress mt-1" style="height: 6px;">\n <div class="progress-bar {{metrics.utilizationClass}}" style="width: {{metrics.workerUtilization}}%"></div>\n </div>\n {{/metrics.workerUtilization}}\n </div>\n </div>\n </div>\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Started</label>\n <div>{{started_at|datetime}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Last Ping</label>\n <div class="{{pingAgeClass}}">{{last_ping|datetime}} ({{pingAgeText}})</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Uptime</label>\n <div>{{uptimeText}}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n \x3c!-- Channel Assignment --\x3e\n <div class="card mb-3">\n <div class="card-header py-2">\n <h6 class="mb-0">\n <i class="bi bi-collection me-2"></i>Assigned Channels\n </h6>\n </div>\n <div class="card-body">\n {{#channels.length}}\n <div class="d-flex flex-wrap gap-2">\n {{#channels}}\n <span class="badge bg-primary-subtle text-primary px-3 py-2">\n {{.}}\n </span>\n {{/channels}}\n </div>\n {{/channels.length}}\n {{^channels.length}}\n <div class="text-center text-muted py-3">\n <i class="bi bi-collection opacity-50"></i>\n <p class="mb-0 mt-2">No channels assigned</p>\n </div>\n {{/channels.length}}\n </div>\n </div>\n\n \x3c!-- Performance Metrics --\x3e\n {{#metrics}}\n <div class="card mb-3">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-speedometer2 me-2"></i>Performance Metrics\n </h6>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-metrics">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body">\n <div class="row">\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-success">{{tasksCompleted|number}}</div>\n <small class="text-muted">Tasks Completed</small>\n {{#tasksCompletedToday}}\n <div class="small text-success mt-1">+{{tasksCompletedToday}} today</div>\n {{/tasksCompletedToday}}\n </div>\n </div>\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-info">{{avgExecutionTime}}ms</div>\n <small class="text-muted">Avg Execution</small>\n <div class="small {{performanceTrend.class}} mt-1">\n <i class="bi {{performanceTrend.icon}}"></i> {{performanceTrend.text}}\n </div>\n </div>\n </div>\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-danger">{{errorCount|number}}</div>\n <small class="text-muted">Errors</small>\n <div class="small text-muted mt-1">{{errorRate}}% error rate</div>\n </div>\n </div>\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-warning">{{queueBacklog|number}}</div>\n <small class="text-muted">Queue Backlog</small>\n </div>\n </div>\n </div>\n\n \x3c!-- Resource Usage Bars --\x3e\n <div class="row mt-3">\n <div class="col-md-6 mb-2">\n <label class="form-label fw-bold small">CPU Usage</label>\n <div class="progress" style="height: 20px;">\n <div class="progress-bar {{cpu.class}}" style="width: {{cpu.percentage}}%">\n {{cpu.percentage}}%\n </div>\n </div>\n </div>\n <div class="col-md-6 mb-2">\n <label class="form-label fw-bold small">Memory Usage</label>\n <div class="progress" style="height: 20px;">\n <div class="progress-bar {{memory.class}}" style="width: {{memory.percentage}}%">\n {{memory.used}}MB / {{memory.total}}MB\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/metrics}}\n\n \x3c!-- Current Tasks --\x3e\n <div class="card mb-3">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-list-task me-2"></i>Current Tasks ({{currentTasks.length}})\n </h6>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-tasks">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body p-0">\n {{#currentTasks.length}}\n <div class="table-responsive">\n <table class="table table-sm table-hover mb-0">\n <thead class="table-light">\n <tr>\n <th class="border-0">Task ID</th>\n <th class="border-0">Function</th>\n <th class="border-0">Channel</th>\n <th class="border-0">Started</th>\n <th class="border-0">Duration</th>\n <th class="border-0 text-end">Actions</th>\n </tr>\n </thead>\n <tbody>\n {{#currentTasks}}\n <tr>\n <td class="font-monospace small">{{id|truncate(12)}}</td>\n <td>{{function}}</td>\n <td><span class="badge bg-primary-subtle text-primary">{{channel}}</span></td>\n <td>{{started|time}}</td>\n <td class="text-muted">{{duration}}</td>\n <td class="text-end">\n <div class="btn-group btn-group-sm">\n <button class="btn btn-outline-info" data-action="view-task" data-task-id="{{id}}" title="View Details">\n <i class="bi bi-eye"></i>\n </button>\n <button class="btn btn-outline-warning" data-action="cancel-task" data-task-id="{{id}}" title="Cancel">\n <i class="bi bi-x-circle"></i>\n </button>\n </div>\n </td>\n </tr>\n {{/currentTasks}}\n </tbody>\n </table>\n </div>\n {{/currentTasks.length}}\n {{^currentTasks.length}}\n <div class="text-center text-muted py-5">\n <i class="bi bi-list-task fs-1 opacity-50"></i>\n <p class="mb-0 mt-2">No active tasks</p>\n </div>\n {{/currentTasks.length}}\n </div>\n </div>\n\n \x3c!-- Runner Logs --\x3e\n <div class="card">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-journal-text me-2"></i>Runner Logs\n </h6>\n <div class="btn-group btn-group-sm">\n <button class="btn btn-outline-secondary active" data-action="filter-logs" data-level="all">All</button>\n <button class="btn btn-outline-primary" data-action="filter-logs" data-level="info">Info</button>\n <button class="btn btn-outline-warning" data-action="filter-logs" data-level="warning">Warning</button>\n <button class="btn btn-outline-danger" data-action="filter-logs" data-level="error">Error</button>\n <button class="btn btn-outline-primary" data-action="refresh-logs">\n <i class="bi bi-arrow-clockwise"></i>\n </button>\n </div>\n </div>\n <div class="card-body p-0">\n <div class="mojo-runner-logs-container" style="max-height: 300px; overflow-y: auto;">\n {{#logs.length}}\n {{#logs}}\n <div class="log-entry p-3 border-bottom" data-level="{{level}}">\n <div class="d-flex justify-content-between align-items-start">\n <div class="log-message flex-grow-1">\n <span class="badge bg-{{levelClass}} me-2">{{level|uppercase}}</span>\n {{message}}\n </div>\n <small class="text-muted ms-3">{{timestamp|time}}</small>\n </div>\n </div>\n {{/logs}}\n {{/logs.length}}\n {{^logs.length}}\n <div class="text-center text-muted py-5">\n <i class="bi bi-journal fs-1 opacity-50"></i>\n <p class="mb-0 mt-2">No logs available</p>\n </div>\n {{/logs.length}}\n </div>\n </div>\n </div>\n {{/runner}}\n\n {{^runner}}\n <div class="alert alert-warning">\n <i class="bi bi-exclamation-triangle me-2"></i>\n No runner data available\n </div>\n {{/runner}}\n </div>\n '}async onInit(){this.runner&&(await this.prepareRunnerData(),await this.loadRunnerMetrics(),await this.loadCurrentTasks(),await this.loadRunnerLogs())}async prepareRunnerData(){if(this.runner&&(this.runner.isActive="active"===this.runner.status,this.runner.statusBadgeClass=this.runner.isActive?"bg-success":"bg-warning",this.runner.statusIcon=this.runner.isActive?"bi-check-circle-fill":"bi-exclamation-triangle-fill",void 0!==this.runner.ping_age&&(this.runner.pingAgeText=this.formatDuration(this.runner.ping_age),this.runner.pingAgeClass=this.runner.ping_age>300?"text-danger":"text-muted"),this.runner.started_at)){const e=Date.now()/1e3-this.runner.started_at;this.runner.uptimeText=this.formatUptime(e)}}async loadRunnerMetrics(){if(this.runner?.hostname)try{const e=await this.getApp().rest.GET(`/api/runners/${this.runner.hostname}/metrics`);if(e.success&&e.data.status){const t=e.data.data;this.metrics={activeWorkers:t.activeWorkers||0,tasksCompleted:t.tasksCompleted||0,tasksCompletedToday:t.tasksCompletedToday||0,avgExecutionTime:t.avgExecutionTime||0,errorCount:t.errorCount||0,errorRate:t.errorRate||0,queueBacklog:t.queueBacklog||0,workerUtilization:Math.round(t.activeWorkers/this.runner.max_workers*100),utilizationClass:this.getUtilizationClass(t.activeWorkers/this.runner.max_workers),performanceTrend:this.getPerformanceTrend(t.avgExecutionTime||0),cpu:this.getResourceStatus(t.cpuUsage||0),memory:this.getMemoryStatus(t.memoryUsed||0,t.memoryTotal||1e3)}}}catch(e){console.error("Failed to load runner metrics:",e),this.metrics=this.getDefaultMetrics()}}async loadCurrentTasks(){if(this.runner?.hostname)try{const e=await this.getApp().rest.GET(`/api/runners/${this.runner.hostname}/tasks`);e.success&&e.data.status?this.currentTasks=e.data.data.map(e=>({...e,duration:this.formatDuration(Date.now()/1e3-e.started)})):this.currentTasks=[]}catch(e){console.error("Failed to load current tasks:",e),this.currentTasks=[]}}async loadRunnerLogs(){if(this.runner?.hostname)try{const e=await this.getApp().rest.GET(`/api/runners/${this.runner.hostname}/logs?limit=50`);e.success&&e.data.status?this.logs=e.data.data.map(e=>({...e,levelClass:this.getLogLevelClass(e.level)})):this.logs=[]}catch(e){console.error("Failed to load runner logs:",e),this.logs=[]}}getDefaultMetrics(){return{activeWorkers:0,tasksCompleted:0,tasksCompletedToday:0,avgExecutionTime:0,errorCount:0,errorRate:0,queueBacklog:0,workerUtilization:0,utilizationClass:"bg-secondary",performanceTrend:{class:"text-muted",icon:"bi-dash",text:"No data"},cpu:{percentage:0,class:"bg-secondary"},memory:{used:0,total:0,percentage:0,class:"bg-secondary"}}}getUtilizationClass(e){return e>.9?"bg-danger":e>.7?"bg-warning":e>.5?"bg-info":"bg-success"}getPerformanceTrend(e){return e<1e3?{class:"text-success",icon:"bi-arrow-up",text:"Excellent"}:e<5e3?{class:"text-warning",icon:"bi-arrow-right",text:"Good"}:{class:"text-danger",icon:"bi-arrow-down",text:"Slow"}}getResourceStatus(e){const t=Math.round(e);let a="bg-success";return t>80?a="bg-danger":t>60?a="bg-warning":t>40&&(a="bg-info"),{percentage:t,class:a}}getMemoryStatus(e,t){const a=Math.round(e/t*100);return{used:Math.round(e),total:Math.round(t),percentage:a,class:this.getResourceStatus(a).class}}getLogLevelClass(e){return{debug:"secondary",info:"primary",warning:"warning",error:"danger"}[e]||"secondary"}formatUptime(e){const t=Math.floor(e/86400),a=Math.floor(e%86400/3600),s=Math.floor(e%3600/60);return t>0?`${t}d ${a}h ${s}m`:a>0?`${a}h ${s}m`:`${s}m`}formatDuration(e){return e<60?`${Math.round(e)}s`:e<3600?`${Math.round(e/60)}m`:e<86400?`${Math.round(e/3600)}h`:`${Math.round(e/86400)}d`}async setRunner(e){this.runner=e,await this.prepareRunnerData(),await this.loadRunnerMetrics(),await this.loadCurrentTasks(),await this.loadRunnerLogs(),this.isMounted()&&await this.render()}async onActionRefreshMetrics(e,t,a){await this.loadRunnerMetrics(),await this.render()}async onActionRefreshTasks(e,t,a){await this.loadCurrentTasks(),await this.render()}async onActionRefreshLogs(e,t,a){await this.loadRunnerLogs(),await this.render()}async onActionFilterLogs(e,t,a){const s=a.getAttribute("data-level");this.element.querySelectorAll(".log-entry").forEach(e=>{"all"===s||e.getAttribute("data-level")===s?e.style.display="block":e.style.display="none"}),this.element.querySelectorAll('[data-action="filter-logs"]').forEach(e=>{e.classList.remove("active")}),a.classList.add("active")}async onActionViewTask(e,t,a){const s=a.getAttribute("data-task-id");this.emit("task:view",{taskId:s,runner:this.runner})}async onActionCancelTask(e,t,a){const s=a.getAttribute("data-task-id");if(confirm("Are you sure you want to cancel this task?"))try{const e=await this.getApp().rest.POST(`/api/tasks/${s}/cancel`);e.success&&e.data.status?(this.showSuccess("Task cancelled successfully"),await this.loadCurrentTasks(),await this.render(),this.emit("task:cancelled",{taskId:s,runner:this.runner})):this.showError(e.data.error||"Failed to cancel task")}catch(i){console.error("Failed to cancel task:",i),this.showError("Failed to cancel task: "+i.message)}}static async show(e,t={}){const s=new RunnerDetailsView({runner:e});await s.onInit();const i=[];return"active"===e.status?i.push({text:"Pause Runner",class:"btn-warning",action:async()=>{if(confirm(`Are you sure you want to pause runner "${e.hostname}"?`))try{const t=await s.getApp().rest.POST(`/api/runners/${e.hostname}/pause`);if(t.success&&t.data.status)return s.showSuccess("Runner paused successfully"),{action:"paused",runner:e};s.showError(t.data.error||"Failed to pause runner")}catch(t){s.showError("Failed to pause runner: "+t.message)}return null}}):i.push({text:"Restart Runner",class:"btn-success",action:async()=>{if(confirm(`Are you sure you want to restart runner "${e.hostname}"?`))try{const t=await s.getApp().rest.POST(`/api/runners/${e.hostname}/restart`);if(t.success&&t.data.status)return s.showSuccess("Runner restart initiated"),{action:"restarted",runner:e};s.showError(t.data.error||"Failed to restart runner")}catch(t){s.showError("Failed to restart runner: "+t.message)}return null}}),i.push({text:"Configure",class:"btn-outline-primary",action:()=>(s.emit("runner:configure",{runner:e}),null)}),i.push({text:"Remove Runner",class:"btn-outline-danger",action:async()=>{const t=`Are you sure you want to remove runner "${e.hostname}"? This action cannot be undone.`;if(confirm(t))try{const t=await s.getApp().rest.DELETE(`/api/runners/${e.hostname}`);if(t.success&&t.data.status)return s.showSuccess("Runner removed successfully"),{action:"removed",runner:e};s.showError(t.data.error||"Failed to remove runner")}catch(a){s.showError("Failed to remove runner: "+a.message)}return null}}),i.push({text:"Export",class:"btn-outline-secondary",action:()=>{try{const t={runner:e,metrics:s.metrics,currentTasks:s.currentTasks,logs:s.logs,exported_at:/* @__PURE__ */(new Date).toISOString(),exported_by:"task-management-system"},a=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),i=URL.createObjectURL(a),n=document.createElement("a");return n.href=i,n.download=`runner-${e.hostname}-${Date.now()}.json`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(i),s.showSuccess("Runner data exported successfully"),null}catch(t){return s.showError("Failed to export runner data"),null}}}),i.push({text:"Close",class:"btn-secondary",dismiss:!0}),await a.Dialog.showDialog({title:`<i class="bi bi-cpu me-2"></i>Runner Details - ${e.hostname}`,body:s,size:"xl",scrollable:!0,buttons:i,...t})}}class TaskStatsView extends t.View{constructor(e={}){super({...e,className:"mojo-task-stats-section"}),this.stats={pending:0,running:0,completed:0,errors:0}}async getTemplate(){return'\n <div class="mojo-task-stats-header mb-4">\n <div class="row">\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Pending Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.pending}}</h3>\n <span class="badge bg-primary-subtle text-primary">\n <i class="bi bi-clock"></i> Queued\n </span>\n </div>\n <div class="text-primary">\n <i class="bi bi-hourglass fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Running Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.running}}</h3>\n <span class="badge bg-success-subtle text-success">\n <i class="bi bi-play-circle"></i> Active\n </span>\n </div>\n <div class="text-success">\n <i class="bi bi-arrow-repeat fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Completed Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.completed}}</h3>\n <span class="badge bg-info-subtle text-info">\n <i class="bi bi-check-circle"></i> Done\n </span>\n </div>\n <div class="text-info">\n <i class="bi bi-check-square fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Failed Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.errors}}</h3>\n <span class="badge bg-danger-subtle text-danger">\n <i class="bi bi-exclamation-circle"></i> Errors\n </span>\n </div>\n <div class="text-danger">\n <i class="bi bi-x-octagon fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async loadStats(){try{const e=await this.getApp().rest.GET("/api/tasks/status");e.success&&e.data.status&&(this.stats=e.data.data)}catch(e){console.error("Failed to load task stats:",e)}}async onInit(){await this.loadStats()}}class TaskRunnersView extends t.View{constructor(e={}){super({...e,className:"mojo-task-runners-section"}),this.runners=[]}async getTemplate(){return'\n <div class="card border shadow-sm mb-4">\n <div class="card-header d-flex justify-content-between align-items-center">\n <h5 class="card-title mb-0">\n <i class="bi bi-cpu me-2"></i>Task Runners\n </h5>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-runners">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body">\n {{#runners.length}}\n <div class="mojo-task-runner-list">\n {{#runners}}\n <div class="mojo-task-runner-item p-3 mb-2 bg-light rounded">\n <div class="row align-items-center">\n <div class="col-md-8 col-lg-9">\n <div class="d-flex align-items-center">\n <div class="mojo-task-runner-status me-3">\n <span class="badge {{statusBadge}}">\n <i class="bi {{statusIcon}}"></i> {{status}}\n </span>\n </div>\n <div class="mojo-task-runner-info">\n <div class="mojo-task-runner-name">\n <strong>{{hostname}}</strong>\n {{#max_workers}}<span class="text-muted ms-2">• {{max_workers}} workers</span>{{/max_workers}}\n </div>\n <div class="mojo-task-runner-channels">\n <small class="text-muted">\n {{#channels.length}}Channels: {{#channels}}{{.}}{{^last}}, {{/last}}{{/channels}}{{/channels.length}}\n {{^channels.length}}No channels assigned{{/channels.length}}\n </small>\n </div>\n </div>\n </div>\n </div>\n <div class="col-md-4 col-lg-3">\n <div class="mojo-task-runner-actions d-flex justify-content-end align-items-center">\n <small class="text-muted me-2 d-none d-sm-inline">{{pingAgeText}}</small>\n <div class="dropdown">\n <button class="btn btn-sm btn-outline-secondary dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false">\n <i class="bi bi-three-dots-vertical"></i>\n </button>\n <ul class="dropdown-menu dropdown-menu-end">\n <li><button class="dropdown-item" data-action="view-runner-details" data-runner-id="{{id}}">\n <i class="bi bi-info-circle me-2"></i>View Details\n </button></li>\n {{#isActive}}\n <li><button class="dropdown-item text-warning" data-action="pause-runner" data-runner-id="{{id}}">\n <i class="bi bi-pause me-2"></i>Pause Runner\n </button></li>\n {{/isActive}}\n {{^isActive}}\n <li><button class="dropdown-item text-success" data-action="restart-runner" data-runner-id="{{id}}">\n <i class="bi bi-play me-2"></i>Restart Runner\n </button></li>\n {{/isActive}}\n <li><hr class="dropdown-divider"></li>\n <li><button class="dropdown-item text-danger" data-action="remove-runner" data-runner-id="{{id}}">\n <i class="bi bi-trash me-2"></i>Remove Runner\n </button></li>\n </ul>\n </div>\n </div>\n </div>\n </div>\n <div class="row mt-2 d-sm-none">\n <div class="col-12">\n <small class="text-muted">Last ping: {{pingAgeText}}</small>\n </div>\n </div>\n </div>\n {{/runners}}\n </div>\n {{/runners.length}}\n {{^runners.length}}\n <div class="text-center text-muted py-4">\n <i class="bi bi-cpu fs-1"></i>\n <p class="mt-2">No task runners found</p>\n </div>\n {{/runners.length}}\n </div>\n </div>\n '}async loadRunners(){try{const e=await this.getApp().rest.GET("/api/tasks/runners");e.success&&e.data.status&&(this.runners=e.data.data.map(e=>{const t="active"===e.status,a=e.ping_age||0;return{...e,isActive:t,statusBadge:t?"bg-success":"bg-warning",statusIcon:t?"bi-check-circle-fill":"bi-exclamation-triangle-fill",pingAgeText:this.formatPingAge(a)}}))}catch(e){console.error("Failed to load runners:",e)}}formatPingAge(e){return e<60?`${Math.round(e)}s ago`:e<3600?`${Math.round(e/60)}m ago`:`${Math.round(e/3600)}h ago`}async onInit(){await this.loadRunners()}async onActionRefreshRunners(e,t){await this.loadRunners()}async onActionViewRunnerDetails(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(s){const e=await RunnerDetailsView.show(s);e?.action&&(await this.loadRunners(),this.emit("runner:"+e.action,e))}}async onActionPauseRunner(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(s&&confirm(`Are you sure you want to pause runner "${s.hostname}"?`))try{t.disabled=!0;const e=await this.getApp().rest.POST(`/api/runners/${a}/pause`);e.success&&e.data.status?(this.showSuccess("Runner paused successfully"),await this.loadRunners()):this.showError(e.data.error||"Failed to pause runner")}catch(i){console.error("Failed to pause runner:",i),this.showError("Failed to pause runner: "+i.message)}finally{t.disabled=!1}}async onActionRestartRunner(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(s&&confirm(`Are you sure you want to restart runner "${s.hostname}"?`))try{t.disabled=!0;const e=await this.getApp().rest.POST(`/api/runners/${a}/restart`);e.success&&e.data.status?(this.showSuccess("Runner restart initiated"),await this.loadRunners()):this.showError(e.data.error||"Failed to restart runner")}catch(i){console.error("Failed to restart runner:",i),this.showError("Failed to restart runner: "+i.message)}finally{t.disabled=!1}}async onActionRemoveRunner(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(!s)return;const i=`Are you sure you want to remove runner "${s.hostname}"? This action cannot be undone.`;if(confirm(i))try{t.disabled=!0;const e=await this.getApp().rest.DELETE(`/api/runners/${a}`);e.success&&e.data.status?(this.showSuccess("Runner removed successfully"),await this.loadRunners()):this.showError(e.data.error||"Failed to remove runner")}catch(n){console.error("Failed to remove runner:",n),this.showError("Failed to remove runner: "+n.message)}finally{t.disabled=!1}}}class TaskChartsView extends t.View{constructor(e={}){super({...e,className:"mojo-task-charts-section"})}async getTemplate(){return'\n <div class="row mb-4">\n <div class="col-xl-6 col-lg-12 mb-4">\n <div class="card border shadow-sm">\n <div class="card-body" style="min-height: 300px;">\n <div data-container="task-flow-chart"></div>\n </div>\n </div>\n </div>\n <div class="col-xl-6 col-lg-12 mb-4">\n <div class="card border shadow-sm">\n <div class="card-body" style="min-height: 300px;">\n <div data-container="task-errors-chart"></div>\n </div>\n </div>\n </div>\n </div>\n '}async onInit(){this.taskFlowChart=new s.MetricsChart({title:'<i class="bi bi-graph-up me-2"></i>Task Flow',endpoint:"/api/metrics/fetch",height:280,granularity:"hours",slugs:["tasks_pub","tasks_completed"],account:"global",chartType:"line",showDateRange:!1,colors:["rgba(13, 110, 253, 0.8)","rgba(25, 135, 84, 0.8)"],yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"task-flow-chart"}),this.addChild(this.taskFlowChart),this.taskErrorsChart=new s.MetricsChart({title:'<i class="bi bi-exclamation-triangle me-2"></i>Task Issues',endpoint:"/api/metrics/fetch",height:280,granularity:"hours",slugs:["tasks_errors","tasks_expired"],account:"global",chartType:"line",showDateRange:!1,colors:["rgba(220, 53, 69, 0.8)","rgba(255, 193, 7, 0.8)"],yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"task-errors-chart"}),this.addChild(this.taskErrorsChart)}}class PendingTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Pending Tasks",collection:new l.Collection({endpoint:"/api/tasks/pending"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"status",label:"Status",formatter:"badge"}]})}}class RunningTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Running Tasks",collection:new l.Collection({endpoint:"/api/tasks/running"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"status",label:"Status",formatter:"badge"}]})}}class CompletedTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Completed Tasks",collection:new l.Collection({endpoint:"/api/tasks/completed"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"completed_at",label:"Completed",sortable:!0,formatter:"datetime"},{key:"status",label:"Status",formatter:"badge"}]})}}class ErrorTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Failed Tasks",collection:new l.Collection({endpoint:"/api/tasks/errors"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"error",label:"Error",sortable:!1},{key:"status",label:"Status",formatter:"badge"}]})}}class TaskManagementPage extends e.Page{constructor(e={}){super({...e,title:"Task Management",className:"mojo-task-management-page"}),this.pageTitle="Task Management",this.pageSubtitle="Async task monitoring and runner management",this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString()}async getTemplate(){return'\n <div class="mojo-task-management-container">\n \x3c!-- Page Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div>\n <h1 class="h3 mb-1">{{pageTitle}}</h1>\n <p class="text-muted mb-0">{{pageSubtitle}}</p>\n <small class="text-info">\n <i class="bi bi-cpu me-1"></i>\n Real-time task processing and runner monitoring\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Data">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button type="button" class="btn btn-outline-primary btn-sm"\n data-action="export-tasks" title="Export Task Data">\n <i class="bi bi-download"></i> Export\n </button>\n <button type="button" class="btn btn-outline-success btn-sm"\n data-action="manage-channels" title="Manage Channels">\n <i class="bi bi-collection"></i> Channels\n </button>\n </div>\n </div>\n\n \x3c!-- Task Stats --\x3e\n <div data-container="task-stats"></div>\n\n \x3c!-- Task Runners --\x3e\n <div data-container="task-runners"></div>\n\n \x3c!-- Task Charts --\x3e\n <div data-container="task-charts"></div>\n\n \x3c!-- Task Tables --\x3e\n <div class="card border shadow-sm">\n <div class="card-header">\n <h5 class="card-title mb-0">\n <i class="bi bi-list-task me-2"></i>Task Management\n </h5>\n </div>\n <div class="card-body">\n <div data-container="task-tables"></div>\n </div>\n </div>\n\n \x3c!-- System Status Footer --\x3e\n <div class="row mt-4">\n <div class="col-12">\n <div class="alert alert-info border-0" role="alert">\n <div class="d-flex align-items-center">\n <i class="bi bi-info-circle-fill me-2"></i>\n <div>\n <strong>Task System Status:</strong> Monitoring active.\n Last updated: <span class="text-muted">{{lastUpdated}}</span>\n </div>\n <div class="ms-auto">\n <button class="btn btn-sm btn-outline-info" data-action="view-system-logs">\n <i class="bi bi-journal-text"></i> View Logs\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async onInit(){this.taskStatsView=new TaskStatsView({containerId:"task-stats"}),this.addChild(this.taskStatsView),this.taskRunnersView=new TaskRunnersView({containerId:"task-runners"}),this.addChild(this.taskRunnersView),this.taskChartsView=new TaskChartsView({containerId:"task-charts"}),this.addChild(this.taskChartsView),this.taskTablesView=new i.TabView({containerId:"task-tables",tabs:{Pending:new PendingTasksTable,Running:new RunningTasksTable,Completed:new CompletedTasksTable,Errors:new ErrorTasksTable},activeTab:"Pending"}),this.addChild(this.taskTablesView)}async onActionRefreshAll(e,t,a){try{const e=a.querySelector("i");e?.classList.add("bi-spin"),a.disabled=!0;const t=[this.taskStatsView?.loadStats(),this.taskRunnersView?.loadRunners(),this.taskChartsView?.taskFlowChart?.refresh(),this.taskChartsView?.taskErrorsChart?.refresh()].filter(Boolean),s=this.taskTablesView?.getActiveTab();if(s){const e=this.taskTablesView.getTab(s);e?.refresh&&t.push(e.refresh())}await Promise.allSettled(t),this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString();const i=this.getApp()?.events;i&&i.emit("tasks:dashboard-refreshed",{page:this,timestamp:this.lastUpdated})}catch(s){console.error("Failed to refresh task dashboard:",s)}finally{const e=a.querySelector("i");e?.classList.remove("bi-spin"),a.disabled=!1}}async onActionExportTasks(e,t,a){try{await(this.taskChartsView?.taskFlowChart?.export("png")),await(this.taskChartsView?.taskErrorsChart?.export("png"));const e=this.taskTablesView?.getActiveTab();if(e){const t=this.taskTablesView.getTab(e);t?.exportToCSV&&t.exportToCSV()}}catch(s){console.error("Failed to export task data:",s)}}async onActionManageChannels(e,t){try{const e=await this.getApp().rest.GET("/api/tasks/channels");if(e.success&&e.data.status){const t=e.data.data.map(e=>`${e.name} (${e.pending} pending, ${e.running} running)`).join("\n");alert(`Task Channels:\n\n${t}\n\nFull channel management interface coming soon!`)}else this.showError("Failed to load channel information")}catch(a){console.error("Failed to load channels:",a);const e=this.getApp()?.router;e&&e.navigateTo("/admin/task-channels")}}async onActionViewSystemLogs(e,t){try{const e=await this.getApp().rest.GET("/api/tasks/logs?limit=50");if(e.success&&e.data.status){const t=e.data.data,a=t.slice(0,10).map(e=>`[${new Date(1e3*e.timestamp).toLocaleString()}] ${e.level.toUpperCase()}: ${e.message}`).join("\n");if(confirm(`Recent Task System Logs:\n\n${a}\n\n... and ${t.length-10} more entries.\n\nView full logs?`)){const e=this.getApp()?.router;e&&e.navigateTo("/admin/logs?filter=tasks")}}else{const e=this.getApp()?.router;e&&e.navigateTo("/admin/logs")}}catch(a){console.error("Failed to load system logs:",a);const e=this.getApp()?.router;e&&e.navigateTo("/admin/logs")}}async refreshDashboard(){return this.onActionRefreshAll(null,null,{disabled:!1,querySelector:()=>null})}getStats(){return this.taskStatsView?.stats||{}}getRunners(){return this.taskRunnersView?.runners||[]}getCharts(){return{taskFlow:this.taskChartsView?.taskFlowChart,taskErrors:this.taskChartsView?.taskErrorsChart}}}class TicketNoteAdapter{constructor(e){this.ticketId=e,this.collection=new i.TicketNoteList({params:{parent:this.ticketId,sort:"created",size:100}})}async fetch(){return await this.collection.fetch(),this.collection.models.map(e=>this.transform(e))}transform(e){return{id:e.get("id"),type:"user_comment",author:{id:e.get("user.id"),name:e.get("user.display_name")||"System",avatarUrl:e.get("user.avatar.url")},timestamp:e.get("created"),content:e.get("note"),attachments:e.get("media")?[e.get("media")]:[]}}async addNote(e){const t=new i.TicketNote,a=await t.save({parent:this.ticketId,note:e.text,media:e.files&&e.files.length>0?e.files[0].id:null});return a.success&&await this.collection.fetch(),a}}class TicketView extends t.View{constructor(e={}){super({className:"ticket-view",...e}),this.model=e.model||new i.Ticket(e.data||{}),this.template='\n <div class="ticket-view-container d-flex flex-column h-100">\n \x3c!-- Ticket Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-3 flex-shrink-0">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="avatar-placeholder rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi bi-ticket-perforated text-secondary" style="font-size: 40px;"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.title|truncate(50)|default(\'Untitled Ticket\')}}</h3>\n <div class="text-muted small">\n <span>Ticket #{{model.id}}</span>\n <span class="mx-2">|</span>\n <span>Priority: {{model.priority|capitalize}}</span>\n {{#model.assignee}}\n <span class="mx-2">|</span>\n <span>Assigned to: {{model.assignee.display_name}}</span>\n {{/model.assignee}}\n </div>\n {{#model.incident}}\n <div class="text-muted small mt-1">\n <i class="bi bi-exclamation-triangle"></i> Related to incident: {{model.incident}}\n </div>\n {{/model.incident}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <span class="badge {{model.status|badgeClass}}">{{model.status|capitalize}}</span>\n </div>\n {{#model.created}}\n <div class="text-muted small mt-1">Created {{model.created|relative}}</div>\n {{/model.created}}\n {{#model.modified}}\n <div class="text-muted small">Updated {{model.modified|relative}}</div>\n {{/model.modified}}\n </div>\n <div data-container="ticket-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Chat View (Full height) --\x3e\n <div class="flex-grow-1" style="min-height: 0;" data-container="chat-view"></div>\n </div>\n '}async onInit(){const t=new TicketNoteAdapter(this.model.get("id"));this.chatView=new i.ChatView({containerId:"chat-view",adapter:t,theme:"compact",currentUserId:this.getCurrentUserId(),inputPlaceholder:"Add a note...",inputButtonText:"Add Note"}),this.addChild(this.chatView);const a=new e.ContextMenu({containerId:"ticket-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Ticket",action:"edit-ticket",icon:"bi-pencil"},{label:"Change Status",action:"change-status",icon:"bi-tag"},{label:"Set Priority",action:"set-priority",icon:"bi-flag"},{label:"Assign User",action:"assign-user",icon:"bi-person"},{type:"divider"},{label:"Close Ticket",action:"close-ticket",icon:"bi-x-circle"}]}});this.addChild(a)}getCurrentUserId(){const e=window.app?.state?.user;return e?.id||null}async onActionEditTicket(){await a.Dialog.showModelForm({title:`Edit Ticket #${this.model.get("id")} - ${this.model.get("title")}`,model:this.model,size:"lg",fields:i.TicketForms.edit.fields})&&this.render()}async onActionChangeStatus(){const e=this.model.get("status"),t=await a.Dialog.showForm({title:"Change Ticket Status",size:"sm",fields:[{name:"status",label:"New Status",type:"select",options:["new","open","in_progress","pending","resolved","closed","ignored"].map(e=>({value:e,label:e.replace("_"," ").toUpperCase()})),value:e,required:!0}]});if(t)try{await this.model.save({status:t.status}),this.render()}catch(s){a.Dialog.alert({type:"error",title:"Error",message:"Failed to update ticket status: "+s.message})}}async onActionSetPriority(){const e=this.model.get("priority"),t=await a.Dialog.showForm({title:"Set Ticket Priority",size:"sm",fields:[{name:"priority",label:"Priority Level",type:"select",options:["low","normal","high","urgent"].map(e=>({value:e,label:e.toUpperCase()})),value:e,required:!0}]});if(t)try{await this.model.save({priority:t.priority}),this.render()}catch(s){a.Dialog.alert({type:"error",title:"Error",message:"Failed to update ticket priority: "+s.message})}}async onActionAssignUser(){a.Dialog.alert({title:"Coming Soon",message:"User assignment feature will be implemented soon."})}async onActionCloseTicket(){if(await a.Dialog.confirm({title:"Close Ticket",message:`Are you sure you want to close ticket #${this.model.get("id")}?`,confirmText:"Close Ticket",confirmClass:"btn-warning"}))try{await this.model.save({status:"closed"}),this.render(),a.Dialog.alert({type:"success",title:"Success",message:"Ticket has been closed successfully."})}catch(e){a.Dialog.alert({type:"error",title:"Error",message:"Failed to close ticket: "+e.message})}}}i.Ticket.VIEW_CLASS=TicketView;class TicketTablePage extends i.TablePage{constructor(e={}){super({name:"admin_tickets",pageName:"Tickets",router:"admin/tickets",Collection:i.TicketList,formCreate:i.TicketForms.create,formEdit:i.TicketForms.edit,itemViewClass:TicketView,viewDialogOptions:{header:!1},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"title",label:"Title",sortable:!0},{key:"status",label:"Status",sortable:!0,editable:!0,editableOptions:{type:"select",options:["new","open","paused","resolved","qa","ignored"]}},{key:"priority",label:"Priority",sortable:!0},{key:"category",label:"Category",sortable:!0,editable:!0,editableOptions:{type:"select",options:[...Object.keys(i.TicketCategories)]}},{key:"assignee.display_name",label:"Assignee",sortable:!0,formatter:"default('Unassigned')"},{key:"incident.id",label:"Incident ID",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:"No tickets found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},...e})}}class UserDeviceLocationTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_user_device_locations",pageName:"Device Locations",router:"admin/user/device-locations",Collection:e.UserDeviceLocationList,columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"user.display_name",label:"User",sortable:!0},{key:"user_device",label:"Device",template:"{{user_device.device_info.user_agent.family}} on {{user_device.device_info.os.family}}",sortable:!0},{key:"ip_address",label:"IP Address",sortable:!0},{key:"geolocation.city",label:"City",formatter:"default('—')"},{key:"geolocation.region",label:"Region",formatter:"default('—')"},{key:"geolocation.country_name",label:"Country",formatter:"default('—')"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No device locations found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class UserDeviceTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_user_devices",pageName:"User Devices",router:"admin/user/devices",Collection:e.UserDeviceList,itemViewClass:DeviceView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"duid",label:"Device ID",sortable:!0,formatter:"truncate_middle(16)"},{key:"user.display_name",label:"User",sortable:!0,formatter:"default('—')"},{key:"device_info.user_agent.family",label:"Browser",formatter:"default('—')"},{key:"device_info.os.family",label:"OS",formatter:"default('—')"},{key:"last_ip",label:"Last IP",sortable:!0},{key:"first_seen",label:"First Seen",formatter:"epoch|datetime"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No user devices found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class UserView extends t.View{constructor(t={}){super({className:"user-view",...t}),this.model=t.model||new e.User(t.data||{}),this.tabView=null,this.profileView=null,this.groupsView=null,this.eventsView=null,this.logsView=null,this.template='\n <div class="user-view-container">\n \x3c!-- User Header --\x3e\n <div data-container="user-header"></div>\n \x3c!-- Tab Container --\x3e\n <div data-container="user-tabs"></div>\n </div>\n '}async onInit(){this.header=new t.View({containerId:"user-header",template:'\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n {{{model.avatar|avatar(\'md\',\'rounded-circle\')}}}\n <div>\n <h3 class="mb-0">{{model.display_name|default(\'Unnamed User\')}}</h3>\n <a href="mailto:{{model.email}}" class="text-decoration-none text-body">{{model.email}}</a>{{{model.email|clipboard}}}\n {{#model.phone_number}}\n <div class="text-muted small mt-1">{{{model.phone_number|phone(false)}}}</div>\n {{/model.phone_number}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <i class="bi bi-circle-fill fs-8 {{model.is_active|boolean(\'text-success\',\'text-secondary\')}}"></i>\n <span>{{model.is_active|boolean(\'Active\',\'Inactive\')}}</span>\n </div>\n {{#model.last_activity}}\n <div class="text-muted small mt-1">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n </div>\n <div data-container="user-context-menu"></div>\n </div>\n </div>'}),this.header.setModel(this.model),this.addChild(this.header),this.profileView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,fields:e.UserDataView.profile.fields}),this.permsView=new r.FormView({fields:e.User.PERMISSION_FIELDS,model:this.model,autosaveModelField:!0});const a=new i.MemberList({params:{user:this.model.get("id"),size:5}});this.groupsView=new i.TableView({collection:a,hideActivePillNames:["user"],columns:[{key:"created",label:"Date Joined",formatter:"date",sortable:!0},{key:"group.name",label:"Group Name",sortable:!0},{key:"permissions|keys|badge",label:"Permissions"}]});const s=new i.IncidentEventList({params:{size:5,model_name:"account.User",model_id:this.model.get("id")}});this.eventsView=new i.TableView({collection:s,hideActivePillNames:["model_name","model_id"],columns:[{key:"id",label:"ID",sortable:!0,width:"40px"},{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"150px"},{key:"category|badge",label:"Category"},{key:"title",label:"Title"}]});const l=new e.UserDeviceList({params:{size:5,user:this.model.get("id")}});this.devicesView=new i.TableView({collection:l,hideActivePillNames:["user"],columns:[{key:"duid|truncate_middle(16)",label:"Device ID",sortable:!0},{key:"device_info.user_agent.family",label:"Browser",formatter:"default('—')"},{key:"device_info.os.family",label:"OS",formatter:"default('—')"},{key:"first_seen",label:"First Seen",formatter:"epoch|datetime"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],size:5});const o=new e.UserDeviceLocationList({params:{size:5,user:this.model.get("id")}});this.locationsView=new i.TableView({collection:o,hideActivePillNames:["user"],columns:[{key:"user_device",label:"Device",template:"{{model.user_device.device_info.user_agent.family}} on {{model.user_device.device_info.os.family}}",sortable:!0},{key:"geolocation.city",label:"City",formatter:"default('—')"},{key:"geolocation.region",label:"Region",formatter:"default('—')"},{key:"geolocation.country_name",label:"Country",formatter:"default('—')"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],size:5});const d=new i.PushDeviceList({params:{size:5,user:this.model.get("id")}});this.pushDevicesView=new i.TableView({collection:d,hideActivePillNames:["user"],columns:[{key:"duid|truncate_middle(16)",label:"Device ID",sortable:!0},{key:"device_info.user_agent.family",label:"Browser",formatter:"default('—')"},{key:"device_info.os.family",label:"OS",formatter:"default('—')"},{key:"first_seen",label:"First Seen",formatter:"epoch|datetime"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],size:5});const c=new i.LogList({params:{size:5,model_name:"account.User",model_id:this.model.get("id")}});this.logsView=new i.TableView({collection:c,permissions:"view_logs",hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"log",label:"Log"}]});const m=new i.LogList({params:{size:5,uid:this.model.get("id")}});this.activityView=new i.TableView({collection:m,hideActivePillNames:["uid"],permissions:"view_logs",columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"path",label:"Path"}]}),this.tabView=new i.TabView({tabs:{Profile:this.profileView,Permissions:this.permsView,Groups:this.groupsView,Events:this.eventsView,Logs:this.logsView,Activity:this.activityView,Devices:this.devicesView,Locations:this.locationsView,"Push Devices":this.pushDevicesView},activeTab:"Profile",containerId:"user-tabs",enableResponsive:!0,dropdownStyle:"select",minWidth:300}),this.addChild(this.tabView);const b=new e.ContextMenu({containerId:"user-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit User",action:"edit-user",icon:"bi-pencil"},{label:"Reset Password",action:"reset-password",icon:"bi-key"},{type:"divider"},this.model.get("is_active")?{label:"Deactivate User",action:"deactivate-user",icon:"bi-person-dash"}:{label:"Activate User",action:"activate-user",icon:"bi-person-check"}]}});this.addChild(b)}async onActionEditUser(){let t=e.UserForms.edit;await a.Dialog.showModelForm({title:`EDIT - #${this.model.id} ${this.options.modelName}`,model:this.model,formConfig:t})&&this.render()}async onActionResetPassword(){}async onActionDeactivateUser(){await a.Dialog.confirm("Are you sure you want to disable this user?")?(await this.model.save({is_active:!1}),this.getApp().toast.success("Member disable")):this.getApp().toast.error("Member disable failed")}async onActionActivateUser(){await a.Dialog.confirm("Are you sure you want to enable this user?")?(await this.model.save({is_active:!0}),this.getApp().toast.success("Member enabled")):this.getApp().toast.error("Member enable failed")}async onActionViewGroup(e,t,a){a.getAttribute("data-id")}async onActionRemoveFromGroup(e,t,a){a.getAttribute("data-id")}async onActionViewEvent(e,t,a){a.getAttribute("data-id")}async onActionViewLog(e,t,a){a.getAttribute("data-id")}async showTab(e){this.tabView&&await this.tabView.showTab(e)}getActiveTab(){return this.tabView?this.tabView.getActiveTab():null}_onModelChange(){}static create(e={}){return new UserView(e)}}e.User.VIEW_CLASS=UserView;class UserTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_users",pageName:"Manage Users",router:"admin/users",Collection:e.UserList,viewDialogOptions:{header:!1},defaultQuery:{sort:"-last_activity",is_active:!0},columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"display_name|tooltip:model.username",label:"Display Name"},{label:"Info",key:"permissions.manage_users",template:"\n {{^model.is_active}}<span class=\"text-danger\">DISABLED</span> {{/model.is_active}}\n {{#model.permissions.manage_users}}{{{model.permissions.manage_users|yesnoicon('bi bi-person-gear text-danger')|tooltip('Manage Users')}}} {{/model.permissions.manage_users}}\n {{#model.permissions.manage_groups}}{{{model.permissions.manage_groups|yesnoicon('bi bi-building-gear text-primary')|tooltip('Manage Groups')}}} {{/model.permissions.manage_groups}}\n {{#model.permissions.view_global}}{{{model.permissions.view_global|yesnoicon('bi bi-globe text-secondary')|tooltip('View Global Menu')}}} {{/model.permissions.view_global}}\n {{#model.permissions.view_admin}}{{{model.permissions.view_admin|yesnoicon('bi bi-wrench text-secondary')|tooltip('View Admin Menu')}}} {{/model.permissions.view_admin}}\n ",sortable:!1},{key:"email",label:"Email",visibility:"xl",className:"text-muted fs-8"},{key:"last_activity",label:"Last Activity",formatter:"relative",className:"text-muted fs-8"}],filters:[{key:"is_active",label:"Active",type:"boolean",defaultValue:!0},{key:"email",label:"Email",type:"text",defaultValue:""},{key:"username",label:"Username",type:"text",defaultValue:""},{key:"locations__ip_address",label:"IP Address",type:"text",defaultValue:""},{key:"last_activity",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No users found. Click "Add" to create a new user.',contextMenu:[{icon:"bi-pencil",action:"edit",label:"Edit Profile"},{icon:"bi-shield-check",action:"edit-permissions",label:"Edit Permissions"},{icon:"bi-shield",action:"change-password",label:"Change Password"},{separator:!0},{icon:"bi-envelope",action:"send-invite",label:"Send Invite"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionEditPermissions(t,s){t.preventDefault();const i=this.collection.get(s.dataset.id);await a.Dialog.showModelForm({model:i,size:"lg",title:`Edit Permissions for "${i._.username}"`,fields:e.UserForms.permissions.fields})}async onActionChangePassword(e,s){const i=this.collection.get(s.dataset.id),n=await a.Dialog.showForm({title:`Change Password for "${i._.username}"`,fields:[{type:"text",name:"username",value:i.get("email")||i.get("username"),attributes:{autocomplete:"username",readonly:"readonly",tabindex:"-1",style:"position: absolute; left: -9999px; opacity: 0; height: 0; width: 0;"}},{name:"new_password",label:"New Password",type:"password",passwordUsage:"new",required:!0,showToggle:!0,attributes:{autocomplete:"new-password"}}]});if(n&&n.new_password){if(t.MOJOUtils.checkPasswordStrength(n.new_password).score<5)return this.getApp().toast.error("Password must be at least 6 characters long and contain at least 2 of the following: uppercase letter, lowercase letter, or number"),void(await this.onActionChangePassword(e,s));const a=await i.save({new_password:n.new_password});this.onPasswordChange(a)||await this.onActionChangePassword(e,s)}}onPasswordChange(e){return e.success?(this.getApp().toast.success("Password changed successfully"),!0):(e.data&&e.data.error?this.getApp().toast.error(e.data.error):this.getApp().toast.error("Failed to change password"),!1)}async onActionSendInvite(e,t){const a=this.collection.get(t.dataset.id),s=await a.save({send_invite:!0});return s.success?(this.getApp().toast.success("Invite sent successfully"),!0):(s.data&&s.data.error?this.getApp().toast.error(s.data.error):this.getApp().toast.error("Failed to send invite"),!1)}}class PhoneNumber extends l.Model{constructor(e={},t={}){super(e,{endpoint:"/api/phonehub/number",...t})}static async normalize(e,a="US"){const s={phone_number:e};a&&(s.country_code=a);const i=await t.rest.POST("/api/phonehub/number/normalize",s),n=i?.data??i;return!0===n?.status||!0===n?.success?{success:!0,phone_number:n?.data?.phone_number??n?.phone_number,data:n?.data??n,response:i}:{success:!1,error:n?.error||"Normalization failed",response:i}}static async lookup(e,a={}){const s=await t.rest.POST("/api/phonehub/number/lookup",{phone_number:e,...a}),i=s?.data??s;if(!0===i?.status||!0===i?.success){const e=i?.data??{};return{success:!0,model:new PhoneNumber(e,{endpoint:"/api/phonehub/number"}),data:e,response:s}}return{success:!1,error:i?.error||"Phone lookup failed",response:s}}}class PhoneNumberList extends l.Collection{constructor(e={}){super({ModelClass:PhoneNumber,endpoint:"/api/phonehub/number",size:10,...e})}}class SMS extends l.Model{constructor(e={},t={}){super(e,{endpoint:"/api/phonehub/sms",...t})}static async send(e={}){const a=await t.rest.POST("/api/phonehub/sms/send",e),s=a?.data??a;if(!0===s?.status||!0===s?.success){const e=s?.data??{};return{success:!0,model:new SMS(e,{endpoint:"/api/phonehub/sms"}),data:e,response:a}}return{success:!1,error:s?.error||"Failed to send SMS",response:a}}}class SMSList extends l.Collection{constructor(e={}){super({ModelClass:SMS,endpoint:"/api/phonehub/sms",size:10,...e})}}class PhoneNumberView extends t.View{constructor(e={}){super({className:"phone-number-view",...e}),this.model=e.model||new PhoneNumber(e.data||{}),this.template='\n <div class="phone-number-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi bi-telephone"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.phone_number|default(\'Unknown Number\')}}</h3>\n <div class="text-muted small">\n {{model.carrier|default(\'—\')}} {{#model.line_type}}· {{model.line_type|capitalize}}{{/model.line_type}}\n </div>\n <div class="text-muted small mt-1">\n {{#model.country_code}}Country: {{model.country_code}}{{/model.country_code}}\n {{#model.region}} · Region: {{model.region}}{{/model.region}}\n {{#model.state}} · State: {{model.state}}{{/model.state}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div data-container="phone-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="phone-tabs"></div>\n </div>\n '}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"phone_number",label:"Phone Number",cols:6},{name:"country_code",label:"Country Code",cols:6},{name:"region",label:"Region",cols:6},{name:"state",label:"State",cols:6},{name:"registered_owner",label:"Registered Owner",cols:6},{name:"owner_type",label:"Owner Type",formatter:"capitalize",cols:6},{name:"is_valid",label:"Valid",formatter:"yesnoicon",cols:4},{name:"is_mobile",label:"Mobile",formatter:"yesnoicon",cols:4},{name:"is_voip",label:"VOIP",formatter:"yesnoicon",cols:4}]}),this.carrierView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"carrier",label:"Carrier",cols:6},{name:"line_type",label:"Line Type",formatter:"capitalize",cols:6},{name:"lookup_provider",label:"Lookup Provider",formatter:"capitalize",cols:6},{name:"lookup_count",label:"Lookup Count",cols:6},{name:"last_lookup_at",label:"Last Lookup",formatter:"datetime",cols:6},{name:"lookup_expires_at",label:"Cache Expires",formatter:"datetime",cols:6}]}),this.addressView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"address_line1",label:"Address Line 1",cols:12},{name:"address_city",label:"City",cols:4},{name:"address_state",label:"State",cols:4},{name:"address_zip",label:"ZIP",cols:4},{name:"address_country",label:"Country",cols:6}]}),this.metadataView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"Record ID",cols:6},{name:"created",label:"Created",formatter:"datetime",cols:6},{name:"modified",label:"Last Modified",formatter:"datetime",cols:6}]});const t={Overview:this.overviewView,Carrier:this.carrierView,Address:this.addressView,Metadata:this.metadataView};this.tabView=new i.TabView({containerId:"phone-tabs",tabs:t,activeTab:"Overview"}),this.addChild(this.tabView);const a=new e.ContextMenu({containerId:"phone-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Refresh Lookup",action:"refresh-lookup",icon:"bi-arrow-repeat"},{type:"divider"},{label:"Delete Record",action:"delete-phone",icon:"bi-trash",danger:!0}]}});this.addChild(a)}async onActionRefreshLookup(){const e=this.model.get("phone_number");if(e)try{this.getApp()?.toast?.info?.("Refreshing lookup...");const t=await PhoneNumber.lookup(e,{force_refresh:!0});if(t.success&&t.data)this.model.set(t.data),await this.render(),this.getApp()?.toast?.success?.("Lookup refreshed");else{const e=t.error||"Lookup failed";this.getApp()?.toast?.error?.(e)}}catch(t){this.getApp()?.toast?.error?.(t.message||"Lookup failed")}else this.getApp()?.toast?.warning?.("No phone number to lookup")}async onActionDeletePhone(){if(await a.Dialog.confirm(`Are you sure you want to delete the record for "${this.model.get("phone_number")||"this number"}"?`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"}))try{const e=await this.model.destroy();e?.success?this.emit("phone:deleted",{model:this.model}):this.getApp()?.toast?.error?.("Delete failed")}catch(e){this.getApp()?.toast?.error?.(e.message||"Delete failed")}}}PhoneNumberView.MODEL_CLASS=PhoneNumber;class PhoneNumberTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_phonehub_numbers",pageName:"Phone Numbers",router:"admin/phonehub/numbers",Collection:PhoneNumberList,itemView:PhoneNumberView,viewDialogOptions:{header:!1},columns:[{key:"phone_number",label:"Phone Number",sortable:!0},{key:"carrier",label:"Carrier",sortable:!0,formatter:"default('—')"},{key:"line_type",label:"Line Type",sortable:!0,formatter:"capitalize"},{key:"is_mobile",label:"Mobile",formatter:"yesnoicon"},{key:"is_voip",label:"VOIP",formatter:"yesnoicon"},{key:"is_valid",label:"Valid",formatter:"yesnoicon"},{key:"registered_owner",label:"Owner",sortable:!0,formatter:"default('—')"},{key:"owner_type",label:"Owner Type",formatter:"capitalize"},{key:"last_lookup_at|relative",label:"Last Lookup",sortable:!0}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,clickAction:"view",showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:"No phone numbers found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},tableViewOptions:{addButtonLabel:"Lookup",addButtonIcon:"bi-search",onAdd:e=>{e.preventDefault(),this.onLookup()}}})}async onLookup(){const e=await this.getApp().showForm({title:"Lookup Phone Number",fields:[{name:"number",type:"text",required:!0}]});if(e&&e.number){const t=await PhoneNumber.lookup(e.number);t.model&&this.tableView._onRowView(t)}}}class SMSView extends t.View{constructor(e={}){super({className:"sms-view",...e}),this.model=e.model||new SMS(e.data||{}),this.template='\n <div class="sms-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi bi-chat-dots"></i>\n </div>\n <div>\n <h3 class="mb-1">\n {{#model.direction}}{{model.direction|capitalize}}{{/model.direction}}\n {{^model.direction}}Message{{/model.direction}}\n <small class="text-muted ms-2">\n {{#model.status}}[{{model.status|capitalize}}]{{/model.status}}\n </small>\n </h3>\n <div class="text-muted small">\n {{#model.from_number}}From: {{model.from_number}}{{/model.from_number}}\n {{#model.to_number}} · To: {{model.to_number}}{{/model.to_number}}\n </div>\n <div class="text-muted small mt-1">\n {{#model.provider}}Provider: {{model.provider|capitalize}}{{/model.provider}}\n {{#model.provider_message_id}} · SID: {{model.provider_message_id}}{{/model.provider_message_id}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div data-container="sms-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="sms-tabs"></div>\n </div>\n '}async onInit(){this.messageView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"direction",label:"Direction",formatter:"capitalize",cols:4},{name:"status",label:"Status",formatter:"capitalize",cols:4},{name:"from_number",label:"From",cols:6},{name:"to_number",label:"To",cols:6},{name:"body",label:"Message Body",cols:12}]}),this.deliveryView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"provider",label:"Provider",formatter:"capitalize",cols:6},{name:"provider_message_id",label:"Provider Message ID",cols:6},{name:"sent_at",label:"Sent At",formatter:"datetime",cols:6},{name:"delivered_at",label:"Delivered At",formatter:"datetime",cols:6},{name:"error_code",label:"Error Code",cols:6},{name:"error_message",label:"Error Message",cols:12}]}),this.metadataView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"Record ID",cols:6},{name:"created",label:"Created",formatter:"datetime",cols:6},{name:"modified",label:"Last Modified",formatter:"datetime",cols:6}]});const t={Message:this.messageView,Delivery:this.deliveryView,Metadata:this.metadataView};this.tabView=new i.TabView({containerId:"sms-tabs",tabs:t,activeTab:"Message"}),this.addChild(this.tabView);const a=new e.ContextMenu({containerId:"sms-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Refresh",action:"refresh-sms",icon:"bi-arrow-repeat"},{type:"divider"},{label:"Delete Message",action:"delete-sms",icon:"bi-trash",danger:!0}]}});this.addChild(a)}async onActionRefreshSms(){try{this.getApp()?.toast?.info?.("Refreshing message..."),await this.model.fetch(),await this.render(),this.getApp()?.toast?.success?.("Message refreshed")}catch(e){this.getApp()?.toast?.error?.(e.message||"Refresh failed")}}async onActionDeleteSms(){if(await a.Dialog.confirm("Are you sure you want to delete this message?","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"}))try{const e=await this.model.destroy();e?.success?this.emit("sms:deleted",{model:this.model}):this.getApp()?.toast?.error?.("Delete failed")}catch(e){this.getApp()?.toast?.error?.(e.message||"Delete failed")}}}SMSView.MODEL_CLASS=SMS;class SMSTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_phonehub_sms",pageName:"SMS Messages",router:"admin/phonehub/sms",Collection:SMSList,itemView:SMSView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"direction",label:"Direction",sortable:!0},{key:"from_number",label:"From",sortable:!0,formatter:"default('—')"},{key:"to_number",label:"To",sortable:!0,formatter:"default('—')"},{key:"status",label:"Status",sortable:!0},{key:"provider",label:"Provider",sortable:!0,formatter:"default('—')"},{key:"body",label:"Message",formatter:"default('—')"},{key:"sent_at",label:"Sent At",sortable:!0,formatter:"datetime"},{key:"delivered_at",label:"Delivered At",sortable:!0,formatter:"datetime"},{key:"created",label:"Created",sortable:!0,formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,clickAction:"view",showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No SMS messages found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}function m(e,t=!0){if(e.registerPage("system/dashboard",AdminDashboardPage,{permissions:["view_admin"]}),e.registerPage("system/jobs",JobsAdminPage,{permissions:["view_jobs","manage_jobs"]}),e.registerPage("system/users",UserTablePage,{permissions:["manage_users"]}),e.registerPage("system/groups",GroupTablePage,{permissions:["manage_groups"]}),e.registerPage("system/members",MemberTablePage,{permissions:["manage_members"]}),e.registerPage("system/s3buckets",S3BucketTablePage,{permissions:["manage_aws"]}),e.registerPage("system/filemanagers",FileManagerTablePage,{permissions:["manage_files"]}),e.registerPage("system/files",FileTablePage,{permissions:["manage_files"]}),e.registerPage("system/incidents",IncidentTablePage,{permissions:["view_incidents"]}),e.registerPage("system/events",EventTablePage,{permissions:["view_incidents"]}),e.registerPage("system/logs",LogTablePage,{permissions:["view_logs"]}),e.registerPage("system/user/devices",UserDeviceTablePage,{permissions:["manage_users"]}),e.registerPage("system/user/device-locations",UserDeviceLocationTablePage,{permissions:["manage_users"]}),e.registerPage("system/system/geoip",GeoLocatedIPTablePage,{permissions:["manage_users"]}),e.registerPage("system/email/mailboxes",EmailMailboxTablePage,{permissions:["manage_aws"]}),e.registerPage("system/email/domains",EmailDomainTablePage,{permissions:["manage_aws"]}),e.registerPage("system/email/sent",SentMessageTablePage,{permissions:["manage_aws"]}),e.registerPage("system/email/templates",EmailTemplateTablePage,{permissions:["manage_aws"]}),e.registerPage("system/incident-dashboard",IncidentDashboardPage,{permissions:["view_incidents"]}),e.registerPage("system/rulesets",RuleSetTablePage,{permissions:["manage_incidents"]}),e.registerPage("system/tickets",TicketTablePage,{permissions:["manage_incidents"]}),e.registerPage("system/metrics/permissions",MetricsPermissionsTablePage,{permissions:["manage_metrics"]}),e.registerPage("system/push/dashboard",PushDashboardPage,{permissions:["manage_users"]}),e.registerPage("system/push/configs",PushConfigTablePage,{permissions:["manage_users"]}),e.registerPage("system/push/templates",PushTemplateTablePage,{permissions:["manage_users"]}),e.registerPage("system/push/deliveries",PushDeliveryTablePage,{permissions:["manage_users"]}),e.registerPage("system/push/devices",PushDeviceTablePage,{permissions:["manage_users"]}),e.registerPage("system/phonehub/numbers",PhoneNumberTablePage,{permissions:["manage_users"]}),e.registerPage("system/phonehub/sms",SMSTablePage,{permissions:["manage_users"]}),t&&e.sidebar&&e.sidebar.getMenuConfig){const t=e.sidebar.getMenuConfig("system");if(t&&t.items){const e=[{text:"Dashboard",route:"?page=system/dashboard",icon:"bi-speedometer2",permissions:["view_admin"]},{text:"Jobs Management",route:"?page=system/jobs",icon:"bi-gear-wide-connected",permissions:["view_jobs","manage_jobs"]},{text:"Users",route:"?page=system/users",icon:"bi-people",permissions:["manage_users"]},{text:"Groups",route:"?page=system/groups",icon:"bi-diagram-3",permissions:["manage_groups"]},{text:"Incidents & Tickets",route:null,icon:"bi-shield-exclamation",permissions:["view_incidents"],children:[{text:"Dashboard",route:"?page=system/incident-dashboard",icon:"bi-bar-chart-line",permissions:["view_incidents"]},{text:"Incidents",route:"?page=system/incidents",icon:"bi-exclamation-triangle",permissions:["view_incidents"]},{text:"Tickets",route:"?page=system/tickets",icon:"bi-ticket-detailed",permissions:["manage_incidents"]},{text:"Events",route:"?page=system/events",icon:"bi-bell",permissions:["view_incidents"]},{text:"Rule Engine",route:"?page=system/rulesets",icon:"bi-gear-wide-connected",permissions:["manage_incidents"]}]},{text:"Security",route:null,icon:"bi-shield",permissions:["manage_groups"],children:[{text:"Logs",route:"?page=system/logs",icon:"bi-journal-text",permissions:["view_logs"]},{text:"User Devices",route:"?page=system/user/devices",icon:"bi-phone",permissions:["manage_users"]},{text:"Device Locations",route:"?page=system/user/device-locations",icon:"bi-geo-alt",permissions:["manage_users"]},{text:"GeoIP Cache",route:"?page=system/system/geoip",icon:"bi-globe",permissions:["manage_users"]},{text:"Metrics Permissions",route:"?page=system/metrics/permissions",icon:"bi-bar-chart-line",permissions:["manage_metrics"]}]},{text:"Storage",route:null,icon:"bi-folder",permissions:["manage_files","manage_aws"],children:[{text:"S3 Buckets",route:"?page=system/s3buckets",icon:"bi-bucket",permissions:["manage_aws"]},{text:"Storage Backends",route:"?page=system/filemanagers",icon:"bi-hdd-stack",permissions:["manage_aws"]},{text:"Files",route:"?page=system/files",icon:"bi-file-earmark",permissions:["manage_files"]}]},{text:"Push Notifications",route:null,icon:"bi-broadcast",permissions:["manage_users"],children:[{text:"Dashboard",route:"?page=system/push/dashboard",icon:"bi-bar-chart-line"},{text:"Configurations",route:"?page=system/push/configs",icon:"bi-gear"},{text:"Templates",route:"?page=system/push/templates",icon:"bi-file-earmark-text"},{text:"Deliveries",route:"?page=system/push/deliveries",icon:"bi-send"},{text:"Devices",route:"?page=system/push/devices",icon:"bi-phone"}]},{text:"Email Admin",route:null,icon:"bi-envelope",permissions:["manage_aws"],children:[{text:"Domains",route:"?page=system/email/domains",icon:"bi-globe",permissions:["manage_aws"]},{text:"Mailboxes",route:"?page=system/email/mailboxes",icon:"bi-inbox",permissions:["manage_aws"]},{text:"Sent",route:"?page=system/email/sent",icon:"bi-send-check",permissions:["manage_aws"]},{text:"Templates",route:"?page=system/email/templates",icon:"bi-file-text",permissions:["manage_aws"]}]},{text:"Phone Hub",route:null,icon:"bi-telephone",permissions:["manage_users"],children:[{text:"Numbers",route:"?page=system/phonehub/numbers",icon:"bi-collection",permissions:["manage_users"]},{text:"SMS",route:"?page=system/phonehub/sms",icon:"bi-chat-dots",permissions:["manage_users"]}]}];t.items.unshift(...e)}}}exports.WebApp=a.WebApp,exports.BUILD_TIME=c.BUILD_TIME,exports.VERSION=c.VERSION,exports.VERSION_INFO=c.VERSION_INFO,exports.VERSION_MAJOR=c.VERSION_MAJOR,exports.VERSION_MINOR=c.VERSION_MINOR,exports.VERSION_REVISION=c.VERSION_REVISION,exports.AdminDashboardPage=AdminDashboardPage,exports.DeviceView=DeviceView,exports.EmailDomainTablePage=EmailDomainTablePage,exports.EmailMailboxTablePage=EmailMailboxTablePage,exports.EmailTemplateTablePage=EmailTemplateTablePage,exports.EmailTemplateView=EmailTemplateView,exports.EmailView=EmailView,exports.EventTablePage=EventTablePage,exports.EventView=EventView,exports.FileManagerTablePage=FileManagerTablePage,exports.FileTablePage=FileTablePage,exports.FileView=FileView,exports.GeoIPView=GeoIPView,exports.GeoLocatedIPTablePage=GeoLocatedIPTablePage,exports.GroupTablePage=GroupTablePage,exports.GroupView=GroupView,exports.IncidentDashboardPage=IncidentDashboardPage,exports.IncidentTablePage=IncidentTablePage,exports.IncidentView=IncidentView,exports.JobDetailsView=JobDetailsView,exports.JobHealthView=JobHealthView,exports.JobStatsView=JobStatsView,exports.JobsAdminPage=JobsAdminPage,exports.LogTablePage=LogTablePage,exports.LogView=LogView,exports.MemberTablePage=MemberTablePage,exports.MemberView=MemberView,exports.MetricsPermissionsTablePage=MetricsPermissionsTablePage,exports.MetricsPermissionsView=MetricsPermissionsView,exports.PhoneNumberTablePage=PhoneNumberTablePage,exports.PhoneNumberView=PhoneNumberView,exports.PushConfigTablePage=PushConfigTablePage,exports.PushDashboardPage=PushDashboardPage,exports.PushDeliveryTablePage=PushDeliveryTablePage,exports.PushDeliveryView=PushDeliveryView,exports.PushDeviceTablePage=PushDeviceTablePage,exports.PushDeviceView=PushDeviceView,exports.PushTemplateTablePage=PushTemplateTablePage,exports.RuleSetTablePage=RuleSetTablePage,exports.RuleSetView=RuleSetView,exports.RunnerDetailsView=RunnerDetailsView,exports.S3BucketTablePage=S3BucketTablePage,exports.SentMessageTablePage=SentMessageTablePage,exports.TaskDetailsView=TaskDetailsView,exports.TaskManagementPage=TaskManagementPage,exports.TicketTablePage=TicketTablePage,exports.TicketView=TicketView,exports.UserDeviceLocationTablePage=UserDeviceLocationTablePage,exports.UserDeviceTablePage=UserDeviceTablePage,exports.UserTablePage=UserTablePage,exports.UserView=UserView,exports.registerAdminPages=m,exports.registerSystemPages=m;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./chunks/ContextMenu-DWau8gXS.js"),t=require("./chunks/Rest-BpDyhFfG.js");require("./chunks/WebSocketClient-E08hfP5f.js");const a=require("./chunks/Dialog-ua-xN2r0.js"),s=require("./chunks/MetricsMiniChartWidget-B-DkwhEe.js"),i=require("./chunks/ChatView-BCtENksD.js"),n=require("./chunks/DataView-DESqBxT-.js"),l=require("./chunks/Collection-B64LJ92k.js"),o=require("./chunks/PDFViewer-etF76Hp4.js"),r=require("./chunks/FormView-Cd20wDM9.js"),d=require("./map.cjs.js"),c=require("./chunks/version-Cf0LAYex.js");class AdminHeaderView extends t.View{constructor(e={}){super({title:"Dashboard",...e,headerActions:[{label:"Export",icon:"bi-download",action:"export",buttonClass:"btn-primary"}],className:"admin-header-section"}),this.stats={user_activity_day:0,total_users:0,group_activity_day:0,total_groups:0,api_calls:0,apiChange:"",incidents:0,incidentsChange:""},this.prepareStatsForTemplate()}async getTemplate(){return'\n <div class="admin-stats-header mb-4">\n <div class="row">\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="user_activity_day"></div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="group_activity_day"></div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="api_activity_day"></div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div data-container="incident_activity_day"></div>\n </div>\n </div>\n </div>\n '}async onInit(){this.userActivity=new s.MetricsMiniChartWidget({icon:"bi bi-people fs-2",title:"User Activity",subtitle:"{{now_value}}",background:"#5388D6",textColor:"#FFFFFF",granularity:"days",trendRange:4,trendOffset:0,slugs:["user_activity_day"],account:"global",chartType:"bar",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"user_activity_day"}),this.addChild(this.userActivity),this.groupActivity=new s.MetricsMiniChartWidget({icon:"bi bi-collection fs-2",title:"Group Activity",subtitle:"{{now_value}}",background:"#1f6a7a",textColor:"#FFFFFF",granularity:"days",trendRange:4,trendOffset:0,slugs:["group_activity_day"],account:"global",chartType:"bar",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"group_activity_day"}),this.addChild(this.groupActivity),this.apiActivity=new s.MetricsMiniChartWidget({icon:"bi bi-graph-up fs-2",title:"API Requests",subtitle:"{{now_value}}",background:"#50A079",textColor:"#FFFFFF",endpoint:"/api/metrics/fetch",trendRange:4,trendOffset:0,granularity:"days",slugs:["api_calls"],account:"global",chartType:"line",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"api_activity_day"}),this.addChild(this.apiActivity),this.incidentActivity=new s.MetricsMiniChartWidget({icon:"bi bi-exclamation-triangle fs-2",title:"Incidents",subtitle:"{{now_value}}",background:"#B14545",textColor:"#FFFFFF",endpoint:"/api/metrics/fetch",trendRange:4,trendOffset:0,granularity:"days",slugs:["incidents"],account:"incident",chartType:"line",showTooltip:!0,showXAxis:!0,height:50,chartWidth:"100%",color:"rgba(245, 245, 255, 0.8)",fill:!0,fillColor:"rgba(245, 245, 255, 0.6)",smoothing:.3,showTrending:!0,containerId:"incident_activity_day"}),this.addChild(this.incidentActivity)}async onBeforeRender(){await Promise.all([this.loadStats(),this.loadValues()])}prepareStatsForTemplate(){}async loadValues(){try{const e=await this.getApp().rest.GET("/api/metrics/value/get",{slugs:["total_users","total_groups"],account:"global"});e.success&&e.data.status&&Object.assign(this.stats,e.data.data)}catch(e){console.error("Failed to load admin stats:",e)}}async loadStats(){try{const e=await this.getApp().rest.GET("/api/metrics/series",{slugs:["user_created","user_activity_day","incidents","api_calls","api_errors","group_activity_day"],account:"global",granularity:"days"});e.success&&e.data.status&&(Object.assign(this.stats,e.data.data),this.prepareStatsForTemplate())}catch(e){console.error("Failed to load admin stats:",e)}}}class AdminDashboardPage extends e.Page{constructor(e={}){super({...e,title:"Admin Dashboard",className:"admin-dashboard-page"}),this.pageTitle="Admin Dashboard",this.pageSubtitle="System monitoring and metrics overview"}async getTemplate(){return'\n <div class="admin-dashboard-container container-lg">\n \x3c!-- Page Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-2">\n <div>\n <p class="text-muted mb-0">{{pageSubtitle}}</p>\n <small class="text-info">\n <i class="bi bi-shield-check me-1"></i>\n Real-time system metrics and performance monitoring\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Charts">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button type="button" class="btn btn-outline-primary btn-sm"\n data-action="export-metrics" title="Export Metrics Data">\n <i class="bi bi-download"></i> Export\n </button>\n <button type="button" class="btn btn-outline-warning btn-sm"\n data-action="view-alerts" title="View System Alerts">\n <i class="bi bi-bell"></i> Alerts\n </button>\n </div>\n </div>\n\n \x3c!-- Stats Header --\x3e\n <div data-container="admin-header"></div>\n <div data-container="example-chart"></div>\n \x3c!-- Charts Section --\x3e\n <div class="row">\n \x3c!-- Full Width API Metrics Chart --\x3e\n <div class="col-12 mb-4">\n <div data-container="api-metrics-chart"></div>\n </div>\n </div>\n\n <div class="row">\n \x3c!-- System Events Chart --\x3e\n <div class="col-xl-6 col-lg-6 mb-4">\n <div data-container="system-events-chart"></div>\n </div>\n\n \x3c!-- System Incidents Chart --\x3e\n <div class="col-xl-6 col-lg-6 mb-4">\n <div data-container="system-incidents-chart"></div>\n </div>\n </div>\n\n \x3c!-- System Status Footer --\x3e\n <div class="row">\n <div class="col-12">\n <div class="alert alert-success border-0" role="alert">\n <div class="d-flex align-items-center">\n <i class="bi bi-check-circle-fill me-2"></i>\n <div>\n <strong>System Status:</strong> All systems operational.\n Last updated: <span class="text-muted">{{lastUpdated}}</span>\n </div>\n <div class="ms-auto">\n <button class="btn btn-sm btn-outline-success" data-action="view-system-status">\n <i class="bi bi-info-circle"></i> Details\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async onInit(){this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString(),this.headerView=new AdminHeaderView({containerId:"admin-header"}),this.addChild(this.headerView),this.apiMetricsChart=new s.MetricsChart({title:'<i class="bi bi-graph-up me-2"></i> API Metrics',endpoint:"/api/metrics/fetch",height:250,granularity:"hours",slugs:["api_calls","api_errors"],account:"global",chartType:"line",showDateRange:!1,yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"api-metrics-chart"}),this.addChild(this.apiMetricsChart),this.systemEventsChart=new s.MetricsChart({title:'<i class="bi bi-activity me-2"></i> System Events',endpoint:"/api/metrics/fetch",granularity:"hours",slugs:["incident_events"],account:"incident",chartType:"line",showDateRange:!1,showMetricsFilter:!1,height:250,colors:["rgba(32, 201, 151, 0.8)"],yAxis:{label:"Events",beginAtZero:!0},tooltip:{y:"number"},containerId:"system-events-chart"}),this.addChild(this.systemEventsChart),this.systemIncidentsChart=new s.MetricsChart({title:'<i class="bi bi-exclamation-triangle me-2"></i> System Incidents',endpoint:"/api/metrics/fetch",granularity:"hours",slugs:["incidents"],account:"incident",chartType:"line",showDateRange:!1,showMetricsFilter:!1,height:250,colors:["rgba(255, 193, 7, 0.8)"],yAxis:{label:"Incidents",beginAtZero:!0},tooltip:{y:"number"},containerId:"system-incidents-chart"}),this.addChild(this.systemIncidentsChart)}async onActionRefreshAll(e,t){try{const e=t.querySelector("i");e?.classList.add("bi-spin"),t.disabled=!0;const a=[this.headerView?.loadStats(),this.apiMetricsChart?.refresh(),this.systemEventsChart?.refresh(),this.systemIncidentsChart?.refresh()].filter(Boolean);await Promise.allSettled(a),this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString();const s=this.getApp()?.events;s&&s.emit("admin:dashboard-refreshed",{page:this,timestamp:this.lastUpdated})}catch(a){console.error("Failed to refresh dashboard:",a);const e=this.element.querySelector(".alert-success");e&&(e.className="alert alert-danger border-0",e.innerHTML='\n <div class="d-flex align-items-center">\n <i class="bi bi-exclamation-triangle-fill me-2"></i>\n <div>\n <strong>Error:</strong> Failed to refresh dashboard data.\n </div>\n </div>\n ',setTimeout(()=>{e.className="alert alert-success border-0",e.innerHTML=`\n <div class="d-flex align-items-center">\n <i class="bi bi-check-circle-fill me-2"></i>\n <div>\n <strong>System Status:</strong> All systems operational.\n Last updated: <span class="text-muted">${this.lastUpdated}</span>\n </div>\n </div>\n `},5e3))}finally{const e=t.querySelector("i");e?.classList.remove("bi-spin"),t.disabled=!1}}async onActionExportMetrics(e,t){try{await(this.apiMetricsChart?.export("png")),await(this.systemEventsChart?.export("png")),await(this.systemIncidentsChart?.export("png"));const e=this.getApp()?.events;e&&e.emit("admin:metrics-exported",{page:this,charts:["api-metrics","system-events","system-incidents"]})}catch(a){console.error("Failed to export metrics:",a)}}async onActionViewAlerts(e,t){const a=this.getApp()?.router;a&&a.navigateTo("/admin/alerts")}async onActionViewSystemStatus(e,t){const a=this.getApp()?.router;a&&a.navigateTo("/admin/system-status")}async refreshDashboard(){return this.onActionRefreshAll(null,null,{disabled:!1,querySelector:()=>null})}getCharts(){return{apiMetrics:this.apiMetricsChart,systemEvents:this.systemEventsChart,systemIncidents:this.systemIncidentsChart}}getStats(){return this.headerView?.stats||{}}}class EmailDomainTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_domains",pageName:"Email Domains",router:"admin/email/domains",Collection:i.EmailDomainList,formCreate:i.EmailDomainForms.create,formEdit:i.EmailDomainForms.edit,columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"name",label:"Domain",sortable:!0},{key:"region",label:"Region",sortable:!0,formatter:"default('—')"},{key:"receiving_enabled",label:"Receiving",formatter:"boolean|badge"},{key:"can_send",label:"Send Verified",formatter:"boolean|badge"},{key:"can_recv",label:"Recv Verified",formatter:"boolean|badge"},{key:"created",label:"Created",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No domains found. Click "Add Domain" to get started.',contextMenu:[{icon:"bi-shield-check",action:"edit-aws-creds",label:"Edit AWS Credentials"},{icon:"bi-rocket-takeoff",action:"onboard",label:"Onboard"},{icon:"bi-shield-check",action:"audit",label:"Audit"},{icon:"bi-arrow-repeat",action:"reconcile",label:"Reconcile"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionEditAwsCreds(e,t){const s=this.collection.get(t.dataset.id);return await a.Dialog.showModelForm({model:s,formConfig:i.EmailDomainForms.credentials}),!0}async onActionOnboard(e,t){const s=this.collection.get(t.dataset.id),n=new i.EmailDomain({id:s.id}),l=await a.Dialog.showForm(i.EmailDomainForms.onboard);if(l)try{const e=await n.onboard(l);if(!e.success)throw new Error(e.message||"Network error during onboarding");if(!e.data?.status)throw new Error(e.data?.error||"Onboarding failed");this.getApp()?.toast?.success("Domain onboarding completed successfully"),await this.refresh()}catch(o){console.error("Onboard error:",o),this.showError(o.message||"Failed to onboard domain")}}async onActionAudit(e,s){const n=this.collection.get(s.dataset.id),l=new i.EmailDomain({id:n.id});try{const e=await l.audit();if(!e.success)throw new Error(e.message||"Network error during audit");if(!e.data?.status)throw new Error(e.data?.error||"Audit failed");const s=e.data?.data||{};await a.Dialog.showDialog({title:`Audit Report - ${n.name}`,body:new t.View({template:'\n <div>\n <p class="text-muted">Drift report and status:</p>\n <pre class="bg-light p-3 rounded small"><code>{{{data.result|json}}}</code></pre>\n </div>\n ',data:{result:s}}),size:"lg"})}catch(o){console.error("Audit error:",o),this.showError(o.message||"Failed to audit domain")}}async onActionReconcile(e,t){const a=this.collection.get(t.dataset.id),s=new i.EmailDomain({id:a.id});try{const e=await s.reconcile();if(!e.success)throw new Error(e.message||"Network error during reconcile");if(!e.data?.status)throw new Error(e.data?.error||"Reconcile failed");this.getApp()?.toast?.success("Reconcile completed successfully"),await this.refresh()}catch(n){console.error("Reconcile error:",n),this.showError(n.message||"Failed to reconcile domain")}}}class EmailMailboxTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_mailboxes",pageName:"Mailboxes",router:"admin/email/mailboxes",Collection:i.MailboxList,formCreate:i.MailboxForms.create,formEdit:i.MailboxForms.edit,clickAction:"edit",columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"email",label:"Email",sortable:!0},{key:"domain.name",label:"Domain",sortable:!0,formatter:"default('—')"},{key:"allow_inbound",label:"Inbound",formatter:"boolean|badge"},{key:"allow_outbound",label:"Outbound",formatter:"boolean|badge"},{key:"is_system_default",label:"System Default",formatter:"boolean|badge"},{key:"is_domain_default",label:"Domain Default",formatter:"boolean|badge"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No mailboxes found. Click "Add Mailbox" to create one.',contextMenu:[{icon:"bi-envelope",action:"send-email",label:"Send Email"},{icon:"bi-envelope",action:"send-template-email",label:"Send Template Email"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionSendEmail(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showForm({title:"Send Email",fields:[{name:"to",label:"To",type:"email",required:!0},{name:"subject",label:"Subject",type:"text",required:!0},{name:"body_html",label:"Body",type:"textarea",required:!0}]});n.from_email=s.get("email");const l=await i.Mailbox.sendEmail(n);if(l.success)this.getApp().toast.success("Email sent successfully");else{let e="Failed to send email";l.data.details?e=l.data.details:l.data.error&&(e=l.data.error),this.getApp().toast.error(e)}}async onActionSendTemplateEmail(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showForm({title:"Send Email",fields:[{name:"to",label:"To",type:"email",required:!0},{name:"subject",label:"Subject",type:"text",required:!0},{name:"template_name",label:"Template",type:"text",required:!0},{name:"template_context",label:"Context",type:"textarea",required:!0}]});n.from_email=s.get("email");const l=await i.Mailbox.sendEmail(n);if(l.success)this.getApp().toast.success("Email sent successfully");else{let e="Failed to send email";l.data.details?e=l.data.details:l.data.error&&(e=l.data.error),this.getApp().toast.error(e)}}}class EmailTemplateView extends t.View{constructor(e={}){super({className:"email-template-view",...e}),this.model=e.model||new i.EmailTemplate(e.data||{}),this.hasHtml=!!this.model.get("html_template"),this.hasText=!!this.model.get("text_template"),this.hasMetadata=this.model.get("metadata")&&Object.keys(this.model.get("metadata")).length>0,this.template='\n <div class="email-template-view-container p-3">\n \x3c!-- Header --\x3e\n <div class="template-header border-bottom pb-3 mb-3">\n <h4 class="mb-1">{{model.name}}</h4>\n <div class="text-muted">\n <strong>Subject:</strong> {{model.subject_template|default(\'Not set\')}}\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="template-tabs"></div>\n </div>\n '}async onInit(){const e={};this.hasHtml&&(e["HTML Preview"]=new t.View({model:this.model,template:'<div class="email-html-content border rounded p-3" style="height: 500px; overflow-y: auto;">{{model.html_template}}</div>'})),this.hasText&&(e["Text Version"]=new t.View({model:this.model,template:'<pre class="email-text-content p-3 bg-light border rounded" style="white-space: pre-wrap; word-wrap: break-word;">{{model.text_template}}</pre>'})),this.hasMetadata&&(e.Metadata=new t.View({model:this.model,template:'<pre class="email-metadata-content p-3 bg-light border rounded"><code>{{model.metadata|json}}</code></pre>'})),this.tabView=new i.TabView({containerId:"template-tabs",tabs:e,activeTab:Object.keys(e)[0]||""}),this.addChild(this.tabView)}}class EmailTemplateTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_templates",pageName:"Email Templates",router:"admin/email/templates",Collection:i.EmailTemplateList,formCreate:i.EmailTemplateForms.create,formEdit:i.EmailTemplateForms.edit,itemViewClass:EmailTemplateView,clickAction:"edit",viewDialogOptions:{header:!1,size:"xl",scrollable:!0},formDialogConfig:{size:"fullscreen"},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"name",label:"Name",sortable:!0},{key:"created",label:"Created",formatter:"datetime"},{key:"modified",label:"Modified",formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No email templates found. Click "Add Template" to create your first one.',tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class StackTraceView extends t.View{constructor(e={}){super({className:"stack-trace-view",...e}),this.stackTrace=e.stackTrace||"",this.template="\n <div class=\"stack-trace-container p-3\">\n <style>\n .stack-trace-line {\n font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;\n font-size: 13px;\n line-height: 1.6;\n padding: 4px 8px;\n margin: 0;\n border-left: 3px solid transparent;\n }\n .stack-trace-line:hover {\n background-color: rgba(0, 0, 0, 0.05);\n }\n .stack-trace-error {\n color: #dc3545;\n font-weight: 600;\n border-left-color: #dc3545;\n background-color: rgba(220, 53, 69, 0.05);\n }\n .stack-trace-file {\n color: #0d6efd;\n font-weight: 500;\n border-left-color: #0d6efd;\n }\n .stack-trace-function {\n color: #6610f2;\n font-weight: 500;\n }\n .stack-trace-location {\n color: #6c757d;\n font-size: 12px;\n }\n .stack-trace-line-number {\n color: #fd7e14;\n font-weight: 600;\n }\n .stack-trace-context {\n color: #495057;\n background-color: rgba(0, 0, 0, 0.02);\n }\n .stack-trace-container {\n background-color: #f8f9fa;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n max-height: 600px;\n overflow-y: auto;\n }\n </style>\n <div class=\"stack-trace-content\">\n {{{formattedStackTrace}}}\n </div>\n </div>\n "}async onBeforeRender(){this.formattedStackTrace=this.formatStackTrace(this.stackTrace)}formatStackTrace(e){if(!e)return'<div class="text-muted p-3">No stack trace available</div>';const t=("string"==typeof e?e:JSON.stringify(e,null,2)).split("\n");let a="";return t.forEach((e,t)=>{if(!e.trim())return void(a+='<div class="stack-trace-line"> </div>');if(0===t&&(e.includes("Error:")||e.includes("Exception:")))return void(a+=`<div class="stack-trace-line stack-trace-error">${this.escapeHtml(e)}</div>`);let s=e.match(/(.+?)\s*\(([^:]+):(\d+):(\d+)\)/);if(s){const[,e,t,i,n]=s;return void(a+=`<div class="stack-trace-line stack-trace-file">\n <span class="stack-trace-function">${this.escapeHtml(e.trim())}</span>\n <span class="stack-trace-location"> (${this.escapeHtml(t)}:<span class="stack-trace-line-number">${i}</span>:${n})</span>\n </div>`)}if(s=e.match(/^\s*at\s+([^:]+):(\d+):(\d+)/),s){const[,e,t,i]=s;return void(a+=`<div class="stack-trace-line stack-trace-file">\n <span class="stack-trace-location">at ${this.escapeHtml(e)}:<span class="stack-trace-line-number">${t}</span>:${i}</span>\n </div>`)}if(s=e.match(/File\s+"([^"]+)",\s+line\s+(\d+),\s+in\s+(.+)/),s){const[,e,t,i]=s;return void(a+=`<div class="stack-trace-line stack-trace-file">\n <span class="stack-trace-location">File "${this.escapeHtml(e)}", line <span class="stack-trace-line-number">${t}</span>, in </span>\n <span class="stack-trace-function">${this.escapeHtml(i)}</span>\n </div>`)}e.trim().startsWith("at ")?a+=`<div class="stack-trace-line stack-trace-file">${this.escapeHtml(e)}</div>`:a+=`<div class="stack-trace-line stack-trace-context">${this.escapeHtml(e)}</div>`}),a}updateStackTrace(e){this.stackTrace=e,this.render()}}class EventView extends t.View{constructor(e={}){super({className:"event-view",...e}),this.model=e.model||new i.IncidentEvent(e.data||{}),this.eventIcon=this.getIconForEvent(this.model.get("level")),this.template='\n <div class="event-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 {{eventIcon.color}}">\n <i class="bi {{eventIcon.icon}}"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.title|default(\'System Event\')}}</h3>\n <div class="text-muted small">\n Category: {{model.category|capitalize}}\n </div>\n <div class="text-muted small mt-1">\n {{model.created|datetime}} from {{model.source_ip|default(\'Unknown IP\')}}\n </div>\n </div>\n </div>\n <div data-container="event-context-menu"></div>\n </div>\n\n \x3c!-- Body --\x3e\n <div data-container="event-tabs"></div>\n </div>\n '}getIconForEvent(e){return e>=40?{icon:"bi-exclamation-octagon-fill",color:"text-danger"}:e>=30?{icon:"bi-exclamation-triangle-fill",color:"text-warning"}:e>=20?{icon:"bi-info-circle-fill",color:"text-info"}:{icon:"bi-bell-fill",color:"text-secondary"}}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Event ID"},{name:"level",label:"Level"},{name:"hostname",label:"Hostname"},{name:"incident",label:"Incident ID"},{name:"model_name",label:"Related Model"},{name:"model_id",label:"Related Model ID"},{name:"details",label:"Details",columns:12}]});const a={Overview:this.overviewView},s=this.model.get("metadata")||{};s.stack_trace&&(this.stackTraceView=new StackTraceView({stackTrace:s.stack_trace}),a["Stack Trace"]=this.stackTraceView),Object.keys(s).length>0&&(this.metadataView=new t.View({model:this.model,template:'<pre class="bg-light p-3 border rounded"><code>{{{model.metadata|json}}}</code></pre>'}),a.Metadata=this.metadataView),this.tabView=new i.TabView({containerId:"event-tabs",tabs:a,activeTab:"Overview"}),this.addChild(this.tabView);const l=[{label:"View Incident",action:"view-incident",icon:"bi-shield-exclamation",disabled:!this.model.get("incident")},{label:"View Related Model",action:"view-model",icon:"bi-box-arrow-up-right",disabled:!this.model.get("model_id")},{type:"divider"},{label:"Delete Event",action:"delete-event",icon:"bi-trash",danger:!0}],o=new e.ContextMenu({containerId:"event-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:l}});this.addChild(o)}async onActionViewIncident(){this.model.get("incident")}async onActionViewModel(){this.model.get("model_name"),this.model.get("model_id")}async onActionDeleteEvent(){await a.Dialog.confirm("Are you sure you want to delete this event? This action cannot be undone.","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("event:deleted",{model:this.model})}}i.IncidentEvent.VIEW_CLASS=EventView;class EventTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_events",pageName:"System Events",router:"admin/events",Collection:i.IncidentEventList,formEdit:i.IncidentEventForms.edit,itemViewClass:EventView,viewDialogOptions:{header:!1,size:"lg"},defaultQuery:{sort:"-id",category__not:"ossec"},columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"datetime"},{key:"level",label:"Level",sortable:!0,formatter:"badge",filter:{type:"select",options:[{value:"5",label:"Critical"},{value:"4",label:"Warning"},{value:"3",label:"Info"},{value:"2",label:"Debug"},{value:"1",label:"Trace"}]}},{key:"category",label:"Category",sortable:!0,formatter:"badge",filter:{type:"text"}},{key:"title",label:"Title",sortable:!0,formatter:"truncate(50)"},{key:"source_ip",label:"Source IP",sortable:!0,filter:{type:"text"}},{key:"metadata.server",label:"Server",sortable:!0,filter:{type:"text"}}],filters:[{key:"category__not",label:"Not Category",filter:{type:"text"}}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No events found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class FileManagerTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_file_managers",pageName:"Manage Storage Backends",router:"admin/file-managers",Collection:i.FileManagerList,formCreate:i.FileManagerForms.create,formEdit:i.FileManagerForms.edit,columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"name",label:"Name",formatter:"default('Unnamed Backend')"},{key:"backend_url",label:"Backend URL",sortable:!0},{key:"is_default",label:"Default",formatter:"boolean|badge"},{key:"is_active",label:"Active",formatter:"boolean|badge"},{key:"backend_type",label:"Type",formatter:"default('Unknown')"},{key:"created",label:"Created",formatter:"epoch|datetime"}],contextMenu:[{icon:"bi-pencil",action:"edit",label:"Edit Name"},{icon:"bi-shield",action:"edit-credentials",label:"Edit Credentials"},{icon:"bi-person",action:"edit-owners",label:"Edit Owners"},{divider:!0},{icon:"bi-copy",action:"clone",label:"Clone Manager"},{divider:!0},{icon:"bi-check",action:"test-connection",label:"Test Connection"},{icon:"bi-question-circle",action:"check-cors",label:"Check CORS"},{icon:"bi-wrench",action:"fix-cors",label:"Fix CORS"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No storage backends found. Click "Add Storage Backend" to configure your first backend.',batchBarLocation:"top",batchActions:[{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Activate",icon:"bi bi-check-circle",action:"batch-activate"},{label:"Deactivate",icon:"bi bi-x-circle",action:"batch-deactivate"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionEditOwners(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showModelForm({title:"Edit Owners",model:s,fields:i.FileManagerForms.owners.fields});if(!n)return!0;n.success?this.getApp().toast.success("Owners Updated successfully"):this.getApp().toast.error("Owners update failed")}async onActionCheckCors(e,t){const s=this.collection.get(t.dataset.id),i=await s.save({check_cors:!0});return i.success&&i.data.status?await a.Dialog.showData({title:`Audit Report - ${s._.name}`,data:i.data,size:"lg"}):this.getApp().toast.error("Connection test failed"),!0}async onActionTestConnection(e,t){const a=this.collection.get(t.dataset.id),s=await a.save({test_connection:!0});return s.success&&s.data.status?this.getApp().toast.success("Connection test successful"):this.getApp().toast.error("Connection test failed"),!0}async onActionEditCredentials(e,t){const s=this.collection.get(t.dataset.id),n=await a.Dialog.showModelForm({title:"Edit Credentials",model:s,fields:i.FileManagerForms.credentials.fields});return!n||(n.success&&n.data.status?this.getApp().toast.success("Credentials updated successfully"):this.getApp().toast.error("Failed to update credentials"),!0)}async onActionClone(e,t){if(!(await a.Dialog.showConfirm({title:"Clone File Manager",message:"This will create a clone with the same credentials."})))return!0;const s=this.collection.get(t.dataset.id),i=await s.save({clone:!0});return i.success&&i.data.status?(this.getApp().toast.success("Connection cloned successfully"),this.collection.fetch()):this.getApp().toast.error("Failed to clone connection"),!0}}class FileView extends t.View{constructor(e={}){super({className:"file-view",...e}),this.model=e.model||new i.File(e.data||{}),this.isImage="image"===this.model.get("category");const t=this.model.get("renditions")||{};this.renditionsCollection=new l.Collection(Object.values(t)),this.template='\n <div class="file-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Thumbnail & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="file-thumbnail" style="width: 80px; height: 80px;">\n {{#isImage}}\n <a href="{{model.url}}" target="_blank" title="View original file">\n <img src="{{model.renditions.thumbnail.url|default(model.url)}}" class="img-fluid rounded" style="width: 80px; height: 80px; object-fit: cover;">\n </a>\n {{/isImage}}\n {{^isImage}}\n <div class="avatar-placeholder rounded bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi bi-file-earmark-text text-secondary" style="font-size: 40px;"></i>\n </div>\n {{/isImage}}\n </div>\n <div>\n <h3 class="mb-1" style="word-break: break-all;">{{model.filename|truncate(40)}}</h3>\n <div class="text-muted small">\n <span><i class="bi bi-hdd"></i> {{model.file_size|filesize}}</span>\n <span class="mx-2">|</span>\n <span>{{model.content_type}}</span>\n </div>\n <div class="text-muted small mt-1">\n Uploaded: {{model.created|datetime}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2 justify-content-end">\n <span class="badge {{model.upload_status|badge}}">{{model.upload_status|capitalize}}</span>\n </div>\n <div class="text-muted small mt-1">\n Public: {{{model.is_public|yesnoicon}}}\n </div>\n </div>\n <div data-container="file-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="file-tabs"></div>\n </div>\n '}async onInit(){this.infoView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"ID"},{name:"filename",label:"Filename"},{name:"storage_filename",label:"Storage Filename"},{name:"content_type",label:"Content Type"},{name:"file_size",label:"File Size",format:"filesize"},{name:"category",label:"Category"},{name:"upload_status",label:"Status",format:"badge"},{name:"created",label:"Created",format:"datetime"},{name:"modified",label:"Modified",format:"datetime"},{name:"user.display_name",label:"Uploaded By"},{name:"file_manager.name",label:"Storage Backend"},{name:"storage_file_path",label:"Storage Path"},{name:"url",label:"Public URL",format:"url"},{name:"is_public",label:"Is Public",format:"boolean"}]}),this.renditionsView=new i.TableView({collection:this.renditionsCollection,columns:[{key:"role",label:"Role",formatter:"badge"},{key:"filename",label:"Filename",formatter:"truncate(40)"},{key:"file_size",label:"Size",formatter:"filesize"},{key:"content_type",label:"Content Type"},{key:"actions",label:"Actions",template:'\n <a href="{{url}}" target="_blank" class="btn btn-sm btn-outline-primary" title="View">\n <i class="bi bi-eye"></i>\n </a>\n <a href="{{url}}" download="{{filename}}" class="btn btn-sm btn-outline-secondary" title="Download">\n <i class="bi bi-download"></i>\n </a>\n '}]});const t={Info:this.infoView};t.Renditions=this.renditionsView,this.tabView=new i.TabView({tabs:t,activeTab:"Info",containerId:"file-tabs"}),this.addChild(this.tabView);const a=new e.ContextMenu({containerId:"file-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View",action:"view-file",icon:"bi-eye"},{label:"Download",action:"download-file",icon:"bi bi-download"},{label:"Edit Details",action:"edit-file",icon:"bi bi-pencil"},{type:"divider"},this.model.get("is_public")?{label:"Make Private",action:"make-private",icon:"bi bi-lock"}:{label:"Make Public",action:"make-public",icon:"bi bi-unlock"},{type:"divider"},{label:"Delete File",action:"delete-file",icon:"bi bi-trash",danger:!0}]}});this.addChild(a)}async onActionViewFile(){const e=this.model.get("content_type"),t=this.model.get("url");if(e.startsWith("image/")){const e=this.model.get("renditions")||{},a=[{src:t,alt:"Original"},...Object.values(e).map(e=>({src:e.url,alt:e.role}))];o.LightboxGallery.show(a,{fitToScreen:!1})}else"application/pdf"===e?o.PDFViewer.showDialog(t,{title:this.model.get("filename")}):window.open(t,"_blank")}async onActionDownloadFile(){const e=this.model.get("url");if(e){const t=document.createElement("a");t.href=e,t.download=this.model.get("filename"),document.body.appendChild(t),t.click(),document.body.removeChild(t)}}async onActionEditFile(){await a.Dialog.showModelForm({title:`Edit File - ${this.model.get("filename")}`,model:this.model,formConfig:i.FileForms.edit})&&this.render()}async onActionMakePublic(){await this.model.save({is_public:!0}),this.render()}async onActionMakePrivate(){await this.model.save({is_public:!1}),this.render()}async onActionDeleteFile(){await a.Dialog.confirm(`Are you sure you want to delete the file "${this.model.get("filename")}"? This action cannot be undone.`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("file:deleted",{model:this.model})}}i.File.VIEW_CLASS=FileView;class FileTablePage extends i.TablePage{constructor(e={}){super({name:"admin_files",pageName:"Manage Files",router:"admin/files",Collection:i.FileList,formEdit:i.FileForms.edit,itemViewClass:FileView,onAdd:async e=>{await this.handleFileUpload(e)},viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"filename",label:"Filename"},{key:"content_type",label:"Type",formatter:"default('Unknown')"},{key:"file_size",label:"Size",formatter:"filesize"},{key:"group.name",label:"Group",formatter:"default('No Group')"},{key:"upload_status",label:"Status",formatter:"badge"},{key:"created",label:"Uploaded",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No files found. Click "Add File" to upload your first file.',batchBarLocation:"top",batchActions:[{label:"Download",icon:"bi bi-download",action:"batch-download"},{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Move to Group",icon:"bi bi-folder",action:"batch-move"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},...e}),this.enableFileDrop({acceptedTypes:["*/*"],maxFileSize:104857600,multiple:!1,validateOnDrop:!0})}async handleFileUpload(e){e&&e.preventDefault();const t=document.createElement("input");t.type="file",t.accept="*/*",t.multiple=!1,t.style.display="none",t.addEventListener("change",async e=>{const a=e.target.files[0];if(!a)return;const s=104857600;if(a.size>s)this.showError(`File size (${this._formatFileSize(a.size)}) exceeds maximum (${this._formatFileSize(s)})`);else try{const e=new i.File;let t={};this.options.requiresGroup&&this.getApp().activeGroup&&(t.group=this.getApp().activeGroup.id);const s=e.upload({file:a,name:a.name,description:`File uploaded on ${/* @__PURE__ */(new Date).toLocaleDateString()}`,showToast:!0,onProgress:e=>{e.percentage},onComplete:e=>{this.refresh()},onError:e=>{console.error("Upload failed:",e),this.showError("Upload failed: "+e.message)},...t});await s}catch(n){console.error("Error starting file upload:",n),this.showError("Failed to start file upload: "+n.message)}finally{t.remove()}}),document.body.appendChild(t),t.click()}_formatFileSize(e){if(0===e)return"0 Bytes";const t=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,t)).toFixed(2))+" "+["Bytes","KB","MB","GB"][t]}async onFileDrop(e,t,a){const s=e[0];s.name,s.type,s.size;try{const e=new i.File;let t={};this.options.requiresGroup&&this.getApp().activeGroup&&(t.group=this.getApp().activeGroup.id);const a=e.upload({file:s,name:s.name,description:`File uploaded via drag & drop on ${/* @__PURE__ */(new Date).toLocaleDateString()}`,showToast:!0,onProgress:e=>{e.percentage},onComplete:e=>{this.refresh()},onError:e=>{console.error("Upload failed:",e),this.showError("Upload failed: "+e.message)},...t});await a}catch(n){console.error("Error starting file upload:",n),this.showError("Failed to start file upload: "+n.message)}}}r.applyFileDropMixin(FileTablePage);class GeoIPView extends t.View{constructor(e={}){super({className:"geoip-view",...e}),this.model=e.model||new i.GeoLocatedIP(e.data||{}),this.hasCoordinates=this.model.get("latitude")&&this.model.get("longitude"),this.template='\n <div class="geoip-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi bi-globe-americas"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.ip_address}}</h3>\n <div class="text-muted small">\n {{model.city|default(\'Unknown Location\')}}, {{model.country_name|default(\'Unknown Location\')}}\n </div>\n <div class="text-muted small mt-1">\n ISP: {{model.isp|capitalize}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Risk Summary + Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n \x3c!-- Risk summary --\x3e\n <div class="text-end">\n <div class="d-flex align-items-baseline justify-content-end gap-2">\n <span class="text-muted">Risk:</span>\n <span class="fw-bold fs-4\n {{#model.is_threat}} text-danger {{/model.is_threat}}\n {{#model.is_suspicious}} text-warning {{/model.is_suspicious}}\n {{^model.is_threat}}{{^model.is_suspicious}} text-success {{/model.is_suspicious}}{{/model.is_threat}}\n ">{{#model.threat_level}}{{model.threat_level|capitalize}}{{/model.threat_level}}{{^model.threat_level}}Unknown{{/model.threat_level}}</span>\n </div>\n <div class="mt-1 small d-flex align-items-center justify-content-end gap-2">\n <span class="text-muted">Score:</span>\n <span class="fw-semibold">{{model.risk_score|default(\'—\')}}</span>\n </div>\n <div class="mt-1 d-flex align-items-center justify-content-end gap-2">\n <i class="bi bi-shield-lock {{#model.is_tor}}fs-4 text-success{{/model.is_tor}}{{^model.is_tor}}text-muted{{/model.is_tor}}" data-bs-toggle="tooltip" title="TOR exit"></i>\n <i class="bi bi-shield {{#model.is_vpn}}fs-4 text-success{{/model.is_vpn}}{{^model.is_vpn}}text-muted{{/model.is_vpn}}" data-bs-toggle="tooltip" title="VPN detected"></i>\n <i class="bi bi-cloud {{#model.is_cloud}}fs-4 text-success{{/model.is_cloud}}{{^model.is_cloud}}text-muted{{/model.is_cloud}}" data-bs-toggle="tooltip" title="Cloud provider"></i>\n <i class="bi bi-hdd-stack {{#model.is_datacenter}}fs-4 text-success{{/model.is_datacenter}}{{^model.is_datacenter}}text-muted{{/model.is_datacenter}}" data-bs-toggle="tooltip" title="Datacenter"></i>\n <i class="bi bi-phone {{#model.is_mobile}}fs-4 text-success{{/model.is_mobile}}{{^model.is_mobile}}text-muted{{/model.is_mobile}}" data-bs-toggle="tooltip" title="Mobile connection"></i>\n <i class="bi bi-diagram-3 {{#model.is_proxy}}fs-4 text-success{{/model.is_proxy}}{{^model.is_proxy}}text-muted{{/model.is_proxy}}" data-bs-toggle="tooltip" title="Proxy"></i>\n </div>\n </div>\n \x3c!-- Actions: context menu aligned to top (not vertically centered) --\x3e\n <div class="d-flex align-items-start">\n <div data-container="geoip-context-menu"></div>\n </div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="geoip-tabs"></div>\n </div>\n '}async onInit(){this.detailsView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"ip_address",label:"IP Address",cols:4},{name:"subnet",label:"Subnet",cols:4},{name:"country_name",label:"Country",cols:4},{name:"country_code",label:"Country Code",cols:4},{name:"region",label:"Region",cols:4},{name:"city",label:"City",cols:4},{name:"postal_code",label:"Postal Code",cols:4},{name:"timezone",label:"Timezone",cols:4},{name:"latitude",label:"Latitude",cols:4},{name:"longitude",label:"Longitude",cols:4}]}),this.networkView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"is_tor",label:"TOR Exit Node",formatter:"yesnoicon",cols:4},{name:"is_vpn",label:"VPN",formatter:"yesnoicon",cols:4},{name:"is_proxy",label:"Proxy",formatter:"yesnoicon",cols:4},{name:"is_cloud",label:"Cloud Provider",formatter:"yesnoicon",cols:4},{name:"is_datacenter",label:"Datacenter",formatter:"yesnoicon",cols:4},{name:"is_mobile",label:"Mobile",formatter:"yesnoicon",cols:4},{name:"mobile_carrier",label:"Mobile Carrier",cols:8},{name:"asn",label:"ASN",cols:4},{name:"asn_org",label:"ASN Organization",cols:8},{name:"isp",label:"ISP",cols:12},{name:"connection_type",label:"Connection Type",cols:6}]}),this.riskView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"threat_level",label:"Threat Level",cols:6},{name:"risk_score",label:"Risk Score",cols:6},{name:"is_threat",label:"Threat",formatter:"yesnoicon",cols:6},{name:"is_suspicious",label:"Suspicious",formatter:"yesnoicon",cols:6},{name:"is_known_attacker",label:"Known Attacker",formatter:"yesnoicon",cols:6},{name:"is_known_abuser",label:"Known Abuser",formatter:"yesnoicon",cols:6}]}),this.metadataView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"Record ID",cols:6},{name:"provider",label:"Data Provider",formatter:"capitalize",cols:6},{name:"created",label:"Created",formatter:"datetime",cols:6},{name:"modified",label:"Last Modified",formatter:"datetime",cols:6},{name:"last_seen",label:"Last Seen",formatter:"datetime",cols:6},{name:"expires_at",label:"Expires",formatter:"datetime",cols:6}]});const t=new i.IncidentEventList({params:{size:5,source_ip:this.model.get("ip_address")}});this.eventsView=new i.TableView({collection:t,hideActivePillNames:["source_ip"],columns:[{key:"id",label:"ID",sortable:!0,width:"40px"},{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"150px"},{key:"category|badge",label:"Category"},{key:"title",label:"Title"}]});const a=new i.LogList({params:{size:5,ip:this.model.get("ip_address")}});this.logsView=new i.TableView({collection:a,permissions:"view_logs",hideActivePillNames:["ip"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"log",label:"Log"}]});const s={Location:this.detailsView,Network:this.networkView,"Risk & Reputation":this.riskView,Events:this.eventsView,Logs:this.logsView,Metadata:this.metadataView};if(this.hasCoordinates){const e=this.model.get("latitude"),t=this.model.get("longitude"),a=[this.model.get("city")||"Unknown",this.model.get("region")||"",this.model.get("country_name")||""].filter(Boolean).join(", ");this.mapView=new d.MapView({markers:[{lat:e,lng:t,popup:`<strong>${this.model.get("ip_address")}</strong><br>${a}`}],tileLayer:"light",zoom:4,height:450}),s.Map=this.mapView}this.tabView=new i.TabView({containerId:"geoip-tabs",tabs:s,activeTab:this.hasCoordinates?"Map":"Location"}),this.addChild(this.tabView);const l=[{label:"Edit Location",action:"edit-location",icon:"bi-geo-alt"},{label:"Edit Security",action:"edit-security",icon:"bi-shield-lock"},{label:"Edit Network",action:"edit-network",icon:"bi-diagram-3"},{type:"divider"},{label:"Refresh Geolocation",action:"refresh-geoip",icon:"bi-arrow-clockwise"}];this.hasCoordinates&&l.push({label:"View on Map",action:"view-on-map",icon:"bi-map"}),l.push({type:"divider"},{label:"Delete Record",action:"delete-geoip",icon:"bi-trash",danger:!0});const o=new e.ContextMenu({containerId:"geoip-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:l}});this.addChild(o)}async onAfterRender(){await super.onAfterRender(),window.bootstrap&&window.bootstrap.Tooltip&&this.element&&this.element.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(e=>{const t=window.bootstrap.Tooltip.getInstance(e);t&&"function"==typeof t.dispose&&t.dispose(),new window.bootstrap.Tooltip(e)})}async onActionEditLocation(){await a.Dialog.showModelForm({title:`Edit Location - ${this.model.get("ip_address")}`,model:this.model,formConfig:i.GeoLocatedIP.EDIT_LOCATION_FORM})&&(await this.render(),this.getApp()?.toast?.success("Location updated successfully"))}async onActionEditSecurity(){await a.Dialog.showModelForm({title:`Edit Security - ${this.model.get("ip_address")}`,model:this.model,formConfig:i.GeoLocatedIP.EDIT_SECURITY_FORM})&&(await this.render(),this.getApp()?.toast?.success("Security settings updated successfully"))}async onActionEditNetwork(){await a.Dialog.showModelForm({title:`Edit Network - ${this.model.get("ip_address")}`,model:this.model,formConfig:i.GeoLocatedIP.EDIT_NETWORK_FORM})&&(await this.render(),this.getApp()?.toast?.success("Network information updated successfully"))}async onActionRefreshGeoip(){await this.model.save({refresh:!0}),this.getApp()?.toast?.info("Refresh request sent for "+this.model.get("ip_address"))}async onActionThreatAnalysis(){await this.model.save({threat_analysis:!0}),this.getApp()?.toast?.info("Requesting threat analysis for "+this.model.get("ip_address"))}async onActionViewOnMap(){if(this.hasCoordinates){const e=`https://www.google.com/maps/search/?api=1&query=${this.model.get("latitude")},${this.model.get("longitude")}`;window.open(e,"_blank")}}async onActionDeleteGeoip(){await a.Dialog.confirm(`Are you sure you want to delete the GeoIP record for "${this.model.get("ip_address")}"?`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("geoip:deleted",{model:this.model})}static async show(e){const t=await i.GeoLocatedIP.lookup(e);if(t){const e=new GeoIPView({model:t}),s=new a.Dialog({header:!1,size:"lg",body:e,buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}]});return await s.render(!0,document.body),s.show(),s}return a.Dialog.alert({message:`Could not find geolocation data for IP: ${e}`,type:"warning"}),null}}i.GeoLocatedIP.VIEW_CLASS=GeoIPView;class GeoLocatedIPTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_system_geoip",pageName:"GeoIP Cache",router:"admin/system/geoip",Collection:i.GeoLocatedIPList,itemView:GeoIPView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"ip_address",label:"IP Address",sortable:!0},{key:"city",label:"City",sortable:!0,formatter:"default('—')"},{key:"region",label:"Region",sortable:!0,formatter:"default('—')"},{key:"country_name",label:"Country",sortable:!0,formatter:"default('—')"},{key:"isp",label:"ISP",sortable:!0,formatter:"default('—')"},{key:"threat_level",label:"Threat",formatter:"default('—')"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,clickAction:"view",showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:"No GeoIP records found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},tableViewOptions:{addButtonLabel:"Lookup IP",onAdd:e=>{e.preventDefault(),this.onLookup()}}})}async onLookup(){const e=await this.getApp().showForm({title:"Lookup IP",fields:[{name:"ip",type:"text",required:!0}]});if(e&&e.ip){const t=await i.GeoLocatedIP.lookup(e.ip);t&&this.tableView._onRowView({model:t})}}}class GroupView extends t.View{constructor(t={}){super({className:"group-view",...t}),this.model=t.model||new e.Group(t.data||{}),this.tabView=null,this.membersView=null,this.childrenView=null,this.logsView=null,this.template='\n <div class="group-view-container">\n \x3c!-- Group Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n {{#model.avatar}}\n {{{model.avatar|avatar(\'md\',\'rounded\')}}}\n {{/model.avatar}}\n {{^model.avatar}}\n <div class="avatar-placeholder rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi bi-collection text-secondary" style="font-size: 40px;"></i>\n </div>\n {{/model.avatar}}\n <div>\n <h3 class="mb-1">{{model.name|truncate(32)|default(\'Unnamed Group\')}}</h3>\n <div class="text-muted small">\n <span>ID: {{model.id}}</span>\n <span class="mx-2">|</span>\n <span>Kind: {{model.kind|capitalize}}</span>\n {{#model.metadata.timezone}}\n <span class="mx-2">|</span>\n <span><i class="bi bi-clock"></i> {{model.metadata.timezone}}</span>\n {{/model.metadata.timezone}}\n </div>\n {{#model.parent}}\n <div class="text-muted small mt-2">\n <div>Parent: <a href="#" data-action="view-parent" data-id="{{model.parent.id}}">{{model.parent.name|truncate(32)}}</a></div>\n <div>ID: {{model.parent.id}} | Kind: {{model.parent.kind|capitalize}}</div>\n </div>\n {{/model.parent}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <i class="bi bi-circle-fill fs-8 {{model.is_active|boolean(\'text-success\',\'text-secondary\')}}"></i>\n <span>{{model.is_active|boolean(\'Active\',\'Inactive\')}}</span>\n </div>\n {{#model.last_activity}}\n <div class="text-muted small mt-1">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n </div>\n <div data-container="group-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="group-tabs"></div>\n </div>\n '}async onInit(){const t=new i.MemberList({params:{group:this.model.get("id"),size:5}});this.membersView=new i.TableView({collection:t,hideActivePillNames:["group"],columns:[{key:"user.display_name",label:"User",sortable:!0},{key:"user.email",label:"Email",sortable:!0},{key:"created",label:"Date Joined",formatter:"date",sortable:!0}],showAdd:!0,addButtonLabel:"Invite",onAdd:async e=>{this.onInviteClick(e)}});const a=new e.GroupList({params:{parent:this.model.get("id"),size:5}});this.childrenView=new i.TableView({collection:a,hideActivePillNames:["parent"],columns:[{key:"name",label:"Name",sortable:!0},{key:"kind",label:"Kind",formatter:"badge"},{key:"created",label:"Created",formatter:"date",sortable:!0}],toolbarButtons:[{label:"Add Multiple",icon:"bi bi-plus-circle",action:"add-multiple",className:"btn-success"}]});const s=new i.LogList({params:{size:5,model_name:"account.Group",model_id:this.model.get("id")}});this.logsView=new i.TableView({collection:s,permissions:"view_logs",hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime"},{key:"level",label:"Level",sortable:!0,formatter:"badge"},{key:"kind",label:"Kind"},{key:"log",label:"Log"}]}),this.tabView=new i.TabView({tabs:{Members:this.membersView,Children:this.childrenView,Logs:this.logsView},activeTab:"Members",containerId:"group-tabs"}),this.addChild(this.tabView);const n=new e.ContextMenu({containerId:"group-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Group",action:"edit-group",icon:"bi-pencil"},{label:"Add Member",action:"add-member",icon:"bi-person-plus"},{label:"Add Child Group",action:"add-child-group",icon:"bi-diagram-3"},{type:"divider"},this.model.get("is_active")?{label:"Deactivate Group",action:"deactivate-group",icon:"bi-x-circle"}:{label:"Activate Group",action:"activate-group",icon:"bi-check-circle"}]}});this.addChild(n)}async onActionEditGroup(){await a.Dialog.showModelForm({title:`Edit Group - ${this.model.get("name")}`,model:this.model,size:"lg",formConfig:e.GroupForms.detailed})&&this.render()}async onActionAddMember(){this.model.id}async onActionAddChildGroup(){this.model.id}async onActionDeactivateGroup(){this.model.id}async onActionActivateGroup(){this.model.id}async onActionViewParent(e,t){const a=t.dataset.id;this.emit("view-parent-group",{groupId:a})}async onInviteClick(e){e.preventDefault(),e.stopPropagation();const t=this.getApp(),a=await t.showForm({title:"Invite User To "+this.model.get("name"),fields:[{type:"email",name:"email",label:"Email",required:!0,columns:12}]});if(a&&a.email){t.showLoading();const e=await t.rest.POST("/api/group/member/invite",{group:this.model.id,email:a.email});t.hideLoading(),e.success?(t.toast.success("User invited successfully"),this.membersView.collection.fetch()):t.toast.error("Failed to invite user")}}async onActionAddMultiple(){const t=await a.Dialog.showForm({title:"Select Members",fields:[{name:"group_ids",type:"collectionmultiselect",required:!0,Collection:e.GroupList,labelField:"name",itemTemplate:'\n <div class="ms-2">\n <div class="fs-7">{{model.name}}</div>\n <div class="fs-8 text-muted">{{model.kind}}</div>\n </div>\n ',valueField:"id",enableSearch:!0,searchPlaceholder:"Search groups...",defaultParams:{is_active:!0,size:100,group:this.model.id}}]});t&&(console.warn(t),this.getApp().toast.warning("This is only for testing"))}}e.Group.VIEW_CLASS=GroupView;class GroupTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_groups",pageName:"Manage Groups",router:"admin/groups",Collection:e.GroupList,formCreate:e.GroupForms.create,formEdit:e.GroupForms.edit,itemViewClass:GroupView,viewDialogOptions:{header:!1},defaultQuery:{sort:"-id",is_active:1},columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"name",label:"Display Name"},{key:"kind|badge",label:"Kind",filter:{type:"select",options:e.Group.GroupKindOptions}},{key:"is_active|yesnoicon",label:"Enabled",visibility:"lg"},{key:"parent.name",label:"Parent",formatter:"default('-')",visibility:"md",class:"text-muted fs-8"},{key:"created",label:"Created",className:"text-muted fs-8",formatter:"epoch|datetime",visibility:"lg"},{key:"last_activity",label:"Activity",className:"text-muted fs-8",formatter:"relative",visibility:"lg"}],filters:[{key:"is_active",label:"Active",type:"select",options:[{label:"Active",value:!0},{label:"Inactive",value:!1}]}],contextMenu:[{icon:"bi-pencil",action:"edit",label:"Edit Group"},{icon:"bi-bullseye",action:"make-active",label:"Make Active Group"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No groups found. Click "Add Group" to create your first one.',batchBarLocation:"top",batchActions:[{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Activate",icon:"bi bi-check-circle",action:"batch-activate"},{label:"Deactivate",icon:"bi bi-x-circle",action:"batch-deactivate"},{label:"Move",icon:"bi bi-arrow-right",action:"batch-move"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}onActionMakeActive(e,t){const a=this.collection.get(t.dataset.id);this.getApp().setActiveGroup(a)}}class IncidentDashboardHeader extends t.View{constructor(e={}){super({...e,className:"incident-dashboard-header"}),this.stats={tickets:{new:0,open:0,paused:0},incidents:{new:0,open:0,paused:0,recent:0},events:{recent:0,warnings:0,critical:0}},this.setModel(new i.IncidentStats)}async getTemplate(){return'\n <div class="row">\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Open Incidents</h6>\n <h3 class="mb-1 fw-bold">{{model.incidents.open}}</h3>\n <span class="badge bg-danger-subtle text-danger">{{model.incidents.new}} New</span>\n </div>\n <div class="text-danger">\n <i class="bi bi-exclamation-triangle fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Open Tickets</h6>\n <h3 class="mb-1 fw-bold">{{model.tickets.open}}</h3>\n <span class="badge bg-warning-subtle text-warning">{{model.tickets.new}} New</span>\n </div>\n <div class="text-warning">\n <i class="bi bi-ticket-perforated fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Recent Events</h6>\n <h3 class="mb-1 fw-bold">{{model.events.recent}}</h3>\n <span class="badge bg-info-subtle text-info">{{model.events.critical}} Critical</span>\n </div>\n <div class="text-info">\n <i class="bi bi-activity fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Recent Incidents</h6>\n <h3 class="mb-1 fw-bold">{{model.incidents.recent}}</h3>\n <span class="badge bg-secondary-subtle text-secondary">Last 24h</span>\n </div>\n <div class="text-secondary">\n <i class="bi bi-clock-history fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async onBeforeRender(){await this.model.fetch()}}class IncidentDashboardPage extends e.Page{constructor(e={}){super({...e,title:"Incidents Dashboard",className:"incident-dashboard-page"})}async getTemplate(){return'\n <div class="container-fluid">\n <div class="d-flex justify-content-between align-items-center mb-2">\n <div>\n <p class="text-muted mb-0">Incidents & Tickets Dashboard</p>\n <small class="text-info">\n <i class="bi bi-shield-check me-1"></i>\n Real-time incident and event monitoring\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Charts">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n </div>\n\n <div data-container="header"></div>\n\n <div class="row">\n <div class="col-xl-8 col-lg-7">\n <div class="card shadow mb-4">\n <div class="card-body" data-container="incidents-chart"></div>\n </div>\n </div>\n <div class="col-xl-4 col-lg-5">\n <div class="card shadow mb-4">\n <div class="card-body" data-container="incidents-by-state-chart"></div>\n </div>\n </div>\n </div>\n\n <div class="row">\n <div class="col-lg-6 mb-4" data-container="my-tickets-table"></div>\n <div class="col-lg-6 mb-4" data-container="high-priority-incidents-table"></div>\n </div>\n </div>\n '}async onInit(){this.header=new IncidentDashboardHeader({containerId:"header"}),this.addChild(this.header),this.systemIncidentsChart=new s.MetricsChart({title:'<i class="bi bi-exclamation-triangle me-2"></i> System Incidents',endpoint:"/api/metrics/fetch",granularity:"hours",slugs:["incidents"],account:"incident",chartType:"line",showDateRange:!1,showMetricsFilter:!1,height:250,colors:["rgba(255, 193, 7, 0.8)"],yAxis:{label:"Incidents",beginAtZero:!0},tooltip:{y:"number"},containerId:"incidents-chart"}),this.addChild(this.systemIncidentsChart);const e=new i.TicketList({params:{assignee:this.getApp().activeUser.id,status:"open"}});this.myTicketsTable=new i.TableView({containerId:"my-tickets-table",title:"My Open Tickets",collection:e,columns:[{key:"id",label:"ID"},{key:"title",label:"Title"},{key:"priority",label:"Priority"}]}),this.addChild(this.myTicketsTable);const t=new i.IncidentList({params:{priority__gte:8,state:"open"}});this.highPriorityIncidentsTable=new i.TableView({containerId:"high-priority-incidents-table",title:"Recent High-Priority Incidents",collection:t,columns:[{key:"id",label:"ID"},{key:"title",label:"Title"},{key:"state",label:"State",formatter:"badge"}]}),this.addChild(this.highPriorityIncidentsTable)}async onActionRefreshAll(e,t){const a=t.querySelector("i");a.classList.add("bi-spin"),t.disabled=!0,await Promise.all([this.header.statsModel.fetch(),this.systemIncidentsChart.refresh(),this.myTicketsTable.collection.fetch(),this.highPriorityIncidentsTable.collection.fetch()]),a.classList.remove("bi-spin"),t.disabled=!1}}class IncidentHistoryAdapter{constructor(e){this.incidentId=e,this.collection=new i.IncidentHistoryList({params:{incident:this.incidentId}})}async fetch(){return await this.collection.fetch(),this.collection.models.map(e=>this.transform(e))}transform(e){return{id:e.get("id"),type:"comment"===e.get("kind")?"user_comment":"system_event",author:{name:e.get("by.display_name")||"System",avatarUrl:e.get("by.avatar.url")},timestamp:e.get("created"),content:e.get("note"),attachments:[]}}async addNote(e){const t=new i.IncidentHistory({incident:this.incidentId,note:e.text,kind:"comment"}),a=await t.save();return a.success&&await this.collection.fetch(),a}}class IncidentView extends t.View{constructor(e={}){super({className:"incident-view",...e}),this.model=e.model||new i.Incident(e.data||{}),this.incidentIcon=this.getIconForIncident(this.model.get("state")),this.template='\n <div class="incident-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 {{incidentIcon.color}}">\n <i class="bi {{incidentIcon.icon}}"></i>\n </div>\n <div>\n <h3 class="mb-1">Incident #{{model.id}}</h3>\n <div class="text-muted small">\n Category: {{model.category|capitalize}}\n </div>\n <div class="text-muted small mt-1">\n Created: {{model.created|datetime}}\n </div>\n </div>\n </div>\n <div class="d-flex align-items-center gap-4">\n <div class="text-end">\n <div>State: <span class="badge bg-primary">{{model.state|capitalize}}</span></div>\n <div class="text-muted small mt-1">Priority: {{model.priority}}</div>\n </div>\n <div data-container="incident-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="incident-tabs"></div>\n </div>\n '}getIconForIncident(e){const t=e?.toLowerCase();return"resolved"===t||"closed"===t?{icon:"bi-check-circle-fill",color:"text-success"}:"new"===t||"opened"===t?{icon:"bi-exclamation-triangle-fill",color:"text-danger"}:"paused"===t||"ignore"===t?{icon:"bi-pause-circle-fill",color:"text-warning"}:{icon:"bi-shield-exclamation",color:"text-secondary"}}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Incident ID"},{name:"state",label:"State",format:"badge"},{name:"priority",label:"Priority"},{name:"category",label:"Category"},{name:"model_name",label:"Related Model"},{name:"model_id",label:"Related Model ID"},{name:"details",label:"Details",columns:12,format:"pre"}]});const a=new i.IncidentEventList({params:{incident:this.model.get("id")}});this.eventsView=new i.TableView({collection:a,hideActivePillNames:["incident"],columns:[{key:"id",label:"ID",width:"70px",sortable:!0},{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"180px"},{key:"category",label:"Category",formatter:"badge",sortable:!0},{key:"title",label:"Title",sortable:!0},{key:"level",label:"Level",sortable:!0,width:"80px"}],showAdd:!1,actions:["view"],paginated:!0,size:10});const s=new IncidentHistoryAdapter(this.model.get("id"));this.historyView=new i.ChatView({adapter:s});const l={Overview:this.overviewView,Events:this.eventsView,"History & Comments":this.historyView},o=this.model.get("metadata")||{};o.stack_trace&&(this.stackTraceView=new StackTraceView({stackTrace:o.stack_trace}),l["Stack Trace"]=this.stackTraceView),Object.keys(o).length>0&&(this.metadataView=new t.View({model:this.model,template:'<pre class="bg-light p-3 border rounded"><code>{{{model.metadata|json}}}</code></pre>'}),l.Metadata=this.metadataView),this.tabView=new i.TabView({containerId:"incident-tabs",tabs:l,activeTab:"Overview"}),this.addChild(this.tabView);const r=new e.ContextMenu({containerId:"incident-context-menu",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Incident",action:"edit-incident",icon:"bi-pencil"},{label:"Resolve",action:"resolve-incident",icon:"bi-check-circle"},{type:"divider"},{label:"Delete Incident",action:"delete-incident",icon:"bi-trash",danger:!0}]}});this.addChild(r)}async onActionEditIncident(){await a.Dialog.showModelForm({title:`Edit Incident #${this.model.id}`,model:this.model,formConfig:i.IncidentForms.edit})&&this.render()}async onActionResolveIncident(){await this.model.save({state:"resolved"}),this.render(),this.emit("incident:updated",{model:this.model})}async onActionDeleteIncident(){await a.Dialog.confirm("Are you sure you want to delete this incident?","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("incident:deleted",{model:this.model})}}i.Incident.VIEW_CLASS=IncidentView;class IncidentTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_incidents",pageName:"Manage Incidents",router:"admin/incidents",Collection:i.IncidentList,formCreate:i.IncidentForms.create,formEdit:i.IncidentForms.edit,itemViewClass:IncidentView,viewDialogOptions:{header:!1,size:"xl"},defaultQuery:{sort:"-id",status:"new"},columns:[{key:"id",label:"ID",width:"60px",sortable:!0,class:"text-muted"},{key:"status",label:"Status",filter:{type:"select",options:["new","open","paused","resolved","qa","ignored"]}},{key:"created",label:"Created",formatter:"epoch|datetime"},{key:"category",label:"Category",sortable:!0,formatter:"default('General')",filter:{type:"text"}},{key:"priority",label:"Priority",filter:{type:"text"}},{key:"title",label:"title",formatter:"truncate(100)|default('No description')"}],filters:[{key:"category__not",label:"Not Category",filter:{type:"text"}}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No incidents found. Click "Add Incident" to create your first incident.',batchBarLocation:"top",batchActions:[{label:"Open",icon:"bi bi-folder2-open",action:"open"},{label:"Resolve",icon:"bi bi-check-circle",action:"resolve"},{label:"Pause",icon:"bi bi-pause-circle",action:"pause"},{label:"Ignore",icon:"bi bi-x-circle",action:"ignore"},{label:"Merge",icon:"bi bi-merge",action:"merge"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionBatchResolve(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to close ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"resolved"}))),this.tableView.collection.fetch())}async onActionBatchOpen(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to open ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"open"}))),this.tableView.collection.fetch())}async onActionBatchPause(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to pause ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"paused"}))),this.tableView.collection.fetch())}async onActionBatchIgnore(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp();await s.confirm(`Are you sure you want to ignore ${a.length} incidents?`)&&(await Promise.all(a.map(e=>e.model.save({status:"ignored"}))),this.tableView.collection.fetch())}async onActionBatchMerge(e,t){const a=this.tableView.getSelectedItems();if(!a.length)return;const s=this.getApp(),i=await s.showForm({title:`Merge ${a.length} incidents`,fields:[{name:"merge",type:"select",label:"Select Parent Incident",options:a.map(e=>({value:e.model.id,label:e.model.id})),required:!0}]});if(!i)return;const n=a.find(e=>e.model.id==i.merge)?.model;if(!n)return;const l=a.map(e=>e.model.id).filter(e=>e!=i.merge);await n.save({merge:l}),this.tableView.collection.fetch()}}class JobStatsView extends t.View{constructor(e={}){super({className:"job-stats-section",...e}),this.stats={pending:0,running:0,completed:0,failed:0,scheduled:0},this.template='\n <div class="job-stats-header mb-4">\n <div class="row">\n <div class="col-xl-2 col-lg-4 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Pending</h6>\n <h3 class="mb-1 fw-bold">{{stats.pending}}</h3>\n <span class="badge bg-primary-subtle text-primary">\n <i class="bi bi-hourglass"></i> Queued\n </span>\n </div>\n <div class="text-primary">\n <i class="bi bi-clock fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-2 col-lg-4 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Running</h6>\n <h3 class="mb-1 fw-bold">{{stats.running}}</h3>\n <span class="badge bg-success-subtle text-success">\n <i class="bi bi-arrow-repeat"></i> Active\n </span>\n </div>\n <div class="text-success">\n <i class="bi bi-play-circle fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-2 col-lg-4 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Scheduled</h6>\n <h3 class="mb-1 fw-bold">{{stats.scheduled}}</h3>\n <span class="badge bg-warning-subtle text-warning">\n <i class="bi bi-calendar-event"></i> Planned\n </span>\n </div>\n <div class="text-warning">\n <i class="bi bi-calendar3 fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Completed</h6>\n <h3 class="mb-1 fw-bold">{{stats.completed}}</h3>\n <span class="badge bg-info-subtle text-info">\n <i class="bi bi-check-circle"></i> Done\n </span>\n </div>\n <div class="text-info">\n <i class="bi bi-check-square fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-md-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Failed</h6>\n <h3 class="mb-1 fw-bold">{{stats.failed}}</h3>\n <span class="badge bg-danger-subtle text-danger">\n <i class="bi bi-x-octagon"></i> Errors\n </span>\n </div>\n <div class="text-danger">\n <i class="bi bi-exclamation-triangle fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}_onModelChange(){this.loadStats(),this.isMounted()&&this.render()}async loadStats(){this.stats=this.model.attributes.totals}}class JobHealthView extends t.View{constructor(e={}){super({className:"job-health-section",...e}),this.health={status:"unknown",runners:{active:0,total:0},channels:[]},this.template='\n <div class="job-health-header mb-4">\n <div class="card border-0 shadow">\n <div class="card-body">\n <div class="row align-items-center">\n <div class="col-md-6">\n <div class="d-flex align-items-center">\n <div class="health-indicator me-3">\n <i class="bi bi-circle-fill fs-4 {{healthStatusClass}}"></i>\n </div>\n <div>\n <h5 class="mb-1">System Health: {{health.overall_status|capitalize}}</h5>\n <small class="text-muted d-block">\n Workers: {{health.runners.active}}/{{health.runners.total}} active\n </small>\n <small class="text-muted d-block">\n Scheduler:\n <span class="{{schedulerStatusClass}} fw-bold">\n <i class="bi {{schedulerIcon}} me-1"></i>{{schedulerStatusText}}\n </span>\n </small>\n </div>\n </div>\n </div>\n <div class="col-md-6">\n <div class="d-flex justify-content-end">\n <div class="btn-group">\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-health">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button class="btn btn-sm btn-outline-secondary" data-action="system-settings">\n <i class="bi bi-gear"></i> Settings\n </button>\n </div>\n </div>\n </div>\n </div>\n {{#health.channelsArray.length}}\n <div class="row mt-3">\n <div class="col-12">\n <small class="text-muted d-block mb-2">Channel Status:</small>\n <div class="d-flex flex-wrap gap-2">\n {{#health.channelsArray}}\n <span class="badge {{statusBadgeClass}} d-flex align-items-center">\n <i class="bi {{statusIcon}} me-1"></i>\n {{channel}} ({{queued}} queued, {{inflight}} inflight)\n </span>\n {{/health.channelsArray}}\n </div>\n </div>\n </div>\n {{/health.channelsArray.length}}\n </div>\n </div>\n </div>\n '}_onModelChange(){this.loadHealth(),this.isMounted()&&this.render()}async loadHealth(){if(!this.model._.totals)return;const e=this.model.attributes;let t="healthy";0===e.totals.runners_active?t="critical":e.scheduler.active||(t="warning"),this.health={overall_status:t,channels:e.channels,runners:{active:e.totals.runners_active,total:e.runners.length},totals:e.totals,scheduler:e.scheduler},this.healthStatusClass=this.getHealthStatusClass(this.health.overall_status),this.setupChannelDisplay(),this.setupSchedulerDisplay()}setupChannelDisplay(){this.health.channels&&(this.health.channelsArray=Object.values(this.health.channels).map(e=>{let t="healthy";const a=(e.queued_count||0)+(e.inflight_count||0);return a>50&&(t="warning"),(a>100||0===e.runners)&&(t="critical"),{...e,status:t,statusBadgeClass:this.getChannelBadgeClass(t),statusIcon:this.getChannelIcon(t),queued:e.queued_count||0,inflight:e.inflight_count||0,pending:e.queued_count||0,running:e.inflight_count||0}}))}setupSchedulerDisplay(){if(!this.health.scheduler)return this.schedulerStatusText="Unknown",this.schedulerStatusClass="text-muted",void(this.schedulerIcon="bi-question-circle-fill");this.health.scheduler.active?(this.schedulerStatusText="Running",this.schedulerStatusClass="text-success",this.schedulerIcon="bi-check-circle-fill"):(this.schedulerStatusText="Stopped",this.schedulerStatusClass="text-danger",this.schedulerIcon="bi-x-octagon-fill")}getHealthStatusClass(e){return{healthy:"text-success",warning:"text-warning",critical:"text-danger",unknown:"text-muted"}[e]||"text-muted"}getChannelBadgeClass(e){return{healthy:"bg-success",warning:"bg-warning",critical:"bg-danger"}[e]||"bg-secondary"}getChannelIcon(e){return{healthy:"bi-check-circle-fill",warning:"bi-exclamation-triangle-fill",critical:"bi-x-octagon-fill"}[e]||"bi-dash-circle-fill"}async onActionRefreshHealth(e,t){try{t.disabled=!0,await this.model.fetch()}catch(a){console.error("Failed to refresh health:",a)}finally{t.disabled=!1}}async onActionSystemSettings(){await a.Dialog.showAlert({title:"System Settings",message:"System settings interface coming soon!",type:"info"})}}class JobDetailsView extends t.View{constructor(e={}){super({className:"job-details-view",...e}),this.model=e.model||new i.Job(e.data||{}),this.tabView=null,this.overviewView=null,this.payloadView=null,this.eventsView=null,this.logsView=null,this.autoRefreshInterval=null,this.template='\n <div class="job-details-container">\n \x3c!-- Job Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="avatar-placeholder rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi {{model.statusIcon}} text-secondary" style="font-size: 40px;"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.func|truncate(32)|default(\'Unknown Function\')}}</h3>\n <div class="text-muted small">\n <span>ID: {{model.id}}</span>\n <span class="mx-2">|</span>\n <span>Channel: <span class="badge bg-primary">{{model.channel}}</span></span>\n {{#model.runner_id}}\n <span class="mx-2">|</span>\n <span>Runner: {{model.runner_id|truncate(16)}}</span>\n {{/model.runner_id}}\n </div>\n <div class="text-muted small mt-2">\n <div>Created: {{model.created|datetime}}</div>\n {{#model.started_at}}\n <div>Started: {{model.started_at|datetime}}</div>\n {{/model.started_at}}\n {{#model.finished_at}}\n <div>Finished: {{model.finished_at|datetime}}</div>\n {{/model.finished_at}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <span class="badge {{model.statusBadgeClass}} fs-6">\n <i class="bi {{model.statusIcon}}"></i> {{model.status|uppercase}}\n </span>\n {{#model.cancel_requested}}\n <span class="badge bg-warning ms-1">\n <i class="bi bi-exclamation-triangle"></i> Cancel Requested\n </span>\n {{/model.cancel_requested}}\n </div>\n {{#model.formattedDuration}}\n <div class="text-muted small mt-1">Duration: {{model.formattedDuration}}</div>\n {{/model.formattedDuration}}\n </div>\n <div data-container="job-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="job-details-tabs"></div>\n </div>\n '}async onInit(){this.overviewView=new t.View({template:'\n <div class="job-overview-tab">\n <div class="card border-0 bg-light mb-3">\n <div class="card-body">\n <div class="row">\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Job ID</label>\n <div class="font-monospace">{{model.id}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Function</label>\n <div class="font-monospace">{{model.func}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Channel</label>\n <div>\n <span class="badge bg-primary">{{model.channel}}</span>\n </div>\n </div>\n {{#model.runner_id}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Runner</label>\n <div class="font-monospace small">{{model.runner_id}}</div>\n </div>\n {{/model.runner_id}}\n </div>\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Status</label>\n <div>\n <span class="badge {{model.statusBadgeClass}} fs-6">\n <i class="bi {{model.statusIcon}}"></i> {{model.status|uppercase}}\n </span>\n {{#model.cancel_requested}}\n <span class="badge bg-warning ms-1">\n <i class="bi bi-exclamation-triangle"></i> Cancel Requested\n </span>\n {{/model.cancel_requested}}\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Created</label>\n <div>{{model.created|datetime}}</div>\n </div>\n {{#model.started_at}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Started</label>\n <div>{{model.started_at|datetime}}</div>\n </div>\n {{/model.started_at}}\n {{#model.finished_at}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Finished</label>\n <div>{{model.finished_at|datetime}}</div>\n </div>\n {{/model.finished_at}}\n {{#model.duration_ms}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Duration</label>\n <div>{{model.formattedDuration}}</div>\n </div>\n {{/model.duration_ms}}\n </div>\n </div>\n </div>\n </div>\n </div>\n ',model:this.model}),this.payloadView=new t.View({template:'\n <div class="job-payload-tab">\n <pre class="bg-light p-3 rounded"><code>{{{model.payload|json}}}</code></pre>\n </div>\n ',model:this.model});const a=new i.JobEventList({params:{job:this.model.get("id"),size:10}});this.eventsView=new i.TableView({collection:a,hideActivePillNames:["job"],columns:[{key:"at",label:"Timestamp",formatter:"datetime",sortable:!0},{key:"event",label:"Event",formatter:"badge"},{key:"details|json",label:"Details"}]});const s=new i.JobLogList({params:{job:this.model.get("id"),size:10}});this.logsView=new i.TableView({collection:s,hideActivePillNames:["job"],columns:[{key:"created|datetime",label:"Created",sortable:!0},{key:"kind",label:"Kind",formatter:"badge"},{key:"message",label:"Message"}]}),this.tabView=new i.TabView({tabs:{Overview:this.overviewView,Payload:this.payloadView,Events:this.eventsView,Logs:this.logsView},activeTab:"Overview",containerId:"job-details-tabs"}),this.addChild(this.tabView);const n=[{label:"Refresh",action:"refresh-job",icon:"bi-arrow-clockwise"}];this.model.canCancel&&this.model.canCancel()&&n.push({label:"Cancel Job",action:"cancel-job",icon:"bi-x-circle",class:"text-danger"}),this.model.canRetry&&this.model.canRetry()&&n.push({label:"Retry Job",action:"retry-job",icon:"bi-arrow-repeat",class:"text-primary"});const l=new e.ContextMenu({containerId:"job-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:n}});this.addChild(l),await this.model.fetch({params:{graph:"detail"}})}async onBeforeRender(){await this.prepareJobData()}async prepareJobData(){this.model&&(this.model._.statusBadgeClass=this.model.getStatusBadgeClass?this.model.getStatusBadgeClass():"bg-secondary",this.model._.statusIcon=this.model.getStatusIcon?this.model.getStatusIcon():"bi-question-circle",this.model._.formattedDuration=this.model.getFormattedDuration?this.model.getFormattedDuration():"N/A")}async loadJobDetails(){if(this.model?.get("id"))try{this.model.getDetailedStatus&&await this.model.getDetailedStatus(),await this.prepareJobData()}catch(e){console.error("Failed to load job details:",e)}}async onActionRefreshJob(){await this.model.fetch({params:{graph:"detail"}})}async onActionCancelJob(){if(confirm("Are you sure you want to cancel this job?"))try{const e=await this.model.cancel();e.success?(await this.loadJobDetails(),await this.render(),this.emit("job-cancelled",{job:this.model})):alert("Failed to cancel job: "+(e.data?.error||"Unknown error"))}catch(e){console.error("Failed to cancel job:",e),alert("Failed to cancel job: "+e.message)}}async onActionRetryJob(){const e=await a.Dialog.showForm({title:"Retry Job",formConfig:i.JobForms.retry});if(e)try{const t=await this.model.retry(e.delay||0);t.success?this.emit("job-retried",{job:this.model,newJobId:t.newJobId}):alert("Failed to retry job: "+(t.data?.error||"Unknown error"))}catch(t){console.error("Failed to retry job:",t),alert("Failed to retry job: "+t.message)}}startAutoRefresh(){this.autoRefreshInterval&&clearInterval(this.autoRefreshInterval),this.model?.isActive&&this.model.isActive()&&(this.autoRefreshInterval=setInterval(async()=>{try{await this.loadJobDetails(),this.isMounted()&&await this.render()}catch(e){console.error("Auto-refresh failed:",e)}},5e3))}stopAutoRefresh(){this.autoRefreshInterval&&(clearInterval(this.autoRefreshInterval),this.autoRefreshInterval=null)}async onDestroy(){this.stopAutoRefresh(),await super.onDestroy()}static async show(e,t={}){const s=new JobDetailsView({model:e});return await a.Dialog.showDialog({title:`<i class="bi bi-info-circle me-2"></i>Job Details - ${e.get("id")}`,body:s,size:"xl",scrollable:!0,buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}],onHide:()=>s.stopAutoRefresh(),...t})}}i.Job.VIEW_CLASS=JobDetailsView;class JobsTable extends i.TableView{constructor(e={}){super({Collection:i.JobList,collectionParams:{size:15,sort:"-created"},options:{searchable:!0,sortable:!0,paginated:!0,size:15,...e.options},columns:[{key:"id",label:"Job ID",formatter:"truncate_middle(12)",sortable:!0,filter:{type:"text",placeholder:"Job ID..."}},{key:"status",label:"Status",formatter:(e,t)=>{const a=t.row;return`<span class="badge ${a.getStatusBadgeClass?a.getStatusBadgeClass():"bg-secondary"}"><i class="${a.getStatusIcon?a.getStatusIcon():"bi-question"} me-1"></i>${e.toUpperCase()}</span>`},sortable:!0,filter:{type:"select",options:[{value:"pending",label:"Pending"},{value:"running",label:"Running"},{value:"completed",label:"Completed"},{value:"failed",label:"Failed"},{value:"canceled",label:"Canceled"},{value:"expired",label:"Expired"}]}},{key:"channel",label:"Channel",formatter:"badge",sortable:!0,filter:{type:"text",placeholder:"Channel..."}},{key:"created",label:"Created",formatter:"datetime",sortable:!0,filter:{type:"daterange",label:"Created Date"}},{key:"started_at",label:"Started",formatter:"datetime",sortable:!0},{key:"finished_at",label:"Finished",formatter:"datetime",sortable:!0}],contextMenu:[{label:"View Details",action:"view-job-details",icon:"bi-info-circle"},{label:"View Events",action:"view-job-events",icon:"bi-clock-history"},{separator:!0},{label:"Cancel Job",action:"cancel-job",icon:"bi-x-circle",danger:!0,condition:e=>e.canCancel&&e.canCancel()},{label:"Retry Job",action:"retry-job",icon:"bi-arrow-clockwise",condition:e=>e.canRetry&&e.canRetry()},{label:"Clone Job",action:"clone-job",icon:"bi-copy"},{separator:!0},{label:"Export Job",action:"export-job",icon:"bi-download"}],filters:[{key:"func",label:"Function",type:"text"}],batchActions:[{label:"Cancel Selected",action:"batch-cancel",icon:"bi-x-circle"},{label:"Retry Selected",action:"batch-retry",icon:"bi-arrow-clockwise"},{label:"Export Selected",action:"batch-export",icon:"bi-download"}],...e})}async onItemViewJobDetails(e){e&&await JobDetailsView.show(e)}async onItemCancelJob(e){const t=await a.Dialog.showConfirm("Are you sure you want to cancel this job?");if(e&&t)try{const t=await e.cancel();t.success?(this.getApp().toast.success("Job cancelled successfully"),await this.collection.fetch()):this.getApp().toast.error(t.data?.error||"Failed to cancel job")}catch(s){this.getApp().toast.error("Error cancelling job: "+s.message)}}async onItemRetryJob(e){if(e){const s=await a.Dialog.showForm({title:"Retry Job",formConfig:i.JobForms.retry});if(s)try{const t=await e.retry(s.delay||0);t.success?(this.getApp().toast.success("Job queued for retry"),await this.collection.fetch()):this.getApp().toast.error(t.data?.error||"Failed to retry job")}catch(t){this.getApp().toast.error("Error retrying job: "+t.message)}}}async onItemCloneJob(e){if(e){const s=e.getPayload(),n=await a.Dialog.showForm({title:"Clone Job",formConfig:{...i.JobForms.clone,fields:i.JobForms.clone.fields.map(t=>("payload"===t.name?t.value=JSON.stringify(s,null,2):"channel"===t.name&&(t.value=e.get("channel")),t))}});if(n)try{let t={};n.payload&&(t=JSON.parse(n.payload));const a={payload:t,channel:n.channel||e.get("channel"),delay:n.delay||0},s=await e.cloneJob(a);s.success?(this.getApp().toast.success("Job cloned successfully"),await this.collection.fetch()):this.getApp().toast.error(s.data?.error||"Failed to clone job")}catch(t){this.getApp().toast.error("Error cloning job: "+t.message)}}}}class RunnersTable extends i.TableView{constructor(e={}){super({Collection:i.JobRunnerList,options:{searchable:!0,sortable:!0,paginated:!0,size:10,...e.options},columns:[{key:"runner_id",label:"Runner ID",formatter:"truncate_middle(16)",sortable:!0},{key:"alive",label:"Status",formatter:e=>{const t=!0===e;return`<span class="badge ${t?"bg-success":"bg-danger"}"><i class="${t?"bi-check-circle-fill":"bi-x-octagon-fill"} me-1"></i>${t?"ALIVE":"DEAD"}</span>`},sortable:!0,filter:{type:"select",options:[{value:!0,label:"Alive"},{value:!1,label:"Dead"}]}},{key:"channels",label:"Channels",formatter:e=>e&&e.length?e.map(e=>`<span class="badge bg-secondary me-1">${e}</span>`).join(""):"None",sortable:!1},{key:"jobs_processed",label:"Processed",sortable:!0},{key:"jobs_failed",label:"Failed",formatter:e=>`<span class="badge ${e>0?"bg-danger":"bg-success"}">${e}</span>`,sortable:!0},{key:"last_heartbeat",label:"Last Heartbeat",formatter:e=>{if(!e)return"Never";const t=new Date(e),a=/* @__PURE__ */new Date-t,s=Math.floor(a/1e3);return s<60?`${s}s ago`:s<3600?`${Math.floor(s/60)}m ago`:`${Math.floor(s/3600)}h ago`},sortable:!0},{key:"started",label:"Uptime",formatter:e=>{if(!e)return"Unknown";const t=new Date(e),a=/* @__PURE__ */new Date-t,s=Math.floor(a/1e3);return s<60?`${s}s`:s<3600?`${Math.floor(s/60)}m`:s<86400?`${Math.floor(s/3600)}h`:`${Math.floor(s/86400)}d`},sortable:!0}],contextMenu:[{label:"Ping Runner",action:"ping-runner",icon:"bi-wifi"},{label:"View Details",action:"view-runner-details",icon:"bi-info-circle"},{separator:!0},{label:"Pause Runner",action:"pause-runner",icon:"bi-pause-circle",condition:e=>!0===e.get("alive")},{label:"Resume Runner",action:"resume-runner",icon:"bi-play-circle",condition:e=>!0!==e.get("alive")},{separator:!0},{label:"Shutdown Runner",action:"shutdown-runner",icon:"bi-power",danger:!0,condition:e=>!0===e.get("alive")}],...e})}async onActionPingRunner(e,t){const a=t.getAttribute("data-id"),s=this.collection.get(a);if(s)try{const e=await s.ping();e.success?(this.getApp().toast.success("Runner ping successful"),await this.collection.fetch()):this.getApp().toast.error(e.data?.error||"Runner ping failed")}catch(i){this.getApp().toast.error("Error pinging runner: "+i.message)}}async onActionShutdownRunner(e,t){const a=t.getAttribute("data-id"),s=this.collection.get(a);if(s&&confirm("Are you sure you want to shutdown this runner?"))try{const e=await s.shutdown(!0);e.success?(this.getApp().toast.success("Runner shutdown initiated"),await this.collection.fetch()):this.getApp().toast.error(e.data?.error||"Failed to shutdown runner")}catch(i){this.getApp().toast.error("Error shutting down runner: "+i.message)}}}class ScheduledJobsTable extends i.TableView{constructor(e={}){super({Collection:i.JobList,collectionParams:{status:"pending"},hideActivePillNames:["status"],options:{searchable:!0,sortable:!0,paginated:!0,size:10,...e.options},columns:[{key:"id",label:"Job ID",formatter:"truncate_middle(12)",sortable:!0},{key:"func",label:"Function",sortable:!0},{key:"channel",label:"Channel",formatter:"badge",sortable:!0},{key:"run_at",label:"Scheduled For",formatter:"datetime",sortable:!0},{key:"created",label:"Created",formatter:"datetime",sortable:!0},{key:"expires_at",label:"Expires At",formatter:"datetime",sortable:!0}],...e})}}class JobsAdminPage extends e.Page{constructor(e={}){super({title:"Jobs Management",className:"jobs-admin-page",...e}),this.pageTitle="Jobs Management",this.pageSubtitle="Async job monitoring and runner management",this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString(),this.autoRefreshInterval=null,this.refreshRate=3e4,this.template='\n <div class="jobs-admin-container">\n \x3c!-- Page Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div>\n <h1 class="h3 mb-1">{{pageTitle}}</h1>\n <p class="text-muted mb-0">{{pageSubtitle}}</p>\n <small class="text-info">\n <i class="bi bi-arrow-clockwise me-1"></i>\n Auto-refresh: {{refreshRateSeconds}}s | Last updated: {{lastUpdated}}\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Data">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button type="button" class="btn btn-outline-primary btn-sm"\n data-action="export-data" title="Export Data">\n <i class="bi bi-download"></i> Export\n </button>\n <div class="dropdown">\n <button class="btn btn-outline-secondary btn-sm dropdown-toggle"\n type="button" data-bs-toggle="dropdown">\n <i class="bi bi-gear"></i> Settings\n </button>\n <ul class="dropdown-menu dropdown-menu-end">\n <li><h6 class="dropdown-header">Auto Refresh</h6></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="5">5 seconds</button></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="10">10 seconds</button></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="30">30 seconds</button></li>\n <li><button class="dropdown-item" data-action="set-refresh-rate" data-rate="0">Off</button></li>\n <li><hr class="dropdown-divider"></li>\n <li><button class="dropdown-item" data-action="runner-broadcast">Broadcast Command</button></li>\n </ul>\n </div>\n <div class="dropdown">\n <button class="btn btn-danger btn-sm dropdown-toggle"\n type="button" data-bs-toggle="dropdown">\n <i class="bi bi-wrench"></i> Manage\n </button>\n <ul class="dropdown-menu dropdown-menu-end">\n <li><button class="dropdown-item" data-action="run-simple-job"><i class="bi bi-play-circle me-2"></i>Run Simple Job</button></li>\n <li><button class="dropdown-item" data-action="run-test-jobs"><i class="bi bi-robot me-2"></i>Run Test Jobs</button></li>\n <li><hr class="dropdown-divider"></li>\n <li><button class="dropdown-item" data-action="clear-stuck"><i class="bi bi-wrench me-2"></i>Clear Stuck Jobs</button></li>\n <li><button class="dropdown-item" data-action="clear-channel"><i class="bi bi-eraser me-2"></i>Clear Channel</button></li>\n <li><button class="dropdown-item" data-action="purge-jobs"><i class="bi bi-trash me-2"></i>Purge Jobs</button></li>\n <li><button class="dropdown-item" data-action="cleanup-consumers"><i class="bi bi-people me-2"></i>Cleanup Consumers</button></li>\n </ul>\n </div>\n </div>\n </div>\n\n \x3c!-- Job Stats --\x3e\n <div data-container="job-stats"></div>\n\n \x3c!-- Job Health --\x3e\n <div class="row">\n <div class="col-12">\n <div data-container="job-health"></div>\n </div>\n <div class="col-12">\n <div class="mb-3" data-container="job-metrics"></div>\n </div>\n </div>\n\n \x3c!-- Job Tables --\x3e\n <div class="card border shadow">\n <div class="card-header">\n <h5 class="card-title mb-0">\n <i class="bi bi-list-task me-2"></i>Job Management\n </h5>\n </div>\n <div class="card-body">\n <div data-container="job-tables"></div>\n </div>\n </div>\n </div>\n '}async onInit(){this.jobStats=new i.JobsEngineStats,this.jobStatsView=new JobStatsView({containerId:"job-stats",model:this.jobStats}),this.addChild(this.jobStatsView),this.jobHealthView=new JobHealthView({containerId:"job-health",model:this.jobStats}),this.addChild(this.jobHealthView),this.jobTablesView=new i.TabView({containerId:"job-tables",tabs:{Jobs:new JobsTable,Runners:new RunnersTable,Scheduled:new ScheduledJobsTable},activeTab:"Jobs"}),this.addChild(this.jobTablesView),this.jobMetricsChart=new s.MetricsChart({title:'<i class="bi bi-graph-up me-2"></i> Job Metrics',endpoint:"/api/metrics/fetch",height:100,granularity:"hours",category:"jobs_channels",account:"global",chartType:"bar",showDateRange:!1,yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"job-metrics"}),this.addChild(this.jobMetricsChart),await this.jobStats.fetch()}startAutoRefresh(){this.autoRefreshInterval&&clearInterval(this.autoRefreshInterval),this.refreshRate>0&&(this.autoRefreshInterval=setInterval(async()=>{await this.refreshData()},this.refreshRate))}async refreshData(){try{await this.jobStats.fetch();const e=this.jobTablesView?.getActiveTab();if(e){const t=this.jobTablesView.getTab(e);t?.collection?.fetch&&await t.collection.fetch()}this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString(),this.updateHeaderTimestamp()}catch(e){console.error("Failed to refresh jobs dashboard:",e)}}updateHeaderTimestamp(){const e=this.element?.querySelector(".text-info");e&&(e.innerHTML=`\n <i class="bi bi-arrow-clockwise me-1"></i>\n Auto-refresh: ${this.refreshRate/1e3}s | Last updated: ${this.lastUpdated}\n `)}get refreshRateSeconds(){return this.refreshRate/1e3}async onActionRefreshAll(e,t){try{const e=t.querySelector("i");e?.classList.add("spinning"),t.disabled=!0,await this.refreshData(),await this.render()}catch(a){console.error("Failed to refresh jobs dashboard:",a)}finally{const e=t.querySelector("i");e?.classList.remove("spinning"),t.disabled=!1}}async onActionSetRefreshRate(e,t){const a=1e3*parseInt(t.getAttribute("data-rate"));this.refreshRate=a,this.startAutoRefresh();const s=0===a?"Off":a/1e3+"s";this.getApp().toast.success(`Auto-refresh set to ${s}`)}async onActionExportData(){await a.Dialog.showAlert({title:"Export Data",message:"Data export functionality coming soon!",type:"info"})}async onActionRunSimpleJob(e,t){await a.Dialog.showConfirm({title:"Run Simple Job",message:"This will run a simple test job to verify the job system is working correctly.",confirmText:"Run Test",confirmClass:"btn-success"})&&await this.executeJobAction(t,()=>i.Job.test(),"Test job started successfully")}async onActionRunTestJobs(e,t){await a.Dialog.showConfirm({title:"Run Test Jobs",message:"This will run a suite of test jobs to verify all job functionalities.",confirmText:"Run Tests",confirmClass:"btn-success"})&&await this.executeJobAction(t,()=>i.Job.tests(),"Test suite started successfully")}async onActionClearStuck(e,t){const s=[{value:"",label:"All Channels"},...(this.jobHealthView?.health?.channelsArray||[]).map(e=>({value:e.channel,label:e.channel}))],n=await a.Dialog.showForm({title:"Clear Stuck Jobs",formConfig:{fields:[{name:"channel",type:"select",label:"Channel",options:s,value:"",help:"Select specific channel or leave empty for all channels"}]}});n&&await this.executeJobAction(t,()=>i.Job.clearStuck(n.channel||null),e=>{const t=e.data.count||0;return`Cleared ${t} stuck job${1!==t?"s":""}${n.channel?` from channel "${n.channel}"`:""}`})}async onActionClearChannel(e,t){const s=(this.jobHealthView?.health?.channelsArray||[]).map(e=>({value:e.channel,label:e.channel})),n=await a.Dialog.showForm({title:"Clear Channel",formConfig:{fields:[{name:"channel",type:"select",label:"Channel",options:s,required:!0,help:"Select the channel to clear."}]}});n&&await this.executeJobAction(t,()=>i.Job.clearChannel(n.channel),`Channel "${n.channel}" cleared successfully.`)}async onActionPurgeJobs(e,t){const s=await a.Dialog.showForm({title:"Purge Old Jobs",formConfig:{fields:[{name:"days_old",type:"number",label:"Days Old",value:30,required:!0,help:"Delete jobs older than this many days."}]}});s&&await this.executeJobAction(t,()=>i.Job.purgeJobs(s.days_old),e=>`Purged ${e.data.count||0} old job(s).`)}async onActionCleanupConsumers(e,t){await a.Dialog.showConfirm({title:"Cleanup Consumers",message:"This will remove stale consumer records from the system. This is generally safe.",confirmText:"Cleanup",confirmClass:"btn-warning"})&&await this.executeJobAction(t,()=>i.Job.cleanConsumers(),e=>`Cleaned up ${e.data.count||0} consumer(s).`)}async onActionRunnerBroadcast(){const e=await a.Dialog.showForm({title:"Broadcast Command to All Runners",formConfig:i.JobRunnerForms.broadcast});if(e)try{const t=await i.JobRunner.broadcast(e.command,{},e.timeout);t.success?(this.getApp().toast.success(`Broadcast command "${e.command}" sent successfully`),await this.refreshData()):this.getApp().toast.error(t.data?.error||"Failed to broadcast command")}catch(t){console.error("Failed to broadcast command:",t),this.getApp().toast.error("Error broadcasting command: "+t.message)}}async executeJobAction(e,t,a){try{e.disabled=!0;const s=e.querySelector("i");s?.classList.add("spinning");const i=await t();if(i.success&&i.data?.status){const e="function"==typeof a?a(i):a;this.getApp().toast.success(e),await this.refreshData()}else this.getApp().toast.error(i.data?.error||"Operation failed")}catch(s){console.error("Job action failed:",s),this.getApp().toast.error("Error: "+s.message)}finally{e.disabled=!1;const t=e.querySelector("i");t?.classList.remove("spinning")}}async onEnter(){this.startAutoRefresh()}async onExit(){this.autoRefreshInterval&&(clearInterval(this.autoRefreshInterval),this.autoRefreshInterval=null)}async refreshDashboard(){return await this.refreshData()}getStats(){return this.jobStatsView?.stats||{}}getHealth(){return this.jobHealthView?.health||{}}}class DeviceView extends t.View{constructor(t={}){super({className:"device-view",...t}),this.model=t.model||new e.UserDevice(t.data||{}),this.deviceInfo=this.model.get("device_info")||{},this.deviceIcon=this.getIconForDevice(this.deviceInfo),this.template='\n <div class="device-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi {{deviceIcon}}"></i>\n </div>\n <div>\n <h3 class="mb-1">\n {{deviceInfo.user_agent.family}} on {{deviceInfo.os.family}}\n </h3>\n <div class="text-muted small">\n DUID: {{model.duid|truncate_middle(32)}}\n </div>\n <div class="text-muted small mt-1">\n User: <a href="#" data-action="view-user">{{model.user.display_name}}</a>\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div class="text-end">\n <div class="text-muted small">Last Seen</div>\n <div>{{model.last_seen|relative}}</div>\n <div class="text-muted small">from {{model.last_ip}}</div>\n </div>\n <div data-container="device-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="device-tabs"></div>\n </div>\n '}getIconForDevice(e){const t=e?.os?.family?.toLowerCase()||"",a=e?.user_agent?.family?.toLowerCase()||"",s=e?.device?.family?.toLowerCase()||"";return a.includes("chrome")?"bi-browser-chrome":a.includes("firefox")?"bi-browser-firefox":a.includes("safari")?"bi-browser-safari":a.includes("edge")?"bi-browser-edge":t.includes("mac")||t.includes("ios")?"bi-apple":t.includes("windows")?"bi-windows":t.includes("android")?"bi-android2":t.includes("linux")?"bi-ubuntu":s.includes("iphone")?"bi-phone":s.includes("ipad")?"bi-tablet":"bi-laptop"}async onInit(){this.infoView=new t.View({model:this.model,className:"p-3",template:'\n <div class="list-group">\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Browser</h6>\n </div>\n <p class="mb-1 fs-5">{{model.device_info.user_agent.family}} {{model.device_info.user_agent.major}}</p>\n </div>\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Operating System</h6>\n </div>\n <p class="mb-1 fs-5">{{model.device_info.os.family}} {{model.device_info.os.major}}.{{model.device_info.os.minor}}</p>\n </div>\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Device</h6>\n </div>\n <p class="mb-1 fs-5">{{model.device_info.device.brand}} {{model.device_info.device.model}}</p>\n </div>\n <div class="list-group-item">\n <div class="d-flex w-100 justify-content-between">\n <h6 class="mb-1 text-muted">Full User Agent</h6>\n </div>\n <small class="text-muted" style="word-break: break-all;">{{model.device_info.string}}</small>\n </div>\n </div>\n '});const a=new e.UserDeviceLocationList({params:{user_device:this.model.get("id"),size:10}});this.locationsView=new i.TableView({collection:a,hideActivePillNames:["user_device"],columns:[{key:"ip_address",label:"IP Address",sortable:!0},{key:"geolocation.city",label:"City",formatter:"default('—')"},{key:"geolocation.region",label:"Region",formatter:"default('—')"},{key:"geolocation.country_name",label:"Country",formatter:"default('—')"},{key:"first_seen",label:"First Seen",formatter:"datetime"},{key:"last_seen",label:"Last Seen",formatter:"datetime"}]}),this.tabView=new i.TabView({tabs:{Info:this.infoView,Locations:this.locationsView},activeTab:"Info",containerId:"device-tabs"}),this.addChild(this.tabView);const s=new e.ContextMenu({containerId:"device-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View User",action:"view-user",icon:"bi-person"},{label:"Block Device",action:"block-device",icon:"bi-shield-slash",disabled:!0},{type:"divider"},{label:"Delete Record",action:"delete-device",icon:"bi-trash",danger:!0}]}});this.addChild(s)}async onActionViewUser(){this.model.get("user"),this.emit("view-user",{userId:this.model.get("user")?.id})}async onActionDeleteDevice(){this.model.id}static async show(t){const s=await e.UserDevice.getByDuid(t);if(s){const e=new DeviceView({model:s}),t=new a.Dialog({header:!1,size:"lg",body:e,buttons:[{text:"Close",class:"btn-secondary",dismiss:!0}]});return await t.render(!0,document.body),t.show(),t}return a.Dialog.alert({message:`Could not find device with DUID: ${t}`,type:"warning"}),null}}e.UserDevice.VIEW_CLASS=DeviceView;class LogView extends t.View{constructor(e={}){super({className:"log-view",...e}),this.model=e.model||new i.Log(e.data||{}),this.logIcon=this.getIconForLog(this.model.get("level")),this.template='\n <div class="log-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 {{logIcon.color}}">\n <i class="bi {{logIcon.icon}}"></i>\n </div>\n <div>\n <h4 class="mb-1">\n <span class="badge bg-secondary">{{model.method}}</span> {{model.path}}\n </h4>\n <div class="text-muted small">\n {{model.created|datetime}}\n </div>\n </div>\n </div>\n <div data-container="log-context-menu"></div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="log-tabs"></div>\n </div>\n '}getIconForLog(e){const t=e?.toLowerCase();return"error"===t||"critical"===t?{icon:"bi-x-octagon-fill",color:"text-danger"}:"warning"===t?{icon:"bi-exclamation-triangle-fill",color:"text-warning"}:"info"===t?{icon:"bi-info-circle-fill",color:"text-info"}:{icon:"bi-journal-text",color:"text-secondary"}}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Log ID"},{name:"level",label:"Level",format:"badge"},{name:"kind",label:"Kind"},{name:"ip",label:"IP Address",template:'<a href="#" data-action="view-ip">{{model.ip}}</a>'},{name:"uid",label:"User ID"},{name:"username",label:"Username"},{name:"duid",label:"Device ID",template:'<a href="#" data-action="view-device">{{model.duid|truncate_middle(32)}}</a>'},{name:"model_name",label:"Related Model"},{name:"model_id",label:"Related Model ID"}]});const a=this.model.get("log");let s=a;try{const e=JSON.parse(a);s=JSON.stringify(e,null,2)}catch(o){}this.logContentView=new t.View({template:`\n <div class="position-relative">\n <button class="btn btn-sm btn-outline-secondary position-absolute top-0 end-0 mt-2 me-2" data-action="copy-log">\n <i class="bi bi-clipboard"></i> Copy\n </button>\n <pre class="bg-light p-3 border rounded" style="max-height: 600px; overflow-y: auto;"><code>${s}</code></pre>\n </div>\n `,onActionCopyLog:()=>{navigator.clipboard.writeText(s),this.getApp()?.toast?.success("Log content copied to clipboard.")}}),this.tabView=new i.TabView({containerId:"log-tabs",tabs:{Overview:this.overviewView,"Log Content":this.logContentView},activeTab:"Overview"}),this.addChild(this.tabView);const l=new e.ContextMenu({containerId:"log-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View User",action:"view-user",icon:"bi-person",disabled:!this.model.get("uid")},{label:"View Device",action:"view-device",icon:"bi-phone",disabled:!this.model.get("duid")},{type:"divider"},{label:"Delete Log",action:"delete-log",icon:"bi-trash",danger:!0}]}});this.addChild(l)}async onActionViewIp(e){e.preventDefault();const t=this.model.get("ip");t&&GeoIPView.show(t)}async onActionViewDevice(e){e.preventDefault();const t=this.model.get("duid");t&&DeviceView.show(t)}async onActionViewUser(){this.model.get("uid")}async onActionDeleteLog(){await a.Dialog.confirm("Are you sure you want to delete this log entry? This action cannot be undone.","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})&&(await this.model.destroy()).success&&this.emit("log:deleted",{model:this.model})}}i.Log.VIEW_CLASS=LogView;class LogTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_logs",pageName:"Manage Logs",router:"admin/logs",Collection:i.LogList,itemViewClass:LogView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"created|epoch|datetime",label:"Timestamp",sortable:!0,filter:{type:"daterange"}},{key:"level",label:"Level",sortable:!0,formatter:"badge",filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{key:"method",label:"Method",filter:{type:"text"}},{key:"path",label:"Path",filter:{type:"text"}},{key:"username",label:"User",filter:{type:"text"}},{key:"ip",label:"IP",filter:{type:"text"}},{key:"duid",label:"Browser ID",formatter:"truncate_middle(16)",filter:{type:"text"}}],defaultQuery:{sort:"-created"},selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No log entries found.",batchBarLocation:"top",batchActions:[{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Archive",icon:"bi bi-archive",action:"batch-archive"},{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Mark as Reviewed",icon:"bi bi-check2",action:"batch-reviewed"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class MemberView extends t.View{constructor(e={}){super({className:"member-view",...e}),this.model=e.model||new i.Member(e.data||{}),this.template='\n <div class="member-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-4">\n <div class="d-flex align-items-center gap-3">\n {{{model.user.avatar|avatar(\'md\',\'rounded-circle\')}}}\n <div>\n <h3 class="mb-0">{{model.user.display_name}}</h3>\n <div class="text-muted">Member of <strong>{{model.group.name}}</strong></div>\n </div>\n </div>\n\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div>Role: <span class="badge bg-primary">{{model.role|capitalize}}</span></div>\n <div class="text-muted small mt-1">Status: {{model.status|capitalize}}</div>\n </div>\n <div data-container="member-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tab Container --\x3e\n <div data-container="member-tabs"></div>\n </div>\n '}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"id",label:"Membership ID"},{name:"user.id",label:"User ID"},{name:"user.display_name",label:"User Name"},{name:"user.email",label:"User Email"},{name:"group.id",label:"Group ID"},{name:"group.name",label:"Group Name"},{name:"role",label:"Role"},{name:"status",label:"Status"},{name:"created",label:"Date Joined",format:"datetime"}]}),this.tabView=new i.TabView({containerId:"member-tabs",tabs:{Overview:this.overviewView},activeTab:"Overview"}),this.addChild(this.tabView);const t=new e.ContextMenu({containerId:"member-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Membership",action:"edit-membership",icon:"bi-pencil"},{type:"divider"},{label:"View User",action:"view-user",icon:"bi-person"},{label:"View Group",action:"view-group",icon:"bi-diagram-3"},{type:"divider"},{label:"Remove From Group",action:"remove-member",icon:"bi-trash",danger:!0}]}});this.addChild(t)}async onActionEditMembership(){await a.Dialog.showModelForm({title:"Edit Membership",model:this.model,formConfig:i.MemberForms.edit})&&this.render()}async onActionViewUser(){this.model.get("user.id")}async onActionViewGroup(){this.model.get("group.id")}async onActionRemoveMember(){await a.Dialog.confirm(`Are you sure you want to remove ${this.model.get("user.display_name")} from ${this.model.get("group.name")}?`,"Confirm Removal")&&(await this.model.destroy()).success&&this.emit("member:removed",{model:this.model})}}i.Member.VIEW_CLASS=MemberView;class MemberTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_members",pageName:"Manage Members",router:"admin/members",Collection:i.MemberList,formEdit:i.MemberForms.edit,itemViewClass:MemberView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"id",label:"ID",width:"60px",sortable:!0,class:"text-muted"},{key:"user.display_name",label:"User",formatter:"default('Unknown User')"},{key:"user.email",label:"Email",formatter:"default('No Email')"},{key:"group.name",label:"Group",formatter:"default('Unknown Group')"},{key:"role",label:"Role",formatter:"badge"},{key:"status",label:"Status",formatter:"badge"},{key:"created",label:"Added",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No members found. Click "Add Member" to add users to groups.',batchBarLocation:"top",batchActions:[{label:"Remove",icon:"bi bi-person-dash",action:"batch-remove"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Change Role",icon:"bi bi-person-gear",action:"batch-role"},{label:"Activate",icon:"bi bi-check-circle",action:"batch-activate"},{label:"Deactivate",icon:"bi bi-x-circle",action:"batch-deactivate"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class MetricsPermissionsView extends t.View{constructor(e={}){super({className:"metrics-permissions-view",...e}),this.model=e.model||new i.MetricsPermission(e.data||{}),this.template='\n <div class="container p-3">\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div>\n <h3 class="mb-1">Permissions for {{model.account}}</h3>\n </div>\n <div data-container="context-menu"></div>\n </div>\n <div data-container="data-view"></div>\n </div>\n '}async onInit(){this.dataView=new n.default({containerId:"data-view",model:this.model,fields:[{name:"view_permissions",label:"View Permissions",format:"list|badge"},{name:"write_permissions",label:"Write Permissions",format:"list|badge"}]}),this.addChild(this.dataView);const t=new e.ContextMenu({containerId:"context-menu",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit",action:"edit",icon:"bi-pencil"},{label:"Delete",action:"delete",icon:"bi-trash",danger:!0}]}});this.addChild(t)}async onActionEdit(){const e=await a.Dialog.showModelForm({title:`Edit Permissions for ${this.model.get("account")}`,model:this.model,formConfig:i.MetricsForms.edit});e&&(this.model.set(e.data.data),this.render())}async onActionDelete(){await a.Dialog.confirm(`Are you sure you want to delete all permissions for ${this.model.get("account")}?`)&&(await this.model.destroy(),this.emit("deleted",this.model))}}i.MetricsPermission.VIEW_CLASS=MetricsPermissionsView;class MetricsPermissionsTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_metrics_permissions",pageName:"Metrics Permissions",router:"admin/metrics/permissions",Collection:i.MetricsPermissionList,formEdit:i.MetricsForms.edit,itemViewClass:MetricsPermissionsView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"account",label:"Account",sortable:!0},{key:"view_permissions",label:"View Permissions",formatter:"list|badge"},{key:"write_permissions",label:"Write Permissions",formatter:"list|badge"}],selectable:!0,searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No metrics permissions found.",emptyIcon:"bi-bar-chart-line",actions:["view","edit","delete"]}})}}class PushConfigTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_configs",pageName:"Push Configurations",router:"admin/push/configs",Collection:i.PushConfigList,formCreate:i.PushConfigForms.create,formEdit:i.PushConfigForms.edit,columns:[{key:"id",label:"ID",width:"70px"},{key:"name",label:"Name"},{key:"group.name",label:"Group",formatter:"default('Default')"},{key:"fcm_sender_id",label:"Project ID"},{key:"is_active",label:"Active",format:"boolean"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,actions:["edit","delete"],tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No push configurations found.",emptyIcon:"bi-gear"}})}}class PushDashboardPage extends e.Page{constructor(e={}){super({...e,title:"Push Notifications Dashboard",className:"push-dashboard-page"})}async getTemplate(){return'\n <div class="container-fluid">\n <h1 class="h3 mb-4">Push Notifications</h1>\n <div class="row">\n \x3c!-- Stat cards --\x3e\n </div>\n <div class="row">\n <div class="col-xl-8 col-lg-7">\n <div class="card shadow mb-4">\n <div class="card-header">Notifications Over Time</div>\n <div class="card-body" data-container="deliveries-chart"></div>\n </div>\n </div>\n <div class="col-xl-4 col-lg-5">\n <div class="card shadow mb-4">\n <div class="card-header">Delivery Status</div>\n <div class="card-body" data-container="status-chart"></div>\n </div>\n </div>\n </div>\n <div class="row">\n <div class="col-lg-6 mb-4" data-container="recent-deliveries"></div>\n <div class="col-lg-6 mb-4" data-container="failed-deliveries"></div>\n </div>\n </div>\n '}async onInit(){this.deliveriesChart=new s.MetricsChart({containerId:"deliveries-chart",endpoint:"/api/metrics/fetch",slugs:["push_sent","push_failed"],chartType:"line"}),this.addChild(this.deliveriesChart),this.statusChart=new s.PieChart({containerId:"status-chart",endpoint:"/api/account/devices/push/stats"}),this.addChild(this.statusChart),this.recentDeliveries=new i.TableView({containerId:"recent-deliveries",title:"Recent Deliveries",Collection:new i.PushDeliveryList({params:{_sort:"-created",_limit:5}}),columns:[{key:"title",label:"Title"},{key:"status",label:"Status",formatter:"badge"}]}),this.addChild(this.recentDeliveries),this.failedDeliveries=new i.TableView({containerId:"failed-deliveries",title:"Failed Deliveries",Collection:new i.PushDeliveryList({params:{status:"failed",_sort:"-created",_limit:5}}),columns:[{key:"title",label:"Title"},{key:"error_message",label:"Error"}]}),this.addChild(this.failedDeliveries)}}class PushDeliveryView extends t.View{constructor(e={}){super({className:"push-delivery-view",...e}),this.model=e.model}getTemplate(){return'\n <div class="p-3">\n <div class="phone-mockup">\n <div class="phone-screen">\n <div class="notification">\n <div class="notification-header">\n <i class="bi bi-app-indicator"></i>\n <strong>Your App</strong>\n <span class="ms-auto small text-muted">now</span>\n </div>\n <div class="notification-body">\n <div class="fw-bold">{{model.title}}</div>\n <div>{{model.body}}</div>\n </div>\n </div>\n </div>\n </div>\n <div class="mt-3">\n <h5>Delivery Details</h5>\n <p><strong>Status:</strong> <span class="badge {{model.status|badge}}">{{model.status}}</span></p>\n <p><strong>Error:</strong> {{model.error_message|default(\'None\')}}</p>\n </div>\n </div>\n '}}class PushDeliveryTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_deliveries",pageName:"Push Deliveries",router:"admin/push/deliveries",Collection:i.PushDeliveryList,itemViewClass:PushDeliveryView,viewDialogOptions:{header:!1,size:"md"},columns:[{key:"id",label:"ID",width:"70px"},{key:"created",label:"Timestamp",formatter:"datetime"},{key:"user.display_name",label:"User"},{key:"device.device_name",label:"Device"},{key:"title",label:"Title"},{key:"category",label:"Category"},{key:"status",label:"Status",formatter:"badge"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No deliveries found.",emptyIcon:"bi-send",actions:["view"]}})}}class PushDeviceView extends t.View{constructor(e={}){super({className:"push-device-view",...e}),this.model=e.model}getTemplate(){return'\n <div class="p-3">\n <h3>{{model.device_name}}</h3>\n <p class="text-muted">{{model.user.display_name}}</p>\n <div data-container="data-view"></div>\n </div>\n '}onInit(){this.dataView=new n.default({containerId:"data-view",model:this.model,fields:[{name:"platform",label:"Platform",format:"badge"},{name:"push_enabled",label:"Push Enabled",format:"boolean"},{name:"app_version",label:"App Version"},{name:"os_version",label:"OS Version"},{name:"last_seen",label:"Last Seen",format:"datetime"},{name:"push_preferences",label:"Preferences",format:"json"}]}),this.addChild(this.dataView)}}class PushDeviceTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_devices",pageName:"Registered Devices",router:"admin/push/devices",Collection:i.PushDeviceList,itemViewClass:PushDeviceView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"id",label:"ID",width:"70px"},{key:"user.display_name",label:"User"},{key:"device_name",label:"Device Name"},{key:"platform",label:"Platform",formatter:"badge"},{key:"app_version",label:"App Version"},{key:"push_enabled",label:"Push Enabled",format:"boolean"},{key:"last_seen",label:"Last Seen",formatter:"datetime"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No devices found.",emptyIcon:"bi-phone",actions:["view","delete"]}})}}class PushTemplateTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_push_templates",pageName:"Push Templates",router:"admin/push/templates",Collection:i.PushTemplateList,formCreate:i.PushTemplateForms.create,formEdit:i.PushTemplateForms.edit,columns:[{key:"id",label:"ID",width:"70px"},{key:"name",label:"Name"},{key:"category",label:"Category"},{key:"group.name",label:"Group",formatter:"default('Default')"},{key:"priority",label:"Priority"},{key:"is_active",label:"Active",format:"boolean"}],searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No push templates found.",emptyIcon:"bi-file-earmark-text",actions:["edit","delete"]}})}}class RuleSetView extends t.View{constructor(e={}){super({className:"ruleset-view",...e}),this.model=e.model||new i.RuleSet(e.data||{}),this.template='\n <div class="ruleset-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary"><i class="bi bi-gear-wide-connected"></i></div>\n <div>\n <h3 class="mb-1">{{model.name}}</h3>\n <div class="text-muted small">Category: {{model.category}} | Priority: {{model.priority}}</div>\n </div>\n </div>\n <div data-container="ruleset-context-menu"></div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="ruleset-tabs"></div>\n </div>\n '}async onInit(){const t=this.model.get("match_by"),a=i.MatchByOptions.find(e=>e.value===t),s=a?a.label:String(t),l=this.model.get("bundle_by"),o=i.BundleByOptions.find(e=>e.value===l),r=o?o.label:String(l);this.configView=new n.default({model:this.model,className:"p-3",columns:2,fields:[{name:"name",label:"Name",cols:4},{name:"category",label:"Category",formatter:"badge",cols:4},{name:"is_active",label:"Is Active",formatter:"yesno_icon",cols:4},{name:"priority",label:"Priority",cols:4},{name:"id",label:"RuleSet ID",cols:4},{name:"match_by",label:"Match Logic",template:s,cols:4},{name:"bundle_by",label:"Bundle By",template:r,cols:4},{name:"bundle_minutes",label:"Bundle Minutes",cols:4},{name:"bundle_by_rule_set",label:"Bundle By Rule Set",formatter:"yesno_icon",cols:4},{name:"handler",label:"Handler",cols:12}]});const d=new i.RuleList({params:{parent:this.model.get("id")}});this.rulesView=new i.TableView({collection:d,hideActivePillNames:["parent"],columns:[{key:"id",label:"ID",width:"70px"},{key:"name",label:"Name"},{key:"field_name",label:"Field"},{key:"comparator",label:"Comparator",width:"120px"},{key:"value",label:"Value"},{key:"value_type",label:"Type",width:"100px"}],showAdd:!0,clickAction:"edit",actions:["edit","delete"],contextMenu:[{label:"Edit Rule",action:"edit",icon:"bi-pencil"},{label:"Duplicate Rule",action:"duplicate",icon:"bi-files"},{divider:!0},{label:"Delete Rule",action:"delete",icon:"bi-trash",danger:!0}],addFormDefaults:{parent:this.model.get("id")}}),this.tabView=new i.TabView({containerId:"ruleset-tabs",tabs:{Configuration:this.configView,Rules:this.rulesView},activeTab:"Configuration"}),this.addChild(this.tabView);const c=new e.ContextMenu({containerId:"ruleset-context-menu",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit RuleSet",action:"edit-ruleset",icon:"bi-pencil"},{label:"Disable",action:"disable-ruleset",icon:"bi-toggle-off"},{type:"divider"},{label:"Delete RuleSet",action:"delete-ruleset",icon:"bi-trash",danger:!0}]}});this.addChild(c)}async onActionEditRuleset(){await a.Dialog.showModelForm({title:`Edit RuleSet - ${this.model.get("name")}`,model:this.model,formConfig:i.RuleSet.EDIT_FORM})&&await this.render()}async onActionDisableRuleset(){const e=!this.model.get("is_active");try{this.model.set("is_active",e),await this.model.save(),await this.render(),a.Dialog.showToast({message:`RuleSet ${e?"enabled":"disabled"} successfully`,type:"success"})}catch(t){a.Dialog.showToast({message:`Failed to update RuleSet: ${t.message}`,type:"error"})}}async onActionDeleteRuleset(){if(await a.Dialog.confirm({title:"Delete RuleSet",message:`Are you sure you want to delete the ruleset "${this.model.get("name")}"? This action cannot be undone.`,confirmText:"Delete",confirmClass:"btn-danger"}))try{await this.model.destroy(),a.Dialog.showToast({message:"RuleSet deleted successfully",type:"success"});const e=this.element?.closest(".modal");if(e){const t=bootstrap.Modal.getInstance(e);t&&t.hide()}this.emit("ruleset:deleted",{model:this.model})}catch(e){a.Dialog.showToast({message:`Failed to delete RuleSet: ${e.message}`,type:"error"})}}}RuleSetView.VIEW_CLASS=RuleSetView;class RuleSetTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_rulesets",pageName:"Rule Engine",router:"admin/rulesets",Collection:i.RuleSetList,itemView:RuleSetView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"name",label:"Name",sortable:!0},{key:"category",label:"Category",sortable:!0,formatter:"badge"},{key:"priority",label:"Priority",sortable:!0},{key:"match_by",label:"Match Logic",formatter:e=>0===e?"ALL":"ANY"}],selectable:!0,searchable:!0,sortable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,tableOptions:{pageSizes:[10,25,50],defaultPageSize:25,emptyMessage:"No rule sets found.",emptyIcon:"bi-gear",actions:["view","edit","delete"]}})}}class S3BucketTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_s3_buckets",pageName:"Manage S3 Buckets",router:"admin/s3-buckets",Collection:i.S3BucketList,formCreate:i.S3BucketForms.create,formEdit:i.S3BucketForms.edit,columns:[{key:"id",label:"ID",width:"60px",sortable:!0,class:"text-muted"},{key:"name",label:"Bucket Name",sortable:!0},{key:"created",label:"Created",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No S3 buckets found. Click "Add S3 Bucket" to create your first bucket.',batchBarLocation:"top",batchActions:[{label:"Delete",icon:"bi bi-trash",action:"batch-delete"},{label:"Export",icon:"bi bi-download",action:"batch-export"},{label:"Make Public",icon:"bi bi-unlock",action:"batch-public"},{label:"Make Private",icon:"bi bi-lock",action:"batch-private"},{label:"Empty Bucket",icon:"bi bi-bucket",action:"batch-empty"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class EmailView extends t.View{constructor(e={}){super({className:"email-view",...e}),this.model=e.model||new i.SentMessage(e.data||{}),this.hasHtml=!!this.model.get("body_html"),this.hasText=!!this.model.get("body_text"),this.hasContext=this.model.get("template_context")&&Object.keys(this.model.get("template_context")).length>0,this.template='\n <div class="email-view-container p-3">\n \x3c!-- Email Header --\x3e\n <div class="email-header border-bottom pb-3 mb-3">\n <h4 class="mb-2">{{model.subject}}</h4>\n <div class="d-flex justify-content-between align-items-center">\n <div class="d-flex align-items-center">\n <div class="flex-shrink-0">\n <i class="bi bi-person-circle fs-2 text-secondary"></i>\n </div>\n <div class="ms-3">\n <div class="fw-bold">{{model.mailbox.email}}</div>\n <div class="text-muted small">To: {{model.to_addresses|list}}</div>\n </div>\n </div>\n <div class="text-end">\n <div class="text-muted small">{{model.created|datetime}}</div>\n <span class="badge {{model.status|badge}} mt-1">{{model.status|capitalize}}</span>\n </div>\n </div>\n </div>\n\n \x3c!-- Email Body Tabs --\x3e\n <div data-container="email-tabs"></div>\n </div>\n '}async onInit(){const e={};this.hasHtml&&(e.HTML=new t.View({model:this.model,template:'<div class="email-html-content border rounded p-3" style="height: 500px; overflow-y: auto;">{{{model.body_html}}}</div>'})),this.hasText&&(e.Text=new t.View({model:this.model,template:'<pre class="email-text-content p-3 bg-light border rounded" style="white-space: pre-wrap; word-wrap: break-word;">{{model.body_text}}</pre>'})),this.hasContext&&(e.Context=new t.View({model:this.model,template:'<pre class="email-context-content p-3 bg-light border rounded"><code>{{model.template_context|json}}</code></pre>'})),this.tabView=new i.TabView({containerId:"email-tabs",tabs:e,activeTab:this.hasHtml?"HTML":this.hasText?"Text":"Context"}),this.addChild(this.tabView)}}i.SentMessage.VIEW_CLASS=EmailView;class SentMessageTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_email_sent",pageName:"Sent Messages",router:"admin/email/sent",Collection:i.SentMessageList,itemViewClass:EmailView,viewDialogOptions:{header:!1,size:"xl",scrollable:!0},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"mailbox.email",label:"From",sortable:!0},{key:"to_addresses",label:"To",sortable:!1,formatter:"list"},{key:"subject",label:"Subject",sortable:!0},{key:"status",label:"Status",formatter:"badge"},{key:"status_reason",label:"Reason",formatter:"truncate(80)|default('—')"},{key:"created",label:"Created",formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No sent messages found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class TaskDetailsView extends t.View{constructor(e={}){super({...e,className:"mojo-task-details-view"}),this.task=e.task||null,this.logs=[],this.metrics=null}async getTemplate(){return'\n <div class="mojo-task-details-container">\n {{#task}}\n \x3c!-- Task Overview --\x3e\n <div class="card border-0 bg-light mb-3">\n <div class="card-body">\n <div class="row">\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Task ID</label>\n <div class="font-monospace">{{id}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Function</label>\n <div>{{function}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Channel</label>\n <div>\n <span class="badge bg-primary">{{channel}}</span>\n </div>\n </div>\n </div>\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Status</label>\n <div>\n <span class="badge {{statusBadgeClass}} fs-6">\n <i class="bi {{statusIcon}}"></i> {{status|uppercase}}\n </span>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Created</label>\n <div>{{created|datetime}}</div>\n </div>\n {{#completed_at}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Completed</label>\n <div>{{completed_at|datetime}}</div>\n </div>\n {{/completed_at}}\n {{#expires}}\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Expires</label>\n <div class="{{expiresClass}}">{{expires|datetime}}</div>\n </div>\n {{/expires}}\n </div>\n </div>\n </div>\n </div>\n\n \x3c!-- Task Data --\x3e\n {{#data}}\n <div class="card mb-3">\n <div class="card-header py-2">\n <h6 class="mb-0">\n <i class="bi bi-database me-2"></i>Task Data\n </h6>\n </div>\n <div class="card-body">\n <pre class="bg-light p-3 rounded mb-0"><code>{{dataFormatted}}</code></pre>\n </div>\n </div>\n {{/data}}\n\n \x3c!-- Error Information --\x3e\n {{#error}}\n <div class="card border-danger mb-3">\n <div class="card-header bg-danger-subtle py-2">\n <h6 class="mb-0 text-danger">\n <i class="bi bi-exclamation-triangle me-2"></i>Error Details\n </h6>\n </div>\n <div class="card-body">\n <div class="alert alert-danger mb-0">\n <strong>Error:</strong> {{error}}\n </div>\n {{#errorDetails}}\n <div class="mt-3">\n <label class="form-label fw-bold small">Stack Trace:</label>\n <pre class="bg-light p-3 rounded small mb-0"><code>{{errorDetails}}</code></pre>\n </div>\n {{/errorDetails}}\n </div>\n </div>\n {{/error}}\n\n \x3c!-- Performance Metrics --\x3e\n {{#metrics}}\n <div class="card mb-3">\n <div class="card-header py-2">\n <h6 class="mb-0">\n <i class="bi bi-speedometer2 me-2"></i>Performance Metrics\n </h6>\n </div>\n <div class="card-body">\n <div class="row">\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-primary">{{executionTime}}ms</div>\n <small class="text-muted">Execution Time</small>\n </div>\n </div>\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-info">{{memoryUsage}}MB</div>\n <small class="text-muted">Memory Usage</small>\n </div>\n </div>\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-warning">{{cpuUsage}}%</div>\n <small class="text-muted">CPU Usage</small>\n </div>\n </div>\n <div class="col-md-3 col-6 mb-3">\n <div class="text-center">\n <div class="h5 mb-1 text-secondary">{{retryCount}}</div>\n <small class="text-muted">Retry Count</small>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/metrics}}\n\n \x3c!-- Task Logs --\x3e\n <div class="card">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-journal-text me-2"></i>Task Logs\n </h6>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-logs">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body p-0">\n <div class="mojo-task-logs-container" style="max-height: 300px; overflow-y: auto;">\n {{#logs.length}}\n {{#logs}}\n <div class="log-entry p-3 border-bottom">\n <div class="d-flex justify-content-between align-items-start">\n <div class="log-message flex-grow-1">\n <span class="badge bg-{{levelClass}} me-2">{{level|uppercase}}</span>\n {{message}}\n </div>\n <small class="text-muted ms-3">{{timestamp|time}}</small>\n </div>\n </div>\n {{/logs}}\n {{/logs.length}}\n {{^logs.length}}\n <div class="text-center text-muted py-5">\n <i class="bi bi-journal fs-1 opacity-50"></i>\n <p class="mb-0 mt-2">No logs available for this task</p>\n </div>\n {{/logs.length}}\n </div>\n </div>\n </div>\n {{/task}}\n\n {{^task}}\n <div class="alert alert-warning">\n <i class="bi bi-exclamation-triangle me-2"></i>\n No task data available\n </div>\n {{/task}}\n </div>\n '}async onInit(){this.task&&(await this.prepareTaskData(),await this.loadTaskLogs(),await this.loadTaskMetrics())}async prepareTaskData(){this.task&&(this.task.statusBadgeClass=this.getStatusBadgeClass(this.task.status),this.task.statusIcon=this.getStatusIcon(this.task.status),this.task.data&&"object"==typeof this.task.data&&(this.task.dataFormatted=JSON.stringify(this.task.data,null,2)),this.task.expires&&(this.task.expiresClass=1e3*this.task.expires<Date.now()?"text-danger":"text-muted"))}getStatusBadgeClass(e){return{pending:"bg-primary",running:"bg-success",completed:"bg-info",error:"bg-danger",cancelled:"bg-secondary",expired:"bg-warning"}[e]||"bg-secondary"}getStatusIcon(e){return{pending:"bi-hourglass",running:"bi-arrow-repeat",completed:"bi-check-circle",error:"bi-x-octagon",cancelled:"bi-x-circle",expired:"bi-clock"}[e]||"bi-question-circle"}getLogLevelClass(e){return{debug:"secondary",info:"primary",warning:"warning",error:"danger"}[e]||"secondary"}async loadTaskLogs(){if(this.task?.id)try{const e=await this.getApp().rest.GET(`/api/tasks/${this.task.id}/logs`);e.success&&e.data.status?this.logs=e.data.data.map(e=>({...e,levelClass:this.getLogLevelClass(e.level)})):this.logs=[]}catch(e){console.error("Failed to load task logs:",e),this.logs=[]}}async loadTaskMetrics(){if(this.task?.id)try{const e=await this.getApp().rest.GET(`/api/tasks/${this.task.id}/metrics`);e.success&&e.data.status&&(this.metrics=e.data.data)}catch(e){console.error("Failed to load task metrics:",e),this.metrics=null}}async setTask(e){this.task=e,await this.prepareTaskData(),await this.loadTaskLogs(),await this.loadTaskMetrics(),this.isMounted()&&await this.render()}async onActionRefreshLogs(e,t,a){if(this.task?.id)try{a.disabled=!0;const e=a.querySelector("i");e&&e.classList.add("spinning"),await this.loadTaskLogs(),await this.render()}catch(s){console.error("Failed to refresh logs:",s),this.showError("Failed to refresh logs: "+s.message)}finally{a.disabled=!1;const e=a.querySelector("i");e&&e.classList.remove("spinning")}}static async show(e,t={}){const a=new TaskDetailsView({task:e});await a.onInit();const s=[];return["pending","running"].includes(e.status)&&s.push({text:"Cancel Task",class:"btn-outline-danger",action:async()=>{if(confirm("Are you sure you want to cancel this task?"))try{const t=await a.getApp().rest.POST(`/api/tasks/${e.id}/cancel`);if(t.success&&t.data.status)return a.showSuccess("Task cancelled successfully"),{action:"cancelled",task:e};a.showError(t.data.error||"Failed to cancel task")}catch(t){a.showError("Failed to cancel task: "+t.message)}return null}}),"error"===e.status&&s.push({text:"Retry Task",class:"btn-outline-primary",action:async()=>{try{const t=await a.getApp().rest.POST(`/api/tasks/${e.id}/retry`);if(t.success&&t.data.status)return a.showSuccess("Task queued for retry"),{action:"retried",task:e};a.showError(t.data.error||"Failed to retry task")}catch(t){a.showError("Failed to retry task: "+t.message)}return null}}),s.push({text:"Clone Task",class:"btn-outline-info",action:async()=>{try{const t=await a.getApp().rest.POST(`/api/tasks/${e.id}/clone`);if(t.success&&t.data.status)return a.showSuccess("Task cloned successfully"),{action:"cloned",originalTask:e,newTask:t.data.data};a.showError(t.data.error||"Failed to clone task")}catch(t){a.showError("Failed to clone task: "+t.message)}return null}}),s.push({text:"Export",class:"btn-outline-secondary",action:()=>{try{const t={task:e,logs:a.logs,metrics:a.metrics,exported_at:/* @__PURE__ */(new Date).toISOString(),exported_by:"task-management-system"},s=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),i=URL.createObjectURL(s),n=document.createElement("a");return n.href=i,n.download=`task-${e.id}-${Date.now()}.json`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(i),a.showSuccess("Task data exported successfully"),null}catch(t){return a.showError("Failed to export task data"),null}}}),s.push({text:"Close",class:"btn-secondary",dismiss:!0}),await Dialog.showDialog({title:`<i class="bi bi-info-circle me-2"></i>Task Details - ${e.id}`,body:a,size:"lg",scrollable:!0,buttons:s,...t})}}class RunnerDetailsView extends t.View{constructor(e={}){super({...e,className:"mojo-runner-details-view"}),this.runner=e.runner||null,this.currentTasks=[],this.logs=[],this.metrics=null,this.config=null}async getTemplate(){return'\n <div class="mojo-runner-details-container">\n {{#runner}}\n \x3c!-- Runner Overview --\x3e\n <div class="card border-0 bg-light mb-3">\n <div class="card-body">\n <div class="row">\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Hostname</label>\n <div class="h5 mb-0 font-monospace">{{hostname}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Status</label>\n <div>\n <span class="badge {{statusBadgeClass}} fs-6">\n <i class="bi {{statusIcon}}"></i> {{status|uppercase}}\n </span>\n </div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Workers</label>\n <div class="h5 mb-0">\n {{#metrics.activeWorkers}}{{metrics.activeWorkers}}/{{/metrics.activeWorkers}}{{max_workers}} workers\n {{#metrics.workerUtilization}}\n <div class="progress mt-1" style="height: 6px;">\n <div class="progress-bar {{metrics.utilizationClass}}" style="width: {{metrics.workerUtilization}}%"></div>\n </div>\n {{/metrics.workerUtilization}}\n </div>\n </div>\n </div>\n <div class="col-md-6">\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Started</label>\n <div>{{started_at|datetime}}</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Last Ping</label>\n <div class="{{pingAgeClass}}">{{last_ping|datetime}} ({{pingAgeText}})</div>\n </div>\n <div class="mb-3">\n <label class="form-label fw-bold text-muted small">Uptime</label>\n <div>{{uptimeText}}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n \x3c!-- Channel Assignment --\x3e\n <div class="card mb-3">\n <div class="card-header py-2">\n <h6 class="mb-0">\n <i class="bi bi-collection me-2"></i>Assigned Channels\n </h6>\n </div>\n <div class="card-body">\n {{#channels.length}}\n <div class="d-flex flex-wrap gap-2">\n {{#channels}}\n <span class="badge bg-primary-subtle text-primary px-3 py-2">\n {{.}}\n </span>\n {{/channels}}\n </div>\n {{/channels.length}}\n {{^channels.length}}\n <div class="text-center text-muted py-3">\n <i class="bi bi-collection opacity-50"></i>\n <p class="mb-0 mt-2">No channels assigned</p>\n </div>\n {{/channels.length}}\n </div>\n </div>\n\n \x3c!-- Performance Metrics --\x3e\n {{#metrics}}\n <div class="card mb-3">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-speedometer2 me-2"></i>Performance Metrics\n </h6>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-metrics">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body">\n <div class="row">\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-success">{{tasksCompleted|number}}</div>\n <small class="text-muted">Tasks Completed</small>\n {{#tasksCompletedToday}}\n <div class="small text-success mt-1">+{{tasksCompletedToday}} today</div>\n {{/tasksCompletedToday}}\n </div>\n </div>\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-info">{{avgExecutionTime}}ms</div>\n <small class="text-muted">Avg Execution</small>\n <div class="small {{performanceTrend.class}} mt-1">\n <i class="bi {{performanceTrend.icon}}"></i> {{performanceTrend.text}}\n </div>\n </div>\n </div>\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-danger">{{errorCount|number}}</div>\n <small class="text-muted">Errors</small>\n <div class="small text-muted mt-1">{{errorRate}}% error rate</div>\n </div>\n </div>\n <div class="col-lg-3 col-md-6 mb-3">\n <div class="text-center p-3 bg-light rounded">\n <div class="h4 mb-1 text-warning">{{queueBacklog|number}}</div>\n <small class="text-muted">Queue Backlog</small>\n </div>\n </div>\n </div>\n\n \x3c!-- Resource Usage Bars --\x3e\n <div class="row mt-3">\n <div class="col-md-6 mb-2">\n <label class="form-label fw-bold small">CPU Usage</label>\n <div class="progress" style="height: 20px;">\n <div class="progress-bar {{cpu.class}}" style="width: {{cpu.percentage}}%">\n {{cpu.percentage}}%\n </div>\n </div>\n </div>\n <div class="col-md-6 mb-2">\n <label class="form-label fw-bold small">Memory Usage</label>\n <div class="progress" style="height: 20px;">\n <div class="progress-bar {{memory.class}}" style="width: {{memory.percentage}}%">\n {{memory.used}}MB / {{memory.total}}MB\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/metrics}}\n\n \x3c!-- Current Tasks --\x3e\n <div class="card mb-3">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-list-task me-2"></i>Current Tasks ({{currentTasks.length}})\n </h6>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-tasks">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body p-0">\n {{#currentTasks.length}}\n <div class="table-responsive">\n <table class="table table-sm table-hover mb-0">\n <thead class="table-light">\n <tr>\n <th class="border-0">Task ID</th>\n <th class="border-0">Function</th>\n <th class="border-0">Channel</th>\n <th class="border-0">Started</th>\n <th class="border-0">Duration</th>\n <th class="border-0 text-end">Actions</th>\n </tr>\n </thead>\n <tbody>\n {{#currentTasks}}\n <tr>\n <td class="font-monospace small">{{id|truncate(12)}}</td>\n <td>{{function}}</td>\n <td><span class="badge bg-primary-subtle text-primary">{{channel}}</span></td>\n <td>{{started|time}}</td>\n <td class="text-muted">{{duration}}</td>\n <td class="text-end">\n <div class="btn-group btn-group-sm">\n <button class="btn btn-outline-info" data-action="view-task" data-task-id="{{id}}" title="View Details">\n <i class="bi bi-eye"></i>\n </button>\n <button class="btn btn-outline-warning" data-action="cancel-task" data-task-id="{{id}}" title="Cancel">\n <i class="bi bi-x-circle"></i>\n </button>\n </div>\n </td>\n </tr>\n {{/currentTasks}}\n </tbody>\n </table>\n </div>\n {{/currentTasks.length}}\n {{^currentTasks.length}}\n <div class="text-center text-muted py-5">\n <i class="bi bi-list-task fs-1 opacity-50"></i>\n <p class="mb-0 mt-2">No active tasks</p>\n </div>\n {{/currentTasks.length}}\n </div>\n </div>\n\n \x3c!-- Runner Logs --\x3e\n <div class="card">\n <div class="card-header py-2 d-flex justify-content-between align-items-center">\n <h6 class="mb-0">\n <i class="bi bi-journal-text me-2"></i>Runner Logs\n </h6>\n <div class="btn-group btn-group-sm">\n <button class="btn btn-outline-secondary active" data-action="filter-logs" data-level="all">All</button>\n <button class="btn btn-outline-primary" data-action="filter-logs" data-level="info">Info</button>\n <button class="btn btn-outline-warning" data-action="filter-logs" data-level="warning">Warning</button>\n <button class="btn btn-outline-danger" data-action="filter-logs" data-level="error">Error</button>\n <button class="btn btn-outline-primary" data-action="refresh-logs">\n <i class="bi bi-arrow-clockwise"></i>\n </button>\n </div>\n </div>\n <div class="card-body p-0">\n <div class="mojo-runner-logs-container" style="max-height: 300px; overflow-y: auto;">\n {{#logs.length}}\n {{#logs}}\n <div class="log-entry p-3 border-bottom" data-level="{{level}}">\n <div class="d-flex justify-content-between align-items-start">\n <div class="log-message flex-grow-1">\n <span class="badge bg-{{levelClass}} me-2">{{level|uppercase}}</span>\n {{message}}\n </div>\n <small class="text-muted ms-3">{{timestamp|time}}</small>\n </div>\n </div>\n {{/logs}}\n {{/logs.length}}\n {{^logs.length}}\n <div class="text-center text-muted py-5">\n <i class="bi bi-journal fs-1 opacity-50"></i>\n <p class="mb-0 mt-2">No logs available</p>\n </div>\n {{/logs.length}}\n </div>\n </div>\n </div>\n {{/runner}}\n\n {{^runner}}\n <div class="alert alert-warning">\n <i class="bi bi-exclamation-triangle me-2"></i>\n No runner data available\n </div>\n {{/runner}}\n </div>\n '}async onInit(){this.runner&&(await this.prepareRunnerData(),await this.loadRunnerMetrics(),await this.loadCurrentTasks(),await this.loadRunnerLogs())}async prepareRunnerData(){if(this.runner&&(this.runner.isActive="active"===this.runner.status,this.runner.statusBadgeClass=this.runner.isActive?"bg-success":"bg-warning",this.runner.statusIcon=this.runner.isActive?"bi-check-circle-fill":"bi-exclamation-triangle-fill",void 0!==this.runner.ping_age&&(this.runner.pingAgeText=this.formatDuration(this.runner.ping_age),this.runner.pingAgeClass=this.runner.ping_age>300?"text-danger":"text-muted"),this.runner.started_at)){const e=Date.now()/1e3-this.runner.started_at;this.runner.uptimeText=this.formatUptime(e)}}async loadRunnerMetrics(){if(this.runner?.hostname)try{const e=await this.getApp().rest.GET(`/api/runners/${this.runner.hostname}/metrics`);if(e.success&&e.data.status){const t=e.data.data;this.metrics={activeWorkers:t.activeWorkers||0,tasksCompleted:t.tasksCompleted||0,tasksCompletedToday:t.tasksCompletedToday||0,avgExecutionTime:t.avgExecutionTime||0,errorCount:t.errorCount||0,errorRate:t.errorRate||0,queueBacklog:t.queueBacklog||0,workerUtilization:Math.round(t.activeWorkers/this.runner.max_workers*100),utilizationClass:this.getUtilizationClass(t.activeWorkers/this.runner.max_workers),performanceTrend:this.getPerformanceTrend(t.avgExecutionTime||0),cpu:this.getResourceStatus(t.cpuUsage||0),memory:this.getMemoryStatus(t.memoryUsed||0,t.memoryTotal||1e3)}}}catch(e){console.error("Failed to load runner metrics:",e),this.metrics=this.getDefaultMetrics()}}async loadCurrentTasks(){if(this.runner?.hostname)try{const e=await this.getApp().rest.GET(`/api/runners/${this.runner.hostname}/tasks`);e.success&&e.data.status?this.currentTasks=e.data.data.map(e=>({...e,duration:this.formatDuration(Date.now()/1e3-e.started)})):this.currentTasks=[]}catch(e){console.error("Failed to load current tasks:",e),this.currentTasks=[]}}async loadRunnerLogs(){if(this.runner?.hostname)try{const e=await this.getApp().rest.GET(`/api/runners/${this.runner.hostname}/logs?limit=50`);e.success&&e.data.status?this.logs=e.data.data.map(e=>({...e,levelClass:this.getLogLevelClass(e.level)})):this.logs=[]}catch(e){console.error("Failed to load runner logs:",e),this.logs=[]}}getDefaultMetrics(){return{activeWorkers:0,tasksCompleted:0,tasksCompletedToday:0,avgExecutionTime:0,errorCount:0,errorRate:0,queueBacklog:0,workerUtilization:0,utilizationClass:"bg-secondary",performanceTrend:{class:"text-muted",icon:"bi-dash",text:"No data"},cpu:{percentage:0,class:"bg-secondary"},memory:{used:0,total:0,percentage:0,class:"bg-secondary"}}}getUtilizationClass(e){return e>.9?"bg-danger":e>.7?"bg-warning":e>.5?"bg-info":"bg-success"}getPerformanceTrend(e){return e<1e3?{class:"text-success",icon:"bi-arrow-up",text:"Excellent"}:e<5e3?{class:"text-warning",icon:"bi-arrow-right",text:"Good"}:{class:"text-danger",icon:"bi-arrow-down",text:"Slow"}}getResourceStatus(e){const t=Math.round(e);let a="bg-success";return t>80?a="bg-danger":t>60?a="bg-warning":t>40&&(a="bg-info"),{percentage:t,class:a}}getMemoryStatus(e,t){const a=Math.round(e/t*100);return{used:Math.round(e),total:Math.round(t),percentage:a,class:this.getResourceStatus(a).class}}getLogLevelClass(e){return{debug:"secondary",info:"primary",warning:"warning",error:"danger"}[e]||"secondary"}formatUptime(e){const t=Math.floor(e/86400),a=Math.floor(e%86400/3600),s=Math.floor(e%3600/60);return t>0?`${t}d ${a}h ${s}m`:a>0?`${a}h ${s}m`:`${s}m`}formatDuration(e){return e<60?`${Math.round(e)}s`:e<3600?`${Math.round(e/60)}m`:e<86400?`${Math.round(e/3600)}h`:`${Math.round(e/86400)}d`}async setRunner(e){this.runner=e,await this.prepareRunnerData(),await this.loadRunnerMetrics(),await this.loadCurrentTasks(),await this.loadRunnerLogs(),this.isMounted()&&await this.render()}async onActionRefreshMetrics(e,t,a){await this.loadRunnerMetrics(),await this.render()}async onActionRefreshTasks(e,t,a){await this.loadCurrentTasks(),await this.render()}async onActionRefreshLogs(e,t,a){await this.loadRunnerLogs(),await this.render()}async onActionFilterLogs(e,t,a){const s=a.getAttribute("data-level");this.element.querySelectorAll(".log-entry").forEach(e=>{"all"===s||e.getAttribute("data-level")===s?e.style.display="block":e.style.display="none"}),this.element.querySelectorAll('[data-action="filter-logs"]').forEach(e=>{e.classList.remove("active")}),a.classList.add("active")}async onActionViewTask(e,t,a){const s=a.getAttribute("data-task-id");this.emit("task:view",{taskId:s,runner:this.runner})}async onActionCancelTask(e,t,a){const s=a.getAttribute("data-task-id");if(confirm("Are you sure you want to cancel this task?"))try{const e=await this.getApp().rest.POST(`/api/tasks/${s}/cancel`);e.success&&e.data.status?(this.showSuccess("Task cancelled successfully"),await this.loadCurrentTasks(),await this.render(),this.emit("task:cancelled",{taskId:s,runner:this.runner})):this.showError(e.data.error||"Failed to cancel task")}catch(i){console.error("Failed to cancel task:",i),this.showError("Failed to cancel task: "+i.message)}}static async show(e,t={}){const s=new RunnerDetailsView({runner:e});await s.onInit();const i=[];return"active"===e.status?i.push({text:"Pause Runner",class:"btn-warning",action:async()=>{if(confirm(`Are you sure you want to pause runner "${e.hostname}"?`))try{const t=await s.getApp().rest.POST(`/api/runners/${e.hostname}/pause`);if(t.success&&t.data.status)return s.showSuccess("Runner paused successfully"),{action:"paused",runner:e};s.showError(t.data.error||"Failed to pause runner")}catch(t){s.showError("Failed to pause runner: "+t.message)}return null}}):i.push({text:"Restart Runner",class:"btn-success",action:async()=>{if(confirm(`Are you sure you want to restart runner "${e.hostname}"?`))try{const t=await s.getApp().rest.POST(`/api/runners/${e.hostname}/restart`);if(t.success&&t.data.status)return s.showSuccess("Runner restart initiated"),{action:"restarted",runner:e};s.showError(t.data.error||"Failed to restart runner")}catch(t){s.showError("Failed to restart runner: "+t.message)}return null}}),i.push({text:"Configure",class:"btn-outline-primary",action:()=>(s.emit("runner:configure",{runner:e}),null)}),i.push({text:"Remove Runner",class:"btn-outline-danger",action:async()=>{const t=`Are you sure you want to remove runner "${e.hostname}"? This action cannot be undone.`;if(confirm(t))try{const t=await s.getApp().rest.DELETE(`/api/runners/${e.hostname}`);if(t.success&&t.data.status)return s.showSuccess("Runner removed successfully"),{action:"removed",runner:e};s.showError(t.data.error||"Failed to remove runner")}catch(a){s.showError("Failed to remove runner: "+a.message)}return null}}),i.push({text:"Export",class:"btn-outline-secondary",action:()=>{try{const t={runner:e,metrics:s.metrics,currentTasks:s.currentTasks,logs:s.logs,exported_at:/* @__PURE__ */(new Date).toISOString(),exported_by:"task-management-system"},a=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),i=URL.createObjectURL(a),n=document.createElement("a");return n.href=i,n.download=`runner-${e.hostname}-${Date.now()}.json`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(i),s.showSuccess("Runner data exported successfully"),null}catch(t){return s.showError("Failed to export runner data"),null}}}),i.push({text:"Close",class:"btn-secondary",dismiss:!0}),await a.Dialog.showDialog({title:`<i class="bi bi-cpu me-2"></i>Runner Details - ${e.hostname}`,body:s,size:"xl",scrollable:!0,buttons:i,...t})}}class TaskStatsView extends t.View{constructor(e={}){super({...e,className:"mojo-task-stats-section"}),this.stats={pending:0,running:0,completed:0,errors:0}}async getTemplate(){return'\n <div class="mojo-task-stats-header mb-4">\n <div class="row">\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Pending Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.pending}}</h3>\n <span class="badge bg-primary-subtle text-primary">\n <i class="bi bi-clock"></i> Queued\n </span>\n </div>\n <div class="text-primary">\n <i class="bi bi-hourglass fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Running Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.running}}</h3>\n <span class="badge bg-success-subtle text-success">\n <i class="bi bi-play-circle"></i> Active\n </span>\n </div>\n <div class="text-success">\n <i class="bi bi-arrow-repeat fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Completed Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.completed}}</h3>\n <span class="badge bg-info-subtle text-info">\n <i class="bi bi-check-circle"></i> Done\n </span>\n </div>\n <div class="text-info">\n <i class="bi bi-check-square fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <div class="col-xl-3 col-lg-6 col-12 mb-3">\n <div class="card h-100 border-0 shadow-sm">\n <div class="card-body">\n <div class="d-flex justify-content-between align-items-start">\n <div>\n <h6 class="card-title text-muted mb-2">Failed Tasks</h6>\n <h3 class="mb-1 fw-bold">{{stats.errors}}</h3>\n <span class="badge bg-danger-subtle text-danger">\n <i class="bi bi-exclamation-circle"></i> Errors\n </span>\n </div>\n <div class="text-danger">\n <i class="bi bi-x-octagon fs-2"></i>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async loadStats(){try{const e=await this.getApp().rest.GET("/api/tasks/status");e.success&&e.data.status&&(this.stats=e.data.data)}catch(e){console.error("Failed to load task stats:",e)}}async onInit(){await this.loadStats()}}class TaskRunnersView extends t.View{constructor(e={}){super({...e,className:"mojo-task-runners-section"}),this.runners=[]}async getTemplate(){return'\n <div class="card border shadow-sm mb-4">\n <div class="card-header d-flex justify-content-between align-items-center">\n <h5 class="card-title mb-0">\n <i class="bi bi-cpu me-2"></i>Task Runners\n </h5>\n <button class="btn btn-sm btn-outline-primary" data-action="refresh-runners">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n </div>\n <div class="card-body">\n {{#runners.length}}\n <div class="mojo-task-runner-list">\n {{#runners}}\n <div class="mojo-task-runner-item p-3 mb-2 bg-light rounded">\n <div class="row align-items-center">\n <div class="col-md-8 col-lg-9">\n <div class="d-flex align-items-center">\n <div class="mojo-task-runner-status me-3">\n <span class="badge {{statusBadge}}">\n <i class="bi {{statusIcon}}"></i> {{status}}\n </span>\n </div>\n <div class="mojo-task-runner-info">\n <div class="mojo-task-runner-name">\n <strong>{{hostname}}</strong>\n {{#max_workers}}<span class="text-muted ms-2">• {{max_workers}} workers</span>{{/max_workers}}\n </div>\n <div class="mojo-task-runner-channels">\n <small class="text-muted">\n {{#channels.length}}Channels: {{#channels}}{{.}}{{^last}}, {{/last}}{{/channels}}{{/channels.length}}\n {{^channels.length}}No channels assigned{{/channels.length}}\n </small>\n </div>\n </div>\n </div>\n </div>\n <div class="col-md-4 col-lg-3">\n <div class="mojo-task-runner-actions d-flex justify-content-end align-items-center">\n <small class="text-muted me-2 d-none d-sm-inline">{{pingAgeText}}</small>\n <div class="dropdown">\n <button class="btn btn-sm btn-outline-secondary dropdown-toggle"\n data-bs-toggle="dropdown" aria-expanded="false">\n <i class="bi bi-three-dots-vertical"></i>\n </button>\n <ul class="dropdown-menu dropdown-menu-end">\n <li><button class="dropdown-item" data-action="view-runner-details" data-runner-id="{{id}}">\n <i class="bi bi-info-circle me-2"></i>View Details\n </button></li>\n {{#isActive}}\n <li><button class="dropdown-item text-warning" data-action="pause-runner" data-runner-id="{{id}}">\n <i class="bi bi-pause me-2"></i>Pause Runner\n </button></li>\n {{/isActive}}\n {{^isActive}}\n <li><button class="dropdown-item text-success" data-action="restart-runner" data-runner-id="{{id}}">\n <i class="bi bi-play me-2"></i>Restart Runner\n </button></li>\n {{/isActive}}\n <li><hr class="dropdown-divider"></li>\n <li><button class="dropdown-item text-danger" data-action="remove-runner" data-runner-id="{{id}}">\n <i class="bi bi-trash me-2"></i>Remove Runner\n </button></li>\n </ul>\n </div>\n </div>\n </div>\n </div>\n <div class="row mt-2 d-sm-none">\n <div class="col-12">\n <small class="text-muted">Last ping: {{pingAgeText}}</small>\n </div>\n </div>\n </div>\n {{/runners}}\n </div>\n {{/runners.length}}\n {{^runners.length}}\n <div class="text-center text-muted py-4">\n <i class="bi bi-cpu fs-1"></i>\n <p class="mt-2">No task runners found</p>\n </div>\n {{/runners.length}}\n </div>\n </div>\n '}async loadRunners(){try{const e=await this.getApp().rest.GET("/api/tasks/runners");e.success&&e.data.status&&(this.runners=e.data.data.map(e=>{const t="active"===e.status,a=e.ping_age||0;return{...e,isActive:t,statusBadge:t?"bg-success":"bg-warning",statusIcon:t?"bi-check-circle-fill":"bi-exclamation-triangle-fill",pingAgeText:this.formatPingAge(a)}}))}catch(e){console.error("Failed to load runners:",e)}}formatPingAge(e){return e<60?`${Math.round(e)}s ago`:e<3600?`${Math.round(e/60)}m ago`:`${Math.round(e/3600)}h ago`}async onInit(){await this.loadRunners()}async onActionRefreshRunners(e,t){await this.loadRunners()}async onActionViewRunnerDetails(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(s){const e=await RunnerDetailsView.show(s);e?.action&&(await this.loadRunners(),this.emit("runner:"+e.action,e))}}async onActionPauseRunner(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(s&&confirm(`Are you sure you want to pause runner "${s.hostname}"?`))try{t.disabled=!0;const e=await this.getApp().rest.POST(`/api/runners/${a}/pause`);e.success&&e.data.status?(this.showSuccess("Runner paused successfully"),await this.loadRunners()):this.showError(e.data.error||"Failed to pause runner")}catch(i){console.error("Failed to pause runner:",i),this.showError("Failed to pause runner: "+i.message)}finally{t.disabled=!1}}async onActionRestartRunner(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(s&&confirm(`Are you sure you want to restart runner "${s.hostname}"?`))try{t.disabled=!0;const e=await this.getApp().rest.POST(`/api/runners/${a}/restart`);e.success&&e.data.status?(this.showSuccess("Runner restart initiated"),await this.loadRunners()):this.showError(e.data.error||"Failed to restart runner")}catch(i){console.error("Failed to restart runner:",i),this.showError("Failed to restart runner: "+i.message)}finally{t.disabled=!1}}async onActionRemoveRunner(e,t){const a=t.getAttribute("data-runner-id"),s=this.runners.find(e=>e.id===a);if(!s)return;const i=`Are you sure you want to remove runner "${s.hostname}"? This action cannot be undone.`;if(confirm(i))try{t.disabled=!0;const e=await this.getApp().rest.DELETE(`/api/runners/${a}`);e.success&&e.data.status?(this.showSuccess("Runner removed successfully"),await this.loadRunners()):this.showError(e.data.error||"Failed to remove runner")}catch(n){console.error("Failed to remove runner:",n),this.showError("Failed to remove runner: "+n.message)}finally{t.disabled=!1}}}class TaskChartsView extends t.View{constructor(e={}){super({...e,className:"mojo-task-charts-section"})}async getTemplate(){return'\n <div class="row mb-4">\n <div class="col-xl-6 col-lg-12 mb-4">\n <div class="card border shadow-sm">\n <div class="card-body" style="min-height: 300px;">\n <div data-container="task-flow-chart"></div>\n </div>\n </div>\n </div>\n <div class="col-xl-6 col-lg-12 mb-4">\n <div class="card border shadow-sm">\n <div class="card-body" style="min-height: 300px;">\n <div data-container="task-errors-chart"></div>\n </div>\n </div>\n </div>\n </div>\n '}async onInit(){this.taskFlowChart=new s.MetricsChart({title:'<i class="bi bi-graph-up me-2"></i>Task Flow',endpoint:"/api/metrics/fetch",height:280,granularity:"hours",slugs:["tasks_pub","tasks_completed"],account:"global",chartType:"line",showDateRange:!1,colors:["rgba(13, 110, 253, 0.8)","rgba(25, 135, 84, 0.8)"],yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"task-flow-chart"}),this.addChild(this.taskFlowChart),this.taskErrorsChart=new s.MetricsChart({title:'<i class="bi bi-exclamation-triangle me-2"></i>Task Issues',endpoint:"/api/metrics/fetch",height:280,granularity:"hours",slugs:["tasks_errors","tasks_expired"],account:"global",chartType:"line",showDateRange:!1,colors:["rgba(220, 53, 69, 0.8)","rgba(255, 193, 7, 0.8)"],yAxis:{label:"Count",beginAtZero:!0},tooltip:{y:"number"},containerId:"task-errors-chart"}),this.addChild(this.taskErrorsChart)}}class PendingTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Pending Tasks",collection:new l.Collection({endpoint:"/api/tasks/pending"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"status",label:"Status",formatter:"badge"}]})}}class RunningTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Running Tasks",collection:new l.Collection({endpoint:"/api/tasks/running"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"status",label:"Status",formatter:"badge"}]})}}class CompletedTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Completed Tasks",collection:new l.Collection({endpoint:"/api/tasks/completed"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"completed_at",label:"Completed",sortable:!0,formatter:"datetime"},{key:"status",label:"Status",formatter:"badge"}]})}}class ErrorTasksTable extends i.TableView{constructor(e={}){super({...e,title:"Failed Tasks",collection:new l.Collection({endpoint:"/api/tasks/errors"}),columns:[{key:"id",label:"Task ID",sortable:!0},{key:"function",label:"Function",sortable:!0},{key:"channel",label:"Channel",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"},{key:"error",label:"Error",sortable:!1},{key:"status",label:"Status",formatter:"badge"}]})}}class TaskManagementPage extends e.Page{constructor(e={}){super({...e,title:"Task Management",className:"mojo-task-management-page"}),this.pageTitle="Task Management",this.pageSubtitle="Async task monitoring and runner management",this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString()}async getTemplate(){return'\n <div class="mojo-task-management-container">\n \x3c!-- Page Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n <div>\n <h1 class="h3 mb-1">{{pageTitle}}</h1>\n <p class="text-muted mb-0">{{pageSubtitle}}</p>\n <small class="text-info">\n <i class="bi bi-cpu me-1"></i>\n Real-time task processing and runner monitoring\n </small>\n </div>\n <div class="btn-group" role="group">\n <button type="button" class="btn btn-outline-secondary btn-sm"\n data-action="refresh-all" title="Refresh All Data">\n <i class="bi bi-arrow-clockwise"></i> Refresh\n </button>\n <button type="button" class="btn btn-outline-primary btn-sm"\n data-action="export-tasks" title="Export Task Data">\n <i class="bi bi-download"></i> Export\n </button>\n <button type="button" class="btn btn-outline-success btn-sm"\n data-action="manage-channels" title="Manage Channels">\n <i class="bi bi-collection"></i> Channels\n </button>\n </div>\n </div>\n\n \x3c!-- Task Stats --\x3e\n <div data-container="task-stats"></div>\n\n \x3c!-- Task Runners --\x3e\n <div data-container="task-runners"></div>\n\n \x3c!-- Task Charts --\x3e\n <div data-container="task-charts"></div>\n\n \x3c!-- Task Tables --\x3e\n <div class="card border shadow-sm">\n <div class="card-header">\n <h5 class="card-title mb-0">\n <i class="bi bi-list-task me-2"></i>Task Management\n </h5>\n </div>\n <div class="card-body">\n <div data-container="task-tables"></div>\n </div>\n </div>\n\n \x3c!-- System Status Footer --\x3e\n <div class="row mt-4">\n <div class="col-12">\n <div class="alert alert-info border-0" role="alert">\n <div class="d-flex align-items-center">\n <i class="bi bi-info-circle-fill me-2"></i>\n <div>\n <strong>Task System Status:</strong> Monitoring active.\n Last updated: <span class="text-muted">{{lastUpdated}}</span>\n </div>\n <div class="ms-auto">\n <button class="btn btn-sm btn-outline-info" data-action="view-system-logs">\n <i class="bi bi-journal-text"></i> View Logs\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n '}async onInit(){this.taskStatsView=new TaskStatsView({containerId:"task-stats"}),this.addChild(this.taskStatsView),this.taskRunnersView=new TaskRunnersView({containerId:"task-runners"}),this.addChild(this.taskRunnersView),this.taskChartsView=new TaskChartsView({containerId:"task-charts"}),this.addChild(this.taskChartsView),this.taskTablesView=new i.TabView({containerId:"task-tables",tabs:{Pending:new PendingTasksTable,Running:new RunningTasksTable,Completed:new CompletedTasksTable,Errors:new ErrorTasksTable},activeTab:"Pending"}),this.addChild(this.taskTablesView)}async onActionRefreshAll(e,t,a){try{const e=a.querySelector("i");e?.classList.add("bi-spin"),a.disabled=!0;const t=[this.taskStatsView?.loadStats(),this.taskRunnersView?.loadRunners(),this.taskChartsView?.taskFlowChart?.refresh(),this.taskChartsView?.taskErrorsChart?.refresh()].filter(Boolean),s=this.taskTablesView?.getActiveTab();if(s){const e=this.taskTablesView.getTab(s);e?.refresh&&t.push(e.refresh())}await Promise.allSettled(t),this.lastUpdated=/* @__PURE__ */(new Date).toLocaleString();const i=this.getApp()?.events;i&&i.emit("tasks:dashboard-refreshed",{page:this,timestamp:this.lastUpdated})}catch(s){console.error("Failed to refresh task dashboard:",s)}finally{const e=a.querySelector("i");e?.classList.remove("bi-spin"),a.disabled=!1}}async onActionExportTasks(e,t,a){try{await(this.taskChartsView?.taskFlowChart?.export("png")),await(this.taskChartsView?.taskErrorsChart?.export("png"));const e=this.taskTablesView?.getActiveTab();if(e){const t=this.taskTablesView.getTab(e);t?.exportToCSV&&t.exportToCSV()}}catch(s){console.error("Failed to export task data:",s)}}async onActionManageChannels(e,t){try{const e=await this.getApp().rest.GET("/api/tasks/channels");if(e.success&&e.data.status){const t=e.data.data.map(e=>`${e.name} (${e.pending} pending, ${e.running} running)`).join("\n");alert(`Task Channels:\n\n${t}\n\nFull channel management interface coming soon!`)}else this.showError("Failed to load channel information")}catch(a){console.error("Failed to load channels:",a);const e=this.getApp()?.router;e&&e.navigateTo("/admin/task-channels")}}async onActionViewSystemLogs(e,t){try{const e=await this.getApp().rest.GET("/api/tasks/logs?limit=50");if(e.success&&e.data.status){const t=e.data.data,a=t.slice(0,10).map(e=>`[${new Date(1e3*e.timestamp).toLocaleString()}] ${e.level.toUpperCase()}: ${e.message}`).join("\n");if(confirm(`Recent Task System Logs:\n\n${a}\n\n... and ${t.length-10} more entries.\n\nView full logs?`)){const e=this.getApp()?.router;e&&e.navigateTo("/admin/logs?filter=tasks")}}else{const e=this.getApp()?.router;e&&e.navigateTo("/admin/logs")}}catch(a){console.error("Failed to load system logs:",a);const e=this.getApp()?.router;e&&e.navigateTo("/admin/logs")}}async refreshDashboard(){return this.onActionRefreshAll(null,null,{disabled:!1,querySelector:()=>null})}getStats(){return this.taskStatsView?.stats||{}}getRunners(){return this.taskRunnersView?.runners||[]}getCharts(){return{taskFlow:this.taskChartsView?.taskFlowChart,taskErrors:this.taskChartsView?.taskErrorsChart}}}class TicketNoteAdapter{constructor(e){this.ticketId=e,this.collection=new i.TicketNoteList({params:{parent:this.ticketId,sort:"created",size:100}})}async fetch(){return await this.collection.fetch(),this.collection.models.map(e=>this.transform(e))}transform(e){return{id:e.get("id"),type:"user_comment",author:{id:e.get("user.id"),name:e.get("user.display_name")||"System",avatarUrl:e.get("user.avatar.url")},timestamp:e.get("created"),content:e.get("note"),attachments:e.get("media")?[e.get("media")]:[]}}async addNote(e){const t=new i.TicketNote,a=await t.save({parent:this.ticketId,note:e.text,media:e.files&&e.files.length>0?e.files[0].id:null});return a.success&&await this.collection.fetch(),a}}class TicketView extends t.View{constructor(e={}){super({className:"ticket-view",...e}),this.model=e.model||new i.Ticket(e.data||{}),this.template='\n <div class="ticket-view-container d-flex flex-column h-100">\n \x3c!-- Ticket Header --\x3e\n <div class="d-flex justify-content-between align-items-start mb-3 flex-shrink-0">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="avatar-placeholder rounded-circle bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi bi-ticket-perforated text-secondary" style="font-size: 40px;"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.title|truncate(50)|default(\'Untitled Ticket\')}}</h3>\n <div class="text-muted small">\n <span>Ticket #{{model.id}}</span>\n <span class="mx-2">|</span>\n <span>Priority: {{model.priority|capitalize}}</span>\n {{#model.assignee}}\n <span class="mx-2">|</span>\n <span>Assigned to: {{model.assignee.display_name}}</span>\n {{/model.assignee}}\n </div>\n {{#model.incident}}\n <div class="text-muted small mt-1">\n <i class="bi bi-exclamation-triangle"></i> Related to incident: {{model.incident}}\n </div>\n {{/model.incident}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <span class="badge {{model.status|badgeClass}}">{{model.status|capitalize}}</span>\n </div>\n {{#model.created}}\n <div class="text-muted small mt-1">Created {{model.created|relative}}</div>\n {{/model.created}}\n {{#model.modified}}\n <div class="text-muted small">Updated {{model.modified|relative}}</div>\n {{/model.modified}}\n </div>\n <div data-container="ticket-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Chat View (Full height) --\x3e\n <div class="flex-grow-1" style="min-height: 0;" data-container="chat-view"></div>\n </div>\n '}async onInit(){const t=new TicketNoteAdapter(this.model.get("id"));this.chatView=new i.ChatView({containerId:"chat-view",adapter:t,theme:"compact",currentUserId:this.getCurrentUserId(),inputPlaceholder:"Add a note...",inputButtonText:"Add Note"}),this.addChild(this.chatView);const a=new e.ContextMenu({containerId:"ticket-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit Ticket",action:"edit-ticket",icon:"bi-pencil"},{label:"Change Status",action:"change-status",icon:"bi-tag"},{label:"Set Priority",action:"set-priority",icon:"bi-flag"},{label:"Assign User",action:"assign-user",icon:"bi-person"},{type:"divider"},{label:"Close Ticket",action:"close-ticket",icon:"bi-x-circle"}]}});this.addChild(a)}getCurrentUserId(){const e=window.app?.state?.user;return e?.id||null}async onActionEditTicket(){await a.Dialog.showModelForm({title:`Edit Ticket #${this.model.get("id")} - ${this.model.get("title")}`,model:this.model,size:"lg",fields:i.TicketForms.edit.fields})&&this.render()}async onActionChangeStatus(){const e=this.model.get("status"),t=await a.Dialog.showForm({title:"Change Ticket Status",size:"sm",fields:[{name:"status",label:"New Status",type:"select",options:["new","open","in_progress","pending","resolved","closed","ignored"].map(e=>({value:e,label:e.replace("_"," ").toUpperCase()})),value:e,required:!0}]});if(t)try{await this.model.save({status:t.status}),this.render()}catch(s){a.Dialog.alert({type:"error",title:"Error",message:"Failed to update ticket status: "+s.message})}}async onActionSetPriority(){const e=this.model.get("priority"),t=await a.Dialog.showForm({title:"Set Ticket Priority",size:"sm",fields:[{name:"priority",label:"Priority Level",type:"select",options:["low","normal","high","urgent"].map(e=>({value:e,label:e.toUpperCase()})),value:e,required:!0}]});if(t)try{await this.model.save({priority:t.priority}),this.render()}catch(s){a.Dialog.alert({type:"error",title:"Error",message:"Failed to update ticket priority: "+s.message})}}async onActionAssignUser(){a.Dialog.alert({title:"Coming Soon",message:"User assignment feature will be implemented soon."})}async onActionCloseTicket(){if(await a.Dialog.confirm({title:"Close Ticket",message:`Are you sure you want to close ticket #${this.model.get("id")}?`,confirmText:"Close Ticket",confirmClass:"btn-warning"}))try{await this.model.save({status:"closed"}),this.render(),a.Dialog.alert({type:"success",title:"Success",message:"Ticket has been closed successfully."})}catch(e){a.Dialog.alert({type:"error",title:"Error",message:"Failed to close ticket: "+e.message})}}}i.Ticket.VIEW_CLASS=TicketView;class TicketTablePage extends i.TablePage{constructor(e={}){super({name:"admin_tickets",pageName:"Tickets",router:"admin/tickets",Collection:i.TicketList,formCreate:i.TicketForms.create,formEdit:i.TicketForms.edit,itemViewClass:TicketView,viewDialogOptions:{header:!1},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"title",label:"Title",sortable:!0},{key:"status",label:"Status",sortable:!0,editable:!0,editableOptions:{type:"select",options:["new","open","paused","resolved","qa","ignored"]}},{key:"priority",label:"Priority",sortable:!0},{key:"category",label:"Category",sortable:!0,editable:!0,editableOptions:{type:"select",options:[...Object.keys(i.TicketCategories)]}},{key:"assignee.display_name",label:"Assignee",sortable:!0,formatter:"default('Unassigned')"},{key:"incident.id",label:"Incident ID",sortable:!0},{key:"created",label:"Created",sortable:!0,formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:"No tickets found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},...e})}}class UserDeviceLocationTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_user_device_locations",pageName:"Device Locations",router:"admin/user/device-locations",Collection:e.UserDeviceLocationList,columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"user.display_name",label:"User",sortable:!0},{key:"user_device",label:"Device",template:"{{user_device.device_info.user_agent.family}} on {{user_device.device_info.os.family}}",sortable:!0},{key:"ip_address",label:"IP Address",sortable:!0},{key:"geolocation.city",label:"City",formatter:"default('—')"},{key:"geolocation.region",label:"Region",formatter:"default('—')"},{key:"geolocation.country_name",label:"Country",formatter:"default('—')"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No device locations found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class UserDeviceTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_user_devices",pageName:"User Devices",router:"admin/user/devices",Collection:e.UserDeviceList,itemViewClass:DeviceView,viewDialogOptions:{header:!1,size:"lg"},columns:[{key:"id",label:"ID",width:"70px",sortable:!0,class:"text-muted"},{key:"duid",label:"Device ID",sortable:!0,formatter:"truncate_middle(16)"},{key:"user.display_name",label:"User",sortable:!0,formatter:"default('—')"},{key:"device_info.user_agent.family",label:"Browser",formatter:"default('—')"},{key:"device_info.os.family",label:"OS",formatter:"default('—')"},{key:"last_ip",label:"Last IP",sortable:!0},{key:"first_seen",label:"First Seen",formatter:"epoch|datetime"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No user devices found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}class UserView extends t.View{constructor(t={}){super({className:"user-view",...t}),this.model=t.model||new e.User(t.data||{}),this.tabView=null,this.profileView=null,this.groupsView=null,this.eventsView=null,this.logsView=null,this.template='\n <div class="user-view-container">\n \x3c!-- User Header --\x3e\n <div data-container="user-header"></div>\n \x3c!-- Tab Container --\x3e\n <div data-container="user-tabs"></div>\n </div>\n '}async onInit(){this.header=new t.View({containerId:"user-header",template:'\n <div class="d-flex justify-content-between align-items-start mb-4">\n \x3c!-- Left Side: Primary Identity --\x3e\n <div class="d-flex align-items-center gap-3">\n {{{model.avatar|avatar(\'md\',\'rounded-circle\')}}}\n <div>\n <h3 class="mb-0">{{model.display_name|default(\'Unnamed User\')}}</h3>\n <a href="mailto:{{model.email}}" class="text-decoration-none text-body">{{model.email}}</a>{{{model.email|clipboard}}}\n {{#model.phone_number}}\n <div class="text-muted small mt-1">{{{model.phone_number|phone(false)}}}</div>\n {{/model.phone_number}}\n </div>\n </div>\n\n \x3c!-- Right Side: Status & Actions --\x3e\n <div class="d-flex align-items-start gap-4">\n <div class="text-end">\n <div class="d-flex align-items-center gap-2">\n <i class="bi bi-circle-fill fs-8 {{model.is_active|boolean(\'text-success\',\'text-secondary\')}}"></i>\n <span>{{model.is_active|boolean(\'Active\',\'Inactive\')}}</span>\n </div>\n {{#model.last_activity}}\n <div class="text-muted small mt-1">Last active {{model.last_activity|relative}}</div>\n {{/model.last_activity}}\n </div>\n <div data-container="user-context-menu"></div>\n </div>\n </div>'}),this.header.setModel(this.model),this.addChild(this.header),this.profileView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,fields:e.UserDataView.profile.fields}),this.permsView=new r.FormView({fields:e.User.PERMISSION_FIELDS,model:this.model,autosaveModelField:!0});const a=new i.MemberList({params:{user:this.model.get("id"),size:5}});this.groupsView=new i.TableView({collection:a,hideActivePillNames:["user"],columns:[{key:"created",label:"Date Joined",formatter:"date",sortable:!0},{key:"group.name",label:"Group Name",sortable:!0},{key:"permissions|keys|badge",label:"Permissions"}]});const s=new i.IncidentEventList({params:{size:5,model_name:"account.User",model_id:this.model.get("id")}});this.eventsView=new i.TableView({collection:s,hideActivePillNames:["model_name","model_id"],columns:[{key:"id",label:"ID",sortable:!0,width:"40px"},{key:"created",label:"Date",formatter:"datetime",sortable:!0,width:"150px"},{key:"category|badge",label:"Category"},{key:"title",label:"Title"}]});const l=new e.UserDeviceList({params:{size:5,user:this.model.get("id")}});this.devicesView=new i.TableView({collection:l,hideActivePillNames:["user"],columns:[{key:"duid|truncate_middle(16)",label:"Device ID",sortable:!0},{key:"device_info.user_agent.family",label:"Browser",formatter:"default('—')"},{key:"device_info.os.family",label:"OS",formatter:"default('—')"},{key:"first_seen",label:"First Seen",formatter:"epoch|datetime"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],size:5});const o=new e.UserDeviceLocationList({params:{size:5,user:this.model.get("id")}});this.locationsView=new i.TableView({collection:o,hideActivePillNames:["user"],columns:[{key:"user_device",label:"Device",template:"{{model.user_device.device_info.user_agent.family}} on {{model.user_device.device_info.os.family}}",sortable:!0},{key:"geolocation.city",label:"City",formatter:"default('—')"},{key:"geolocation.region",label:"Region",formatter:"default('—')"},{key:"geolocation.country_name",label:"Country",formatter:"default('—')"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],size:5});const d=new i.PushDeviceList({params:{size:5,user:this.model.get("id")}});this.pushDevicesView=new i.TableView({collection:d,hideActivePillNames:["user"],columns:[{key:"duid|truncate_middle(16)",label:"Device ID",sortable:!0},{key:"device_info.user_agent.family",label:"Browser",formatter:"default('—')"},{key:"device_info.os.family",label:"OS",formatter:"default('—')"},{key:"first_seen",label:"First Seen",formatter:"epoch|datetime"},{key:"last_seen",label:"Last Seen",formatter:"epoch|datetime"}],size:5});const c=new i.LogList({params:{size:5,model_name:"account.User",model_id:this.model.get("id")}});this.logsView=new i.TableView({collection:c,permissions:"view_logs",hideActivePillNames:["model_name","model_id"],columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"log",label:"Log"}]});const m=new i.LogList({params:{size:5,uid:this.model.get("id")}});this.activityView=new i.TableView({collection:m,hideActivePillNames:["uid"],permissions:"view_logs",columns:[{key:"created",label:"Timestamp",sortable:!0,formatter:"epoch|datetime",filter:{name:"created",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}},{key:"level",label:"Level",sortable:!0,filter:{type:"select",options:[{value:"info",label:"Info"},{value:"warning",label:"Warning"},{value:"error",label:"Error"}]}},{key:"kind",label:"Kind",filter:{type:"text"}},{name:"path",label:"Path"}]}),this.tabView=new i.TabView({tabs:{Profile:this.profileView,Permissions:this.permsView,Groups:this.groupsView,Events:this.eventsView,Logs:this.logsView,Activity:this.activityView,Devices:this.devicesView,Locations:this.locationsView,"Push Devices":this.pushDevicesView},activeTab:"Profile",containerId:"user-tabs",enableResponsive:!0,dropdownStyle:"select",minWidth:300}),this.addChild(this.tabView);const b=new e.ContextMenu({containerId:"user-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Edit User",action:"edit-user",icon:"bi-pencil"},{label:"Reset Password",action:"reset-password",icon:"bi-key"},{type:"divider"},this.model.get("is_active")?{label:"Deactivate User",action:"deactivate-user",icon:"bi-person-dash"}:{label:"Activate User",action:"activate-user",icon:"bi-person-check"}]}});this.addChild(b)}async onActionEditUser(){let t=e.UserForms.edit;await a.Dialog.showModelForm({title:`EDIT - #${this.model.id} ${this.options.modelName}`,model:this.model,formConfig:t})&&this.render()}async onActionResetPassword(){}async onActionDeactivateUser(){await a.Dialog.confirm("Are you sure you want to disable this user?")?(await this.model.save({is_active:!1}),this.getApp().toast.success("Member disable")):this.getApp().toast.error("Member disable failed")}async onActionActivateUser(){await a.Dialog.confirm("Are you sure you want to enable this user?")?(await this.model.save({is_active:!0}),this.getApp().toast.success("Member enabled")):this.getApp().toast.error("Member enable failed")}async onActionViewGroup(e,t,a){a.getAttribute("data-id")}async onActionRemoveFromGroup(e,t,a){a.getAttribute("data-id")}async onActionViewEvent(e,t,a){a.getAttribute("data-id")}async onActionViewLog(e,t,a){a.getAttribute("data-id")}async showTab(e){this.tabView&&await this.tabView.showTab(e)}getActiveTab(){return this.tabView?this.tabView.getActiveTab():null}_onModelChange(){}static create(e={}){return new UserView(e)}}e.User.VIEW_CLASS=UserView;class UserTablePage extends i.TablePage{constructor(t={}){super({...t,name:"admin_users",pageName:"Manage Users",router:"admin/users",Collection:e.UserList,viewDialogOptions:{header:!1},defaultQuery:{sort:"-last_activity",is_active:!0},columns:[{key:"id",label:"ID",sortable:!0,class:"text-muted"},{key:"display_name|tooltip:model.username",label:"Display Name"},{label:"Info",key:"permissions.manage_users",template:"\n {{^model.is_active}}<span class=\"text-danger\">DISABLED</span> {{/model.is_active}}\n {{#model.permissions.manage_users}}{{{model.permissions.manage_users|yesnoicon('bi bi-person-gear text-danger')|tooltip('Manage Users')}}} {{/model.permissions.manage_users}}\n {{#model.permissions.manage_groups}}{{{model.permissions.manage_groups|yesnoicon('bi bi-building-gear text-primary')|tooltip('Manage Groups')}}} {{/model.permissions.manage_groups}}\n {{#model.permissions.view_global}}{{{model.permissions.view_global|yesnoicon('bi bi-globe text-secondary')|tooltip('View Global Menu')}}} {{/model.permissions.view_global}}\n {{#model.permissions.view_admin}}{{{model.permissions.view_admin|yesnoicon('bi bi-wrench text-secondary')|tooltip('View Admin Menu')}}} {{/model.permissions.view_admin}}\n ",sortable:!1},{key:"email",label:"Email",visibility:"xl",className:"text-muted fs-8"},{key:"last_activity",label:"Last Activity",formatter:"relative",className:"text-muted fs-8"}],filters:[{key:"is_active",label:"Active",type:"boolean",defaultValue:!0},{key:"email",label:"Email",type:"text",defaultValue:""},{key:"username",label:"Username",type:"text",defaultValue:""},{key:"locations__ip_address",label:"IP Address",type:"text",defaultValue:""},{key:"last_activity",type:"daterange",startName:"dr_start",endName:"dr_end",fieldName:"dr_field",label:"Date Range",format:"YYYY-MM-DD",displayFormat:"MMM DD, YYYY",separator:" to "}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:'No users found. Click "Add" to create a new user.',contextMenu:[{icon:"bi-pencil",action:"edit",label:"Edit Profile"},{icon:"bi-shield-check",action:"edit-permissions",label:"Edit Permissions"},{icon:"bi-shield",action:"change-password",label:"Change Password"},{separator:!0},{icon:"bi-envelope",action:"send-invite",label:"Send Invite"}],tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}async onActionEditPermissions(t,s){t.preventDefault();const i=this.collection.get(s.dataset.id);await a.Dialog.showModelForm({model:i,size:"lg",title:`Edit Permissions for "${i._.username}"`,fields:e.UserForms.permissions.fields})}async onActionChangePassword(e,s){const i=this.collection.get(s.dataset.id),n=await a.Dialog.showForm({title:`Change Password for "${i._.username}"`,fields:[{type:"text",name:"username",value:i.get("email")||i.get("username"),attributes:{autocomplete:"username",readonly:"readonly",tabindex:"-1",style:"position: absolute; left: -9999px; opacity: 0; height: 0; width: 0;"}},{name:"new_password",label:"New Password",type:"password",passwordUsage:"new",required:!0,showToggle:!0,attributes:{autocomplete:"new-password"}}]});if(n&&n.new_password){if(t.MOJOUtils.checkPasswordStrength(n.new_password).score<5)return this.getApp().toast.error("Password must be at least 6 characters long and contain at least 2 of the following: uppercase letter, lowercase letter, or number"),void(await this.onActionChangePassword(e,s));const a=await i.save({new_password:n.new_password});this.onPasswordChange(a)||await this.onActionChangePassword(e,s)}}onPasswordChange(e){return e.success?(this.getApp().toast.success("Password changed successfully"),!0):(e.data&&e.data.error?this.getApp().toast.error(e.data.error):this.getApp().toast.error("Failed to change password"),!1)}async onActionSendInvite(e,t){const a=this.collection.get(t.dataset.id),s=await a.save({send_invite:!0});return s.success?(this.getApp().toast.success("Invite sent successfully"),!0):(s.data&&s.data.error?this.getApp().toast.error(s.data.error):this.getApp().toast.error("Failed to send invite"),!1)}}class PhoneNumber extends l.Model{constructor(e={},t={}){super(e,{endpoint:"/api/phonehub/number",...t})}static async normalize(e,a="US"){const s={phone_number:e};a&&(s.country_code=a);const i=await t.rest.POST("/api/phonehub/number/normalize",s),n=i?.data??i;return!0===n?.status||!0===n?.success?{success:!0,phone_number:n?.data?.phone_number??n?.phone_number,data:n?.data??n,response:i}:{success:!1,error:n?.error||"Normalization failed",response:i}}static async lookup(e,a={}){const s=await t.rest.POST("/api/phonehub/number/lookup",{phone_number:e,...a}),i=s?.data??s;if(!0===i?.status||!0===i?.success){const e=i?.data??{};return{success:!0,model:new PhoneNumber(e,{endpoint:"/api/phonehub/number"}),data:e,response:s}}return{success:!1,error:i?.error||"Phone lookup failed",response:s}}}class PhoneNumberList extends l.Collection{constructor(e={}){super({ModelClass:PhoneNumber,endpoint:"/api/phonehub/number",size:10,...e})}}class SMS extends l.Model{constructor(e={},t={}){super(e,{endpoint:"/api/phonehub/sms",...t})}static async send(e={}){const a=await t.rest.POST("/api/phonehub/sms/send",e),s=a?.data??a;if(!0===s?.status||!0===s?.success){const e=s?.data??{};return{success:!0,model:new SMS(e,{endpoint:"/api/phonehub/sms"}),data:e,response:a}}return{success:!1,error:s?.error||"Failed to send SMS",response:a}}}class SMSList extends l.Collection{constructor(e={}){super({ModelClass:SMS,endpoint:"/api/phonehub/sms",size:10,...e})}}class PhoneNumberView extends t.View{constructor(e={}){super({className:"phone-number-view",...e}),this.model=e.model||new PhoneNumber(e.data||{}),this.template='\n <div class="phone-number-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi bi-telephone"></i>\n </div>\n <div>\n <h3 class="mb-1">{{model.phone_number|default(\'Unknown Number\')}}</h3>\n <div class="text-muted small">\n {{model.carrier|default(\'—\')}} {{#model.line_type}}· {{model.line_type|capitalize}}{{/model.line_type}}\n </div>\n <div class="text-muted small mt-1">\n {{#model.country_code}}Country: {{model.country_code}}{{/model.country_code}}\n {{#model.region}} · Region: {{model.region}}{{/model.region}}\n {{#model.state}} · State: {{model.state}}{{/model.state}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div data-container="phone-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="phone-tabs"></div>\n </div>\n '}async onInit(){this.overviewView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"phone_number",label:"Phone Number",cols:6},{name:"country_code",label:"Country Code",cols:6},{name:"region",label:"Region",cols:6},{name:"state",label:"State",cols:6},{name:"registered_owner",label:"Registered Owner",cols:6},{name:"owner_type",label:"Owner Type",formatter:"capitalize",cols:6},{name:"is_valid",label:"Valid",formatter:"yesnoicon",cols:4},{name:"is_mobile",label:"Mobile",formatter:"yesnoicon",cols:4},{name:"is_voip",label:"VOIP",formatter:"yesnoicon",cols:4}]}),this.carrierView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"carrier",label:"Carrier",cols:6},{name:"line_type",label:"Line Type",formatter:"capitalize",cols:6},{name:"lookup_provider",label:"Lookup Provider",formatter:"capitalize",cols:6},{name:"lookup_count",label:"Lookup Count",cols:6},{name:"last_lookup_at",label:"Last Lookup",formatter:"datetime",cols:6},{name:"lookup_expires_at",label:"Cache Expires",formatter:"datetime",cols:6}]}),this.addressView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"address_line1",label:"Address Line 1",cols:12},{name:"address_city",label:"City",cols:4},{name:"address_state",label:"State",cols:4},{name:"address_zip",label:"ZIP",cols:4},{name:"address_country",label:"Country",cols:6}]}),this.metadataView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"Record ID",cols:6},{name:"created",label:"Created",formatter:"datetime",cols:6},{name:"modified",label:"Last Modified",formatter:"datetime",cols:6}]});const t={Overview:this.overviewView,Carrier:this.carrierView,Address:this.addressView,Metadata:this.metadataView};this.tabView=new i.TabView({containerId:"phone-tabs",tabs:t,activeTab:"Overview"}),this.addChild(this.tabView);const a=new e.ContextMenu({containerId:"phone-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Refresh Lookup",action:"refresh-lookup",icon:"bi-arrow-repeat"},{type:"divider"},{label:"Delete Record",action:"delete-phone",icon:"bi-trash",danger:!0}]}});this.addChild(a)}async onActionRefreshLookup(){const e=this.model.get("phone_number");if(e)try{this.getApp()?.toast?.info?.("Refreshing lookup...");const t=await PhoneNumber.lookup(e,{force_refresh:!0});if(t.success&&t.data)this.model.set(t.data),await this.render(),this.getApp()?.toast?.success?.("Lookup refreshed");else{const e=t.error||"Lookup failed";this.getApp()?.toast?.error?.(e)}}catch(t){this.getApp()?.toast?.error?.(t.message||"Lookup failed")}else this.getApp()?.toast?.warning?.("No phone number to lookup")}async onActionDeletePhone(){if(await a.Dialog.confirm(`Are you sure you want to delete the record for "${this.model.get("phone_number")||"this number"}"?`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"}))try{const e=await this.model.destroy();e?.success?this.emit("phone:deleted",{model:this.model}):this.getApp()?.toast?.error?.("Delete failed")}catch(e){this.getApp()?.toast?.error?.(e.message||"Delete failed")}}}PhoneNumberView.MODEL_CLASS=PhoneNumber;class PhoneNumberTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_phonehub_numbers",pageName:"Phone Numbers",router:"admin/phonehub/numbers",Collection:PhoneNumberList,itemView:PhoneNumberView,viewDialogOptions:{header:!1},columns:[{key:"phone_number",label:"Phone Number",sortable:!0},{key:"carrier",label:"Carrier",sortable:!0,formatter:"default('—')"},{key:"line_type",label:"Line Type",sortable:!0,formatter:"capitalize"},{key:"is_mobile",label:"Mobile",formatter:"yesnoicon"},{key:"is_voip",label:"VOIP",formatter:"yesnoicon"},{key:"is_valid",label:"Valid",formatter:"yesnoicon"},{key:"registered_owner",label:"Owner",sortable:!0,formatter:"default('—')"},{key:"owner_type",label:"Owner Type",formatter:"capitalize"},{key:"last_lookup_at|relative",label:"Last Lookup",sortable:!0}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,clickAction:"view",showRefresh:!0,showAdd:!0,showExport:!0,emptyMessage:"No phone numbers found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1},tableViewOptions:{addButtonLabel:"Lookup",addButtonIcon:"bi-search",onAdd:e=>{e.preventDefault(),this.onLookup()}}})}async onLookup(){const e=await this.getApp().showForm({title:"Lookup Phone Number",fields:[{name:"number",type:"text",required:!0}]});if(e&&e.number){const t=await PhoneNumber.lookup(e.number);t.model&&this.tableView._onRowView(t)}}}class SMSView extends t.View{constructor(e={}){super({className:"sms-view",...e}),this.model=e.model||new SMS(e.data||{}),this.template='\n <div class="sms-view-container">\n \x3c!-- Header --\x3e\n <div class="d-flex justify-content-between align-items-center mb-4">\n \x3c!-- Left Side: Icon & Info --\x3e\n <div class="d-flex align-items-center gap-3">\n <div class="fs-1 text-primary">\n <i class="bi bi-chat-dots"></i>\n </div>\n <div>\n <h3 class="mb-1">\n {{#model.direction}}{{model.direction|capitalize}}{{/model.direction}}\n {{^model.direction}}Message{{/model.direction}}\n <small class="text-muted ms-2">\n {{#model.status}}[{{model.status|capitalize}}]{{/model.status}}\n </small>\n </h3>\n <div class="text-muted small">\n {{#model.from_number}}From: {{model.from_number}}{{/model.from_number}}\n {{#model.to_number}} · To: {{model.to_number}}{{/model.to_number}}\n </div>\n <div class="text-muted small mt-1">\n {{#model.provider}}Provider: {{model.provider|capitalize}}{{/model.provider}}\n {{#model.provider_message_id}} · SID: {{model.provider_message_id}}{{/model.provider_message_id}}\n </div>\n </div>\n </div>\n\n \x3c!-- Right Side: Actions --\x3e\n <div class="d-flex align-items-center gap-4">\n <div data-container="sms-context-menu"></div>\n </div>\n </div>\n\n \x3c!-- Tabs --\x3e\n <div data-container="sms-tabs"></div>\n </div>\n '}async onInit(){this.messageView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"direction",label:"Direction",formatter:"capitalize",cols:4},{name:"status",label:"Status",formatter:"capitalize",cols:4},{name:"from_number",label:"From",cols:6},{name:"to_number",label:"To",cols:6},{name:"body",label:"Message Body",cols:12}]}),this.deliveryView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"provider",label:"Provider",formatter:"capitalize",cols:6},{name:"provider_message_id",label:"Provider Message ID",cols:6},{name:"sent_at",label:"Sent At",formatter:"datetime",cols:6},{name:"delivered_at",label:"Delivered At",formatter:"datetime",cols:6},{name:"error_code",label:"Error Code",cols:6},{name:"error_message",label:"Error Message",cols:12}]}),this.metadataView=new n.default({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"Record ID",cols:6},{name:"created",label:"Created",formatter:"datetime",cols:6},{name:"modified",label:"Last Modified",formatter:"datetime",cols:6}]});const t={Message:this.messageView,Delivery:this.deliveryView,Metadata:this.metadataView};this.tabView=new i.TabView({containerId:"sms-tabs",tabs:t,activeTab:"Message"}),this.addChild(this.tabView);const a=new e.ContextMenu({containerId:"sms-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"Refresh",action:"refresh-sms",icon:"bi-arrow-repeat"},{type:"divider"},{label:"Delete Message",action:"delete-sms",icon:"bi-trash",danger:!0}]}});this.addChild(a)}async onActionRefreshSms(){try{this.getApp()?.toast?.info?.("Refreshing message..."),await this.model.fetch(),await this.render(),this.getApp()?.toast?.success?.("Message refreshed")}catch(e){this.getApp()?.toast?.error?.(e.message||"Refresh failed")}}async onActionDeleteSms(){if(await a.Dialog.confirm("Are you sure you want to delete this message?","Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"}))try{const e=await this.model.destroy();e?.success?this.emit("sms:deleted",{model:this.model}):this.getApp()?.toast?.error?.("Delete failed")}catch(e){this.getApp()?.toast?.error?.(e.message||"Delete failed")}}}SMSView.MODEL_CLASS=SMS;class SMSTablePage extends i.TablePage{constructor(e={}){super({...e,name:"admin_phonehub_sms",pageName:"SMS Messages",router:"admin/phonehub/sms",Collection:SMSList,itemView:SMSView,viewDialogOptions:{header:!1,size:"xl"},columns:[{key:"direction",label:"Direction",sortable:!0},{key:"from_number",label:"From",sortable:!0,formatter:"default('—')"},{key:"to_number",label:"To",sortable:!0,formatter:"default('—')"},{key:"status",label:"Status",sortable:!0},{key:"provider",label:"Provider",sortable:!0,formatter:"default('—')"},{key:"body",label:"Message",formatter:"default('—')"},{key:"sent_at",label:"Sent At",sortable:!0,formatter:"datetime"},{key:"delivered_at",label:"Delivered At",sortable:!0,formatter:"datetime"},{key:"created",label:"Created",sortable:!0,formatter:"datetime"}],selectable:!0,searchable:!0,sortable:!0,filterable:!0,paginated:!0,clickAction:"view",showRefresh:!0,showAdd:!1,showExport:!0,emptyMessage:"No SMS messages found.",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1}})}}function m(e,t=!0){if(e.registerPage("system/dashboard",AdminDashboardPage,{permissions:["view_admin"]}),e.registerPage("system/jobs",JobsAdminPage,{permissions:["view_jobs","manage_jobs"]}),e.registerPage("system/users",UserTablePage,{permissions:["manage_users"]}),e.registerPage("system/groups",GroupTablePage,{permissions:["manage_groups"]}),e.registerPage("system/members",MemberTablePage,{permissions:["manage_members"]}),e.registerPage("system/s3buckets",S3BucketTablePage,{permissions:["manage_aws"]}),e.registerPage("system/filemanagers",FileManagerTablePage,{permissions:["manage_files"]}),e.registerPage("system/files",FileTablePage,{permissions:["manage_files"]}),e.registerPage("system/incidents",IncidentTablePage,{permissions:["view_incidents"]}),e.registerPage("system/events",EventTablePage,{permissions:["view_incidents"]}),e.registerPage("system/logs",LogTablePage,{permissions:["view_logs"]}),e.registerPage("system/user/devices",UserDeviceTablePage,{permissions:["manage_users"]}),e.registerPage("system/user/device-locations",UserDeviceLocationTablePage,{permissions:["manage_users"]}),e.registerPage("system/system/geoip",GeoLocatedIPTablePage,{permissions:["manage_users"]}),e.registerPage("system/email/mailboxes",EmailMailboxTablePage,{permissions:["manage_aws"]}),e.registerPage("system/email/domains",EmailDomainTablePage,{permissions:["manage_aws"]}),e.registerPage("system/email/sent",SentMessageTablePage,{permissions:["manage_aws"]}),e.registerPage("system/email/templates",EmailTemplateTablePage,{permissions:["manage_aws"]}),e.registerPage("system/incident-dashboard",IncidentDashboardPage,{permissions:["view_incidents"]}),e.registerPage("system/rulesets",RuleSetTablePage,{permissions:["manage_incidents"]}),e.registerPage("system/tickets",TicketTablePage,{permissions:["manage_incidents"]}),e.registerPage("system/metrics/permissions",MetricsPermissionsTablePage,{permissions:["manage_metrics"]}),e.registerPage("system/push/dashboard",PushDashboardPage,{permissions:["manage_users"]}),e.registerPage("system/push/configs",PushConfigTablePage,{permissions:["manage_users"]}),e.registerPage("system/push/templates",PushTemplateTablePage,{permissions:["manage_users"]}),e.registerPage("system/push/deliveries",PushDeliveryTablePage,{permissions:["manage_users"]}),e.registerPage("system/push/devices",PushDeviceTablePage,{permissions:["manage_users"]}),e.registerPage("system/phonehub/numbers",PhoneNumberTablePage,{permissions:["manage_users"]}),e.registerPage("system/phonehub/sms",SMSTablePage,{permissions:["manage_users"]}),t&&e.sidebar&&e.sidebar.getMenuConfig){const t=e.sidebar.getMenuConfig("system");if(t&&t.items){const e=[{text:"Dashboard",route:"?page=system/dashboard",icon:"bi-speedometer2",permissions:["view_admin"]},{text:"Jobs Management",route:"?page=system/jobs",icon:"bi-gear-wide-connected",permissions:["view_jobs","manage_jobs"]},{text:"Users",route:"?page=system/users",icon:"bi-people",permissions:["manage_users"]},{text:"Groups",route:"?page=system/groups",icon:"bi-diagram-3",permissions:["manage_groups"]},{text:"Incidents & Tickets",route:null,icon:"bi-shield-exclamation",permissions:["view_incidents"],children:[{text:"Dashboard",route:"?page=system/incident-dashboard",icon:"bi-bar-chart-line",permissions:["view_incidents"]},{text:"Incidents",route:"?page=system/incidents",icon:"bi-exclamation-triangle",permissions:["view_incidents"]},{text:"Tickets",route:"?page=system/tickets",icon:"bi-ticket-detailed",permissions:["manage_incidents"]},{text:"Events",route:"?page=system/events",icon:"bi-bell",permissions:["view_incidents"]},{text:"Rule Engine",route:"?page=system/rulesets",icon:"bi-gear-wide-connected",permissions:["manage_incidents"]}]},{text:"Security",route:null,icon:"bi-shield",permissions:["manage_groups"],children:[{text:"Logs",route:"?page=system/logs",icon:"bi-journal-text",permissions:["view_logs"]},{text:"User Devices",route:"?page=system/user/devices",icon:"bi-phone",permissions:["manage_users"]},{text:"Device Locations",route:"?page=system/user/device-locations",icon:"bi-geo-alt",permissions:["manage_users"]},{text:"GeoIP Cache",route:"?page=system/system/geoip",icon:"bi-globe",permissions:["manage_users"]},{text:"Metrics Permissions",route:"?page=system/metrics/permissions",icon:"bi-bar-chart-line",permissions:["manage_metrics"]}]},{text:"Storage",route:null,icon:"bi-folder",permissions:["manage_files","manage_aws"],children:[{text:"S3 Buckets",route:"?page=system/s3buckets",icon:"bi-bucket",permissions:["manage_aws"]},{text:"Storage Backends",route:"?page=system/filemanagers",icon:"bi-hdd-stack",permissions:["manage_aws"]},{text:"Files",route:"?page=system/files",icon:"bi-file-earmark",permissions:["manage_files"]}]},{text:"Push Notifications",route:null,icon:"bi-broadcast",permissions:["manage_users"],children:[{text:"Dashboard",route:"?page=system/push/dashboard",icon:"bi-bar-chart-line"},{text:"Configurations",route:"?page=system/push/configs",icon:"bi-gear"},{text:"Templates",route:"?page=system/push/templates",icon:"bi-file-earmark-text"},{text:"Deliveries",route:"?page=system/push/deliveries",icon:"bi-send"},{text:"Devices",route:"?page=system/push/devices",icon:"bi-phone"}]},{text:"Email Admin",route:null,icon:"bi-envelope",permissions:["manage_aws"],children:[{text:"Domains",route:"?page=system/email/domains",icon:"bi-globe",permissions:["manage_aws"]},{text:"Mailboxes",route:"?page=system/email/mailboxes",icon:"bi-inbox",permissions:["manage_aws"]},{text:"Sent",route:"?page=system/email/sent",icon:"bi-send-check",permissions:["manage_aws"]},{text:"Templates",route:"?page=system/email/templates",icon:"bi-file-text",permissions:["manage_aws"]}]},{text:"Phone Hub",route:null,icon:"bi-telephone",permissions:["manage_users"],children:[{text:"Numbers",route:"?page=system/phonehub/numbers",icon:"bi-collection",permissions:["manage_users"]},{text:"SMS",route:"?page=system/phonehub/sms",icon:"bi-chat-dots",permissions:["manage_users"]}]}];t.items.unshift(...e)}}}exports.WebApp=a.WebApp,exports.BUILD_TIME=c.BUILD_TIME,exports.VERSION=c.VERSION,exports.VERSION_INFO=c.VERSION_INFO,exports.VERSION_MAJOR=c.VERSION_MAJOR,exports.VERSION_MINOR=c.VERSION_MINOR,exports.VERSION_REVISION=c.VERSION_REVISION,exports.AdminDashboardPage=AdminDashboardPage,exports.DeviceView=DeviceView,exports.EmailDomainTablePage=EmailDomainTablePage,exports.EmailMailboxTablePage=EmailMailboxTablePage,exports.EmailTemplateTablePage=EmailTemplateTablePage,exports.EmailTemplateView=EmailTemplateView,exports.EmailView=EmailView,exports.EventTablePage=EventTablePage,exports.EventView=EventView,exports.FileManagerTablePage=FileManagerTablePage,exports.FileTablePage=FileTablePage,exports.FileView=FileView,exports.GeoIPView=GeoIPView,exports.GeoLocatedIPTablePage=GeoLocatedIPTablePage,exports.GroupTablePage=GroupTablePage,exports.GroupView=GroupView,exports.IncidentDashboardPage=IncidentDashboardPage,exports.IncidentTablePage=IncidentTablePage,exports.IncidentView=IncidentView,exports.JobDetailsView=JobDetailsView,exports.JobHealthView=JobHealthView,exports.JobStatsView=JobStatsView,exports.JobsAdminPage=JobsAdminPage,exports.LogTablePage=LogTablePage,exports.LogView=LogView,exports.MemberTablePage=MemberTablePage,exports.MemberView=MemberView,exports.MetricsPermissionsTablePage=MetricsPermissionsTablePage,exports.MetricsPermissionsView=MetricsPermissionsView,exports.PhoneNumberTablePage=PhoneNumberTablePage,exports.PhoneNumberView=PhoneNumberView,exports.PushConfigTablePage=PushConfigTablePage,exports.PushDashboardPage=PushDashboardPage,exports.PushDeliveryTablePage=PushDeliveryTablePage,exports.PushDeliveryView=PushDeliveryView,exports.PushDeviceTablePage=PushDeviceTablePage,exports.PushDeviceView=PushDeviceView,exports.PushTemplateTablePage=PushTemplateTablePage,exports.RuleSetTablePage=RuleSetTablePage,exports.RuleSetView=RuleSetView,exports.RunnerDetailsView=RunnerDetailsView,exports.S3BucketTablePage=S3BucketTablePage,exports.SentMessageTablePage=SentMessageTablePage,exports.TaskDetailsView=TaskDetailsView,exports.TaskManagementPage=TaskManagementPage,exports.TicketTablePage=TicketTablePage,exports.TicketView=TicketView,exports.UserDeviceLocationTablePage=UserDeviceLocationTablePage,exports.UserDeviceTablePage=UserDeviceTablePage,exports.UserTablePage=UserTablePage,exports.UserView=UserView,exports.registerAdminPages=m,exports.registerSystemPages=m;
|
|
2
2
|
//# sourceMappingURL=admin.cjs.js.map
|