web-mojo 2.4.3 → 2.4.4
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-models.cjs.js +1 -1
- package/dist/admin-models.cjs.js.map +1 -1
- package/dist/admin-models.es.js +1 -1
- package/dist/admin-models.es.js.map +1 -1
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.es.js +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.css +49 -0
- package/dist/charts.es.js +1 -1
- package/dist/chunks/{AssistantPanelView-Bipbq_Ob.js → AssistantPanelView-B3orPoag.js} +2 -2
- package/dist/chunks/{AssistantPanelView-Bipbq_Ob.js.map → AssistantPanelView-B3orPoag.js.map} +1 -1
- package/dist/chunks/{AssistantPanelView-BWSQqGV9.js → AssistantPanelView-GeIHDvyN.js} +2 -2
- package/dist/chunks/{AssistantPanelView-BWSQqGV9.js.map → AssistantPanelView-GeIHDvyN.js.map} +1 -1
- package/dist/chunks/admin-FvzwR-f7.js +2 -0
- package/dist/chunks/admin-FvzwR-f7.js.map +1 -0
- package/dist/chunks/admin-yjCkhxvU.js +2 -0
- package/dist/chunks/admin-yjCkhxvU.js.map +1 -0
- package/dist/chunks/exportChart-CmfLeCi8.js +2 -0
- package/dist/chunks/exportChart-CmfLeCi8.js.map +1 -0
- package/dist/chunks/exportChart-Ue-zU0_c.js +2 -0
- package/dist/chunks/exportChart-Ue-zU0_c.js.map +1 -0
- package/dist/chunks/{version-BEKxO75G.js → version-CpR8sTVV.js} +2 -2
- package/dist/chunks/{version-BEKxO75G.js.map → version-CpR8sTVV.js.map} +1 -1
- package/dist/chunks/{version-LBA3E61x.js → version-CsLH6aiB.js} +2 -2
- package/dist/chunks/{version-LBA3E61x.js.map → version-CsLH6aiB.js.map} +1 -1
- package/dist/css/web-mojo.css +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 +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/package.json +1 -1
- package/dist/chunks/admin-Cct4cQOQ.js +0 -2
- package/dist/chunks/admin-Cct4cQOQ.js.map +0 -1
- package/dist/chunks/admin-D8GKQBwN.js +0 -2
- package/dist/chunks/admin-D8GKQBwN.js.map +0 -1
- package/dist/chunks/exportChart-CF7VJ4fb.js +0 -2
- package/dist/chunks/exportChart-CF7VJ4fb.js.map +0 -1
- package/dist/chunks/exportChart-DVIvZLoP.js +0 -2
- package/dist/chunks/exportChart-DVIvZLoP.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-FvzwR-f7.js","sources":["../../src/extensions/charts/KPITile.js","../../src/extensions/charts/KPIStrip.js","../../src/extensions/admin/account/AdminDashboardPage.js","../../src/extensions/admin/account/devices/LoginLocationMapView.js","../../src/extensions/admin/account/users/sections/AdminProfileSection.js","../../src/extensions/admin/account/users/sections/AdminPersonalSection.js","../../src/extensions/admin/account/users/sections/AdminSecuritySection.js","../../src/extensions/admin/account/users/sections/AdminConnectedSection.js","../../src/extensions/admin/account/users/sections/AdminNotificationsSection.js","../../src/extensions/admin/account/users/sections/AdminApiKeysSection.js","../../src/extensions/admin/shared/AdminMetadataSection.js","../../src/extensions/admin/account/users/UserView.js","../../src/extensions/admin/account/users/UserTablePage.js","../../src/extensions/admin/account/users/MemberView.js","../../src/extensions/admin/account/users/MemberTablePage.js","../../src/core/models/ApiKey.js","../../src/extensions/admin/account/groups/GroupView.js","../../src/extensions/admin/account/groups/GroupTablePage.js","../../src/extensions/admin/account/devices/DeviceView.js","../../src/extensions/admin/account/devices/UserDeviceTablePage.js","../../src/extensions/admin/account/devices/UserDeviceLocationTablePage.js","../../src/extensions/admin/account/devices/GeoIPView.js","../../src/extensions/admin/account/devices/GeoLocatedIPTablePage.js","../../src/extensions/admin/account/api_keys/ApiKeyView.js","../../src/extensions/admin/account/api_keys/ApiKeyTablePage.js","../../src/extensions/admin/aws/CloudWatchChart.js","../../src/extensions/admin/aws/CloudWatchDashboardPage.js","../../src/extensions/admin/incidents/dashboard/StatusStripPanel.js","../../src/core/views/data/StackTraceView.js","../../src/extensions/admin/security/HandlerBuilderView.js","../../src/extensions/admin/incidents/RuleSetView.js","../../src/extensions/admin/incidents/adapters/IncidentHistoryAdapter.js","../../src/extensions/admin/assistant/AssistantMessageView.js","../../src/extensions/admin/assistant/AssistantConversationListView.js","../../src/extensions/admin/assistant/AssistantView.js","../../src/extensions/admin/assistant/AssistantContextChat.js","../../src/extensions/admin/incidents/IncidentView.js","../../src/extensions/admin/incidents/dashboard/PriorityQueueView.js","../../src/extensions/admin/incidents/adapters/TicketNoteAdapter.js","../../src/extensions/admin/incidents/TicketView.js","../../src/extensions/admin/incidents/dashboard/TicketsQueueView.js","../../src/extensions/admin/incidents/dashboard/ThreatCompositionChart.js","../../src/extensions/admin/incidents/dashboard/GeographyPanel.js","../../src/extensions/admin/incidents/dashboard/DistributionStrip.js","../../src/extensions/admin/incidents/dashboard/TopSourcesPanel.js","../../src/extensions/admin/incidents/dashboard/AuthFailuresPanel.js","../../src/extensions/admin/incidents/dashboard/HealthStrip.js","../../src/extensions/admin/incidents/dashboard/SecurityDashboardPage.js","../../src/extensions/admin/incidents/IncidentTablePage.js","../../src/extensions/admin/incidents/EventView.js","../../src/extensions/admin/incidents/EventTablePage.js","../../src/extensions/admin/incidents/TicketTablePage.js","../../src/extensions/admin/incidents/RuleSetTablePage.js","../../src/extensions/admin/messaging/email/EmailDomainTablePage.js","../../src/extensions/admin/messaging/email/EmailMailboxTablePage.js","../../src/extensions/admin/messaging/email/EmailTemplateView.js","../../src/extensions/admin/messaging/email/EmailTemplateTablePage.js","../../src/extensions/admin/messaging/email/EmailView.js","../../src/extensions/admin/messaging/email/SentMessageTablePage.js","../../src/extensions/admin/messaging/PublicMessageView.js","../../src/extensions/admin/messaging/PublicMessageTablePage.js","../../src/extensions/admin/messaging/sms/PhoneNumberView.js","../../src/extensions/admin/messaging/sms/PhoneNumberTablePage.js","../../src/extensions/admin/messaging/sms/SMSView.js","../../src/extensions/admin/messaging/sms/SMSTablePage.js","../../src/extensions/admin/messaging/push/PushDashboardPage.js","../../src/extensions/admin/messaging/push/PushConfigTablePage.js","../../src/extensions/admin/messaging/push/PushTemplateTablePage.js","../../src/extensions/admin/messaging/push/PushDeliveryView.js","../../src/extensions/admin/messaging/push/PushDeliveryTablePage.js","../../src/extensions/admin/messaging/push/PushDeviceView.js","../../src/extensions/admin/messaging/push/PushDeviceTablePage.js","../../src/extensions/admin/jobs/JobStatsView.js","../../src/extensions/admin/jobs/JobsRunnersStrip.js","../../src/extensions/admin/jobs/sections/JobOverviewSection.js","../../src/extensions/admin/jobs/sections/JobOperationsSection.js","../../src/extensions/admin/jobs/JobDashboardPage.js","../../src/extensions/admin/jobs/RunnerDetailsView.js","../../src/extensions/admin/jobs/sections/JobRunnersSection.js","../../src/extensions/admin/jobs/JobRunnersPage.js","../../src/extensions/admin/jobs/JobDetailsView.js","../../src/extensions/admin/jobs/sections/JobTableSection.js","../../src/extensions/admin/jobs/JobsTablePage.js","../../src/extensions/admin/security/BlockedIPsTablePage.js","../../src/extensions/admin/monitoring/LogView.js","../../src/extensions/admin/security/FirewallLogTablePage.js","../../src/extensions/admin/security/BouncerSignalView.js","../../src/extensions/admin/security/BouncerSignalTablePage.js","../../src/extensions/admin/security/BouncerDeviceView.js","../../src/extensions/admin/security/BouncerDeviceTablePage.js","../../src/extensions/admin/security/BotSignatureTablePage.js","../../src/extensions/admin/security/IPSetView.js","../../src/extensions/admin/security/IPSetTablePage.js","../../src/extensions/admin/monitoring/LogTablePage.js","../../src/extensions/admin/monitoring/MetricsPermissionsView.js","../../src/extensions/admin/monitoring/MetricsPermissionsTablePage.js","../../src/core/models/Settings.js","../../src/extensions/admin/settings/SettingView.js","../../src/extensions/admin/settings/SettingTablePage.js","../../src/extensions/admin/storage/FileManagerTablePage.js","../../src/extensions/admin/storage/FileTablePage.js","../../src/extensions/admin/storage/S3BucketTablePage.js","../../src/extensions/admin/shortlinks/ShortLinkView.js","../../src/extensions/admin/shortlinks/ShortLinkTablePage.js","../../src/extensions/admin/shortlinks/ShortLinkClickTablePage.js","../../src/extensions/admin/account/devices/UserDeviceLocationView.js","../../src/extensions/admin/aws/CloudWatchResourceView.js","../../src/extensions/admin/jobs/JobHealthView.js","../../src/extensions/admin/assistant/AssistantSkillView.js","../../src/extensions/admin/assistant/AssistantSkillTablePage.js","../../src/extensions/admin/assistant/AssistantConversationView.js","../../src/extensions/admin/assistant/AssistantConversationTablePage.js","../../src/extensions/admin/assistant/AssistantMemoryPage.js","../../src/admin.js","../../src/extensions/admin/jobs/ScheduledTaskTablePage.js"],"sourcesContent":["/**\n * KPITile — compact dashboard tile: small label, big tabular-numerals value,\n * color-coded delta badge, embedded MiniChart sparkline.\n *\n * Difference from MetricsMiniChartWidget: KPITile is presentation-only\n * (no fetch, no settings menu, no icon, no colored background). It renders\n * pre-fetched data passed in via constructor or `setData()`. This makes it\n * ideal for compact \"pulse\" rows where one parent fetches once and feeds\n * many tiles — see KPIStrip.\n *\n * For richer single-tile widgets with their own fetch + settings menu,\n * use MetricsMiniChartWidget.\n *\n * Click emits `tile:click` with `{ tile, slug }`.\n *\n * Delta rendering:\n * - `deltaPct` present and finite → \"+12%\" / \"−8%\"\n * - `deltaPct` absent (e.g. prev was 0) and `delta` present → \"+4\" absolute\n * - both null → no badge.\n * Never renders Infinity%.\n *\n * `severity` (optional): 'critical' | 'high' | 'warn' | 'info' | 'good'.\n * Adds a left-stripe accent. Use for tiles that ARE the alert.\n *\n * `tone` (optional): 'good' | 'bad' | null. Decides whether a rising\n * delta colors green or red. For blocks/incidents/failures, rising\n * is bad (`tone: 'bad'`). For resolved counts, rising is good\n * (`tone: 'good'`). Default null = neutral grey.\n */\n\nimport View from '@core/View.js';\nimport MiniChart from './MiniChart.js';\n\nclass KPITile extends View {\n constructor(options = {}) {\n super({\n tagName: 'button',\n ...options,\n className: `mojo-kpi-tile ${options.severity ? 'mojo-kpi-tile-' + options.severity : ''} ${options.className || ''}`.trim()\n });\n\n this.slug = options.slug || null;\n this.label = options.label || '';\n this.value = options.value ?? null;\n this.delta = options.delta ?? null;\n this.deltaPct = options.deltaPct ?? null;\n this.severity = options.severity || null;\n this.tone = options.tone || null;\n this.sparklineValues = Array.isArray(options.sparkline) ? options.sparkline.slice() : [];\n this.sparklineColor = options.sparklineColor || null;\n this.sparklineHeight = options.sparklineHeight || 36;\n this.formatter = typeof options.formatter === 'function' ? options.formatter : null;\n\n // type=button so it doesn't submit any wrapping form\n this.element.setAttribute('type', 'button');\n }\n\n async onInit() {\n // Embed MiniChart for the sparkline (smoothed line + faint fill).\n // height stays small; width is 100% of container.\n this.sparkline = new MiniChart({\n containerId: 'spark',\n chartType: 'line',\n data: this.sparklineValues,\n color: this.sparklineColor || this._defaultSparkColor(),\n fillColor: this._fillColorFor(this.sparklineColor || this._defaultSparkColor()),\n fill: true,\n smoothing: 0.3,\n height: this.sparklineHeight,\n width: '100%',\n showTooltip: false,\n showCrosshair: false,\n showXAxis: false,\n animate: false,\n padding: 2\n });\n this.addChild(this.sparkline);\n }\n\n async getTemplate() {\n const valueDisplay = this._formatValue(this.value);\n const deltaHtml = this._renderDelta();\n return `\n <span class=\"mojo-kpi-tile-label\">${this.escapeHtml(this.label)}</span>\n <span class=\"mojo-kpi-tile-value\">${this.escapeHtml(valueDisplay)}</span>\n ${deltaHtml}\n <div data-container=\"spark\" class=\"mojo-kpi-tile-spark\"></div>\n `;\n }\n\n async onAfterRender() {\n if (!this._clickBound) {\n this.element.addEventListener('click', () => {\n this.emit?.('tile:click', { tile: this, slug: this.slug });\n });\n this._clickBound = true;\n }\n }\n\n /**\n * Update tile data without recreating the view. Used by KPIStrip after\n * a refresh fetch.\n */\n setData({ value, delta, deltaPct, sparkline } = {}) {\n if (value !== undefined) this.value = value;\n if (delta !== undefined) this.delta = delta;\n if (deltaPct !== undefined) this.deltaPct = deltaPct;\n if (sparkline !== undefined) {\n this.sparklineValues = Array.isArray(sparkline) ? sparkline.slice() : [];\n this.sparkline?.setData(this.sparklineValues);\n }\n if (this.isMounted()) this.render();\n }\n\n _defaultSparkColor() {\n const map = {\n critical: 'rgba(220, 53, 69, 1)', // bs-danger\n high: 'rgba(253, 126, 20, 1)', // bs-orange\n warn: 'rgba(255, 193, 7, 1)', // bs-warning\n info: 'rgba(13, 202, 240, 1)', // bs-info\n good: 'rgba(25, 135, 84, 1)' // bs-success\n };\n // Default to info (cyan) when no severity is set — matches the\n // mockup contract where neutral tiles still get a coloured trend\n // line. Pure grey blends into the dark surface.\n return map[this.severity] || 'rgba(13, 202, 240, 1)';\n }\n\n _fillColorFor(stroke) {\n // Convert \"rgba(r,g,b,1)\" → faint \"rgba(r,g,b,0.12)\" for the area fill.\n const m = String(stroke).match(/rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/);\n if (m) return `rgba(${m[1]}, ${m[2]}, ${m[3]}, 0.12)`;\n return 'rgba(108, 117, 125, 0.12)';\n }\n\n _formatValue(v) {\n if (v == null) return '—';\n if (this.formatter) return this.formatter(v);\n if (typeof v === 'number') return v.toLocaleString();\n return String(v);\n }\n\n _renderDelta() {\n if (this.deltaPct == null && this.delta == null) return '';\n\n let label, sign;\n if (this.deltaPct != null && Number.isFinite(this.deltaPct)) {\n const rounded = Math.abs(this.deltaPct) >= 10\n ? Math.round(this.deltaPct)\n : Math.round(this.deltaPct * 10) / 10;\n sign = rounded > 0 ? '+' : (rounded < 0 ? '−' : '±');\n label = `${sign}${Math.abs(rounded)}%`;\n } else if (this.delta != null) {\n // No percent (prev was 0) — show absolute delta. Never Infinity%.\n sign = this.delta > 0 ? '+' : (this.delta < 0 ? '−' : '±');\n label = `${sign}${Math.abs(this.delta)}`;\n } else {\n return '';\n }\n\n let cls = 'mojo-kpi-tile-delta';\n if (sign === '±') {\n cls += ' mojo-kpi-tile-delta-flat';\n } else if (this.tone === 'bad') {\n cls += sign === '+' ? ' mojo-kpi-tile-delta-bad' : ' mojo-kpi-tile-delta-good';\n } else if (this.tone === 'good') {\n cls += sign === '+' ? ' mojo-kpi-tile-delta-good' : ' mojo-kpi-tile-delta-bad';\n } else {\n cls += ' mojo-kpi-tile-delta-neutral';\n }\n\n return `<span class=\"${cls}\">${this.escapeHtml(label)}</span>`;\n }\n}\n\nexport default KPITile;\n","/**\n * KPIStrip — orchestrator for a row of KPITiles.\n *\n * One batched fetch to `/api/metrics/series?with_delta=true` populates\n * the value and delta on every metric tile. Optional REST count tiles\n * (e.g. \"open incidents\" — not a time-series counter, just a list count)\n * issue parallel REST GETs. Optional sparkline fetch makes one batched\n * `/api/metrics/fetch` call for the trailing N days of data per slug.\n *\n * The tile spec list shape:\n * tiles: [\n * { slug: 'incidents', label: 'Incidents', tone: 'bad' },\n * { slug: 'auth:failures', label: 'Failed Auth', tone: 'bad', severity: 'warn' },\n * { rest: { endpoint: '/api/incident/incident', params: { status: 'open', size: 1 } },\n * label: 'Open Incidents', tone: 'bad', severity: 'critical', key: 'open-incidents' }\n * ]\n *\n * For metric tiles: `slug` identifies which entry in the `series` and\n * `fetch` responses to read. For REST count tiles: `rest` provides the\n * endpoint/params and the strip reads `data.count` from the response.\n *\n * Click on any tile re-emits as `tile:click` from the strip with\n * `{ tile, slug, key }`.\n */\n\nimport View from '@core/View.js';\nimport KPITile from './KPITile.js';\n\nclass KPIStrip extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `mojo-kpi-strip ${options.className || ''}`.trim()\n });\n\n this.tiles = Array.isArray(options.tiles) ? options.tiles : [];\n this.account = options.account || 'incident';\n this.granularity = options.granularity || 'days';\n this.sparklineDays = options.sparklineDays ?? 7;\n this.sparklineGranularity = options.sparklineGranularity || 'days';\n this.seriesEndpoint = options.seriesEndpoint || '/api/metrics/series';\n this.fetchEndpoint = options.fetchEndpoint || '/api/metrics/fetch';\n this.includeSparkline = options.includeSparkline !== false;\n this.tileHeight = options.tileHeight || 36;\n\n this._tileViews = []; // matches `this.tiles` order\n }\n\n async getTemplate() {\n return `<div class=\"mojo-kpi-strip-grid\" data-container=\"grid\"></div>`;\n }\n\n async onInit() {\n // Build one KPITile per spec; mount under a single grid container.\n // We use containerId='grid' on the strip and append children manually\n // by wrapping each in its own dynamic container div.\n for (let i = 0; i < this.tiles.length; i++) {\n const spec = this.tiles[i];\n const containerId = `kpi-tile-${i}`;\n const tile = new KPITile({\n containerId,\n slug: spec.slug || spec.key || `tile-${i}`,\n label: spec.label || spec.slug || '',\n severity: spec.severity || null,\n tone: spec.tone || null,\n sparklineHeight: this.tileHeight,\n formatter: spec.formatter || null\n });\n tile.on?.('tile:click', (data) => {\n this.emit?.('tile:click', { ...data, key: spec.key || spec.slug });\n });\n this._tileViews.push(tile);\n this.addChild(tile);\n }\n }\n\n async getViewData() {\n // Inject per-tile container divs into the grid template before render.\n return {\n ...this.data,\n tilesHtml: this._tileViews.map((_, i) =>\n `<div data-container=\"kpi-tile-${i}\" class=\"mojo-kpi-strip-cell\"></div>`\n ).join('')\n };\n }\n\n // We can't do template variables for child containers safely (the grid\n // needs N data-container slots known at template time). Override the\n // template to build the grid markup at render-time.\n async renderTemplate() {\n const cells = this._tileViews.map((_, i) =>\n `<div data-container=\"kpi-tile-${i}\" class=\"mojo-kpi-strip-cell\"></div>`\n ).join('');\n return `<div class=\"mojo-kpi-strip-grid\">${cells}</div>`;\n }\n\n async onAfterRender() {\n await this.refresh();\n }\n\n /**\n * Refresh all tiles from the backend. One series call + parallel REST\n * count calls + (optional) one sparkline fetch.\n */\n async refresh() {\n const rest = this.getApp()?.rest;\n if (!rest) return;\n\n const metricSpecs = this.tiles.filter(t => t.slug);\n const restSpecs = this.tiles.filter(t => t.rest);\n\n // Sparkline fetch slugs: metric-tile slugs PLUS any REST tile's\n // sparklineSlug (so a \"New Incidents\" REST count tile can borrow\n // the trail from the matching `incidents` metric series). Dedupe.\n const sparkSlugs = Array.from(new Set([\n ...metricSpecs.map(t => t.slug),\n ...restSpecs.map(t => t.sparklineSlug).filter(Boolean)\n ]));\n const slugs = metricSpecs.map(t => t.slug);\n\n const promises = [];\n\n // 1) Batched series call with deltas. Param-name convention\n // matters — both /api/metrics/series AND /api/metrics/fetch\n // require `slugs=a,b,c` (plural, comma-separated) on the\n // production backend. The singular `slug=` form returns 400\n // \"missing required parameter\" (dev backend was permissive,\n // which masked the bug for a while). See the matching note\n // on the sparkline fetch below + MetricsChart.buildApiParams.\n // Include sparklineSlugs too so REST tiles that borrow a trail\n // also get a delta badge.\n let seriesPromise = null;\n if (sparkSlugs.length) {\n const params = {\n slugs: sparkSlugs.join(','),\n account: this.account,\n granularity: this.granularity,\n with_delta: true,\n _: Date.now()\n };\n seriesPromise = rest.GET(this.seriesEndpoint, params).catch(err => {\n console.warn('[KPIStrip] series fetch failed:', err);\n return null;\n });\n promises.push(seriesPromise);\n }\n\n // 2) Batched fetch for sparkline data — covers both metric tiles\n // and any REST tiles with a sparklineSlug.\n // Both /api/metrics/series AND /api/metrics/fetch require\n // `slugs=a,b,c` (plural). The singular `slug=` form returns\n // 400 \"missing required parameter\" on the production backend.\n let sparkPromise = null;\n if (this.includeSparkline && sparkSlugs.length) {\n const drStart = new Date(Date.now() - this.sparklineDays * 86400000);\n const sparkParams = {\n slugs: sparkSlugs.join(','),\n account: this.account,\n granularity: this.sparklineGranularity,\n with_labels: true,\n dr_start: Math.floor(drStart.getTime() / 1000),\n _: Date.now()\n };\n sparkPromise = rest.GET(this.fetchEndpoint, sparkParams).catch(err => {\n console.warn('[KPIStrip] sparkline fetch failed:', err);\n return null;\n });\n promises.push(sparkPromise);\n }\n\n // 3) Parallel REST count calls\n const restPromises = restSpecs.map(spec => {\n const params = { ...(spec.rest.params || {}), _: Date.now() };\n return rest.GET(spec.rest.endpoint, params).catch(err => {\n console.warn(`[KPIStrip] count fetch failed for ${spec.label}:`, err);\n return null;\n });\n });\n promises.push(...restPromises);\n\n await Promise.allSettled(promises);\n\n // ── distribute results ───────────────────────────────────────\n const seriesResp = seriesPromise ? await seriesPromise : null;\n const sparkResp = sparkPromise ? await sparkPromise : null;\n // /api/metrics/series → response.data = { status, data: {slug: N},\n // prev_data, deltas: {slug: {delta, delta_pct?}}, ... }\n // /api/metrics/fetch → response.data = { status, data: { labels: [...],\n // data: { slug: [...values], ... } } }\n const seriesData = this._unwrap(seriesResp); // -> response.data\n const sparkInner = this._unwrap(sparkResp)?.data; // -> { labels, data: {slug:[]}}\n\n // metric tiles\n for (const spec of metricSpecs) {\n const tileIdx = this.tiles.indexOf(spec);\n const tile = this._tileViews[tileIdx];\n if (!tile) continue;\n\n const value = seriesData?.data?.[spec.slug] ?? null;\n const deltaInfo = seriesData?.deltas?.[spec.slug] || {};\n const delta = deltaInfo.delta ?? null;\n // delta_pct is intentionally omitted by backend when prev=0\n const deltaPct = deltaInfo.delta_pct ?? null;\n\n // Sparkline values: prefer slug key; fall back to 'default' which\n // the backend uses when a single-slug fetch is collapsed.\n const slugSeries = sparkInner?.data?.[spec.slug];\n const fallbackSeries = sparkInner?.data?.default;\n const sparkSeries = Array.isArray(slugSeries)\n ? slugSeries\n : (Array.isArray(fallbackSeries) ? fallbackSeries : []);\n\n tile.setData({\n value,\n delta,\n deltaPct,\n sparkline: sparkSeries\n });\n }\n\n // REST count tiles\n let restIdx = 0;\n for (const spec of restSpecs) {\n const tileIdx = this.tiles.indexOf(spec);\n const tile = this._tileViews[tileIdx];\n if (!tile) continue;\n const resp = await restPromises[restIdx++];\n const count = this._readRestCount(resp);\n\n const setPayload = { value: count, delta: null, deltaPct: null };\n\n // If the tile borrowed a sparkline slug, pick up its trail\n // AND its delta info from the batched series + fetch responses\n // so the tile gets a \"+12%\" badge alongside the count.\n if (spec.sparklineSlug) {\n const slugSeries = sparkInner?.data?.[spec.sparklineSlug];\n const fallback = sparkInner?.data?.default;\n setPayload.sparkline = Array.isArray(slugSeries)\n ? slugSeries\n : (Array.isArray(fallback) ? fallback : []);\n\n const deltaInfo = seriesData?.deltas?.[spec.sparklineSlug];\n if (deltaInfo) {\n setPayload.delta = deltaInfo.delta ?? null;\n setPayload.deltaPct = deltaInfo.delta_pct ?? null;\n }\n }\n tile.setData(setPayload);\n }\n\n this.emit?.('strip:refreshed');\n }\n\n _unwrap(response) {\n if (!response) return null;\n // Standard mojo response: { success, data: { status, data, deltas, ... } }\n if (response.success && response.data) return response.data;\n return response;\n }\n\n _readRestCount(response) {\n if (!response) return null;\n const body = response.data || response;\n // _mode=count returns { status, count } at the top level\n // (no `data` array). Fall through to the older shape for any\n // tile spec that hasn't been migrated yet.\n if (typeof body?.count === 'number') return body.count;\n if (typeof body?.data?.count === 'number') return body.data.count;\n return null;\n }\n}\n\nexport default KPIStrip;\n","/**\n * AdminDashboardPage - Administrative dashboard with system metrics and charts\n */\n\nimport Page from '@core/Page.js';\nimport View from '@core/View.js';\nimport { MetricsChart, MetricsMiniChart, MetricsMiniChartWidget, exportChartPng } from '@ext/charts/index.js';\n\n// Embedded HeaderView for dashboard statistics\nclass AdminHeaderView extends View {\n constructor(options = {}) {\n super({\n title: 'Dashboard',\n ...options,\n headerActions: [\n {\n label: 'Export',\n icon: 'bi-download',\n action: 'export',\n buttonClass: 'btn-primary'\n }\n ],\n className: 'admin-header-section'\n });\n\n // Mock data - replace with real API calls\n this.stats = {\n user_activity_day: 0,\n total_users: 0,\n group_activity_day: 0,\n total_groups: 0,\n api_calls: 0,\n apiChange: '',\n incidents: 0,\n incidentsChange: ''\n };\n\n // Prepare formatted data for template\n this.prepareStatsForTemplate();\n }\n\n async getTemplate() {\n 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 `;\n }\n\n async onInit() {\n // TODO: Replace with actual API calls to fetch real statistics\n // this.loadStats();\n // slug: user_activity_day, group_activity_day\n this.userActivity = new MetricsMiniChartWidget({\n icon: \"bi bi-people fs-2\",\n title: 'User Activity',\n subtitle: '{{now_value}} <span class=\"subtitle-label\">{{now_label}}</span> {{total_users}} <span class=\"subtitle-label\">Total</span>',\n background: \"#5388D6\",\n textColor: \"#FFFFFF\",\n granularity: 'days',\n trendRange: 4,\n trendOffset: 0,\n slugs: ['user_activity_day'],\n account: 'global',\n chartType: 'bar',\n showTooltip: true,\n showXAxis: true,\n height: 50,\n chartWidth: '100%',\n color: 'rgba(245, 245, 255, 0.8)',\n fill: true,\n fillColor: 'rgba(245, 245, 255, 0.6)',\n smoothing: 0.3,\n showTrending: true,\n showSettings: true,\n showDateRange: true,\n containerId: 'user_activity_day'\n });\n this.addChild(this.userActivity);\n\n this.groupActivity = new MetricsMiniChartWidget({\n icon: \"bi bi-collection fs-2\",\n title: 'Group Activity',\n subtitle: '{{now_value}} <span class=\"subtitle-label\">{{now_label}}</span> {{total_groups}} <span class=\"subtitle-label\">Total</span>',\n background: \"#1f6a7a\",\n textColor: \"#FFFFFF\",\n granularity: 'days',\n trendRange: 4,\n trendOffset: 0,\n slugs: ['group_activity_day'],\n account: 'global',\n chartType: 'bar',\n showTooltip: true,\n showXAxis: true,\n height: 50,\n chartWidth: '100%',\n color: 'rgba(245, 245, 255, 0.8)',\n fill: true,\n fillColor: 'rgba(245, 245, 255, 0.6)',\n smoothing: 0.3,\n showTrending: true,\n containerId: 'group_activity_day'\n });\n this.addChild(this.groupActivity);\n\n this.apiActivity = new MetricsMiniChartWidget({\n icon: \"bi bi-graph-up fs-2\",\n title: 'API Requests',\n subtitle: '{{now_value}} <span class=\"subtitle-label\">{{now_label}}</span> {{total}} <span class=\"subtitle-label\">Total</span>',\n background: \"#50A079\",\n textColor: \"#FFFFFF\",\n endpoint: '/api/metrics/fetch',\n trendRange: 4,\n trendOffset: 0,\n granularity: 'days',\n slugs: ['api_calls'],\n account: 'global',\n chartType: 'line',\n showTooltip: true,\n showXAxis: true,\n height: 50,\n chartWidth: '100%',\n color: 'rgba(245, 245, 255, 0.8)',\n fill: true,\n fillColor: 'rgba(245, 245, 255, 0.6)',\n smoothing: 0.3,\n showTrending: true,\n containerId: 'api_activity_day'\n });\n this.addChild(this.apiActivity);\n\n this.incidentActivity = new MetricsMiniChartWidget({\n icon: \"bi bi-exclamation-triangle fs-2\",\n title: 'Incidents',\n subtitle: '{{now_value}} <span class=\"subtitle-label\">{{now_label}}</span> {{total}} <span class=\"subtitle-label\">Total</span>',\n background: \"#B14545\",\n textColor: \"#FFFFFF\",\n endpoint: '/api/metrics/fetch',\n trendRange: 4,\n trendOffset: 0,\n granularity: 'days',\n slugs: ['incidents'],\n account: 'incident',\n chartType: 'line',\n showTooltip: true,\n showXAxis: true,\n height: 50,\n chartWidth: '100%',\n color: 'rgba(245, 245, 255, 0.8)',\n fill: true,\n fillColor: 'rgba(245, 245, 255, 0.6)',\n smoothing: 0.3,\n showTrending: true,\n containerId: 'incident_activity_day'\n });\n this.addChild(this.incidentActivity);\n }\n\n async onBeforeRender() {\n\n }\n\n prepareStatsForTemplate() {\n // Determine badge and icon classes based on incidents change\n // const isDecreasing = this.stats.incidentsChange.startsWith('-');\n // this.stats.incidentsBadgeClass = isDecreasing ? 'bg-success-subtle text-success' : 'bg-warning-subtle text-warning';\n // this.stats.incidentsIconClass = isDecreasing ? 'arrow-down' : 'arrow-up';\n\n }\n\n async loadValues() {\n try {\n const response = await this.getApp().rest.GET('/api/metrics/value/get', {\n slugs: ['total_users', 'total_groups'],\n account: \"global\"\n });\n if (response.success && response.data.status) {\n Object.assign(this.stats, response.data.data);\n }\n if (this.groupActivity) {\n this.groupActivity.header.total_groups = this.stats.total_groups || 0;\n this.groupActivity.header.render();\n }\n if (this.userActivity) {\n this.userActivity.header.total_users = this.stats.total_users || 0;\n this.userActivity.header.render();\n }\n } catch (error) {\n console.error('Failed to load admin stats:', error);\n }\n }\n\n async loadStats() {\n // Example of how to load real data\n try {\n const response = await this.getApp().rest.GET('/api/metrics/series', {\n slugs: ['user_created', 'user_activity_day', \"incidents\",\n \"api_calls\", \"api_errors\", \"group_activity_day\"],\n account: \"global\",\n granularity: \"days\"\n });\n if (response.success && response.data.status) {\n Object.assign(this.stats, response.data.data);\n this.prepareStatsForTemplate();\n }\n } catch (error) {\n console.error('Failed to load admin stats:', error);\n }\n }\n}\n\nexport default class AdminDashboardPage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n title: 'Admin Dashboard',\n className: 'admin-dashboard-page'\n });\n\n // Page data\n this.pageTitle = 'Admin Dashboard';\n this.pageSubtitle = 'System monitoring and metrics overview';\n }\n\n async getTemplate() {\n return `\n <div class=\"admin-dashboard-container container-lg\">\n <!-- Page Header -->\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 <!-- Stats Header -->\n <div data-container=\"admin-header\"></div>\n <div data-container=\"example-chart\"></div>\n <!-- Charts Section -->\n <div class=\"row\">\n <!-- Full Width API Metrics Chart -->\n <div class=\"col-12 mb-4\">\n <div data-container=\"api-metrics-chart\"></div>\n </div>\n </div>\n\n <!-- System Status Footer -->\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 `;\n }\n\n async onInit() {\n // Set last updated time\n this.lastUpdated = new Date().toLocaleString();\n\n // Create and add header view\n this.headerView = new AdminHeaderView({\n containerId: 'admin-header'\n });\n this.addChild(this.headerView);\n\n // Create API Metrics Chart\n this.apiMetricsChart = new MetricsChart({\n title: `<i class=\"bi bi-graph-up me-2\"></i> API Metrics`,\n endpoint: '/api/metrics/fetch',\n height: 250,\n granularity: 'hours',\n slugs: ['api_calls', 'api_errors'],\n account: 'global',\n chartType: 'bar',\n showDateRange: false,\n yAxis: {\n label: 'Count',\n beginAtZero: true\n },\n tooltip: {\n // Integer counts; pass 'number:2' if you want decimal precision.\n y: 'number:0'\n },\n containerId: 'api-metrics-chart'\n });\n this.addChild(this.apiMetricsChart);\n\n }\n\n // Action Handlers\n async onActionRefreshAll(event, element) {\n const button = element || event?.currentTarget || null;\n const icon = button?.querySelector?.('i');\n try {\n icon?.classList.add('bi-spin');\n if (button) button.disabled = true;\n\n // Refresh all charts\n const promises = [\n this.headerView?.loadValues(),\n this.apiMetricsChart?.refresh()\n ].filter(Boolean);\n\n await Promise.allSettled(promises);\n\n\n // Update last updated time\n this.lastUpdated = new Date().toLocaleString();\n\n // Emit refresh event\n const eventBus = this.getApp()?.events;\n if (eventBus) {\n eventBus.emit('admin:dashboard-refreshed', {\n page: this,\n timestamp: this.lastUpdated\n });\n }\n\n } catch (error) {\n console.error('Failed to refresh dashboard:', error);\n // Show error feedback\n const alert = this.element.querySelector('.alert-success');\n if (alert) {\n alert.className = 'alert alert-danger border-0';\n alert.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 `;\n\n // Reset after 5 seconds\n setTimeout(() => {\n alert.className = 'alert alert-success border-0';\n alert.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 `;\n }, 5000);\n }\n } finally {\n icon?.classList.remove('bi-spin');\n if (button) button.disabled = false;\n }\n }\n\n async onActionExportMetrics(event, element) {\n try {\n // Export the metrics chart as PNG via the SVG-to-PNG helper.\n if (this.apiMetricsChart?.chart) exportChartPng(this.apiMetricsChart.chart);\n\n // Show success feedback\n const eventBus = this.getApp()?.events;\n if (eventBus) {\n eventBus.emit('admin:metrics-exported', {\n page: this,\n charts: ['api-metrics']\n });\n }\n\n } catch (error) {\n console.error('Failed to export metrics:', error);\n }\n }\n\n async onActionViewAlerts(event, element) {\n // Navigate to alerts page or show alerts modal\n const router = this.getApp()?.router;\n if (router) {\n router.navigateTo('/admin/alerts');\n }\n }\n\n async onActionViewSystemStatus(event, element) {\n // Navigate to system status page\n const router = this.getApp()?.router;\n if (router) {\n router.navigateTo('/admin/system-status');\n }\n }\n\n // Public API\n async refreshDashboard() {\n return this.onActionRefreshAll(null, null, { disabled: false, querySelector: () => null });\n }\n\n getCharts() {\n return {\n apiMetrics: this.apiMetricsChart\n };\n }\n\n getStats() {\n return this.headerView?.stats || {};\n }\n\n async onAfterRender() {\n this.headerView?.loadValues();\n }\n}\n","/**\n * LoginLocationMapView - Interactive map of login locations\n *\n * Plots country-level login count markers from the Login Events summary API,\n * with drill-down to region level on marker click.\n *\n * Usage:\n * // System-wide\n * new LoginLocationMapView({ height: 360, mapStyle: 'dark' })\n *\n * // Per-user\n * new LoginLocationMapView({ userId: 42, height: 300, mapStyle: 'dark' })\n */\n\nimport View from '@core/View.js';\n\nclass LoginLocationMapView extends View {\n constructor(options = {}) {\n super({\n className: 'login-location-map-view',\n ...options\n });\n\n this.userId = options.userId || null;\n this.height = options.height || 360;\n this.mapStyle = options.mapStyle || 'dark';\n this.drStart = options.drStart || null;\n this.drEnd = options.drEnd || null;\n\n // Drill-down state\n this._drillCountry = null;\n this._refreshing = false;\n this.mapView = null;\n this._mapAvailable = false;\n }\n\n async getTemplate() {\n return `\n <div class=\"login-location-map\">\n <div class=\"d-none align-items-center gap-2 mb-2\" data-region=\"drill-bar\">\n <button class=\"btn btn-sm btn-outline-secondary\" data-action=\"reset-drill-down\">\n <i class=\"bi bi-arrow-left me-1\"></i>All Countries\n </button>\n <span class=\"text-muted small\" data-region=\"drill-label\"></span>\n </div>\n <div data-container=\"map\" style=\"height:${this.height}px;\"></div>\n <div class=\"text-muted small px-1 pt-2\" data-region=\"status\"></div>\n </div>\n `;\n }\n\n async onInit() {\n try {\n const MapLibreView = (await import('@ext/map/MapLibreView.js')).default;\n this.mapView = new MapLibreView({\n containerId: 'map',\n height: this.height,\n style: this.mapStyle,\n zoom: 1.3,\n center: [10, 20],\n pitch: 15,\n bearing: 0,\n showNavigationControl: true,\n autoFitBounds: false\n });\n this.addChild(this.mapView);\n this._mapAvailable = true;\n await this.refresh();\n } catch (e) {\n this._mapAvailable = false;\n this._setStatus('Map extension not available.');\n }\n }\n\n // ── Data fetching ────────────────────────────────\n\n async refresh() {\n if (this._refreshing || !this._mapAvailable) return;\n this._refreshing = true;\n this._setStatus('Loading locations\\u2026');\n try {\n const data = await this._fetchSummary();\n this._applyMarkers(data);\n this._setStatus('');\n } catch (error) {\n console.error('LoginLocationMapView refresh error', error);\n this._setStatus('Unable to load login locations.');\n } finally {\n this._refreshing = false;\n }\n }\n\n async _fetchSummary(countryCode = null) {\n const rest = this.getApp()?.rest;\n if (!rest) throw new Error('REST client unavailable');\n\n const params = {};\n if (this.drStart) params.dr_start = this.drStart;\n if (this.drEnd) params.dr_end = this.drEnd;\n\n let url;\n if (this.userId) {\n url = '/api/account/logins/user';\n params.user_id = this.userId;\n } else {\n url = '/api/account/logins/summary';\n }\n\n if (countryCode) {\n params.country_code = countryCode;\n params.region = true;\n }\n\n const response = await rest.GET(url, params);\n if (!response.success || !response.data?.status) {\n throw new Error(response.data?.error || 'Login summary API error');\n }\n return response.data.data || [];\n }\n\n // ── Marker rendering ─────────────────────────────\n\n _applyMarkers(entries) {\n if (!entries.length) {\n this.mapView.updateMarkers([]);\n this._setStatus('No login locations found.');\n return;\n }\n\n const maxCount = Math.max(...entries.map(e => e.count));\n\n const markers = entries\n .filter(e => e.latitude && e.longitude)\n .map(entry => {\n const intensity = entry.count / (maxCount || 1);\n const markerSize = Math.round(18 + intensity * 26);\n const isRegion = !!entry.region;\n const label = isRegion ? entry.region : entry.country_code;\n const newCount = isRegion ? (entry.new_region_count || 0) : (entry.new_country_count || 0);\n const newLabel = isRegion ? 'new region' : 'new country';\n\n const popup = `\n <div class=\"text-center\" style=\"min-width:120px;\">\n <div class=\"fw-semibold\">${label}</div>\n <div class=\"text-muted\">${entry.count.toLocaleString()} login${entry.count !== 1 ? 's' : ''}</div>\n ${newCount > 0 ? `<div><span class=\"badge bg-warning text-dark\" style=\"font-size:0.65rem;\">${newCount} ${newLabel}</span></div>` : ''}\n </div>\n `;\n\n return {\n lng: entry.longitude,\n lat: entry.latitude,\n size: markerSize,\n color: this._getMarkerColor(intensity),\n popup,\n _countryCode: entry.country_code,\n _isRegion: isRegion\n };\n });\n\n this.mapView.updateMarkers(markers);\n\n // Attach click handlers for drill-down (country → region)\n if (!this._drillCountry) {\n this._attachMarkerClicks(markers);\n }\n }\n\n _attachMarkerClicks(markers) {\n if (!this.mapView?.mapMarkers) return;\n this.mapView.mapMarkers.forEach((glMarker, i) => {\n const data = markers[i];\n if (!data || data._isRegion) return;\n const el = glMarker.getElement();\n if (el) {\n el.addEventListener('dblclick', (e) => {\n e.stopPropagation();\n this.drillDown(data._countryCode);\n });\n }\n });\n }\n\n _getMarkerColor(intensity) {\n const start = [32, 201, 151]; // teal\n const end = [255, 193, 7]; // amber\n const mix = start.map((s, i) => Math.round(s + (end[i] - s) * intensity));\n return `rgba(${mix[0]}, ${mix[1]}, ${mix[2]}, 0.9)`;\n }\n\n // ── Drill-down ───────────────────────────────────\n\n async drillDown(countryCode) {\n if (this._refreshing) return;\n this._drillCountry = countryCode;\n this._showDrillBar(countryCode);\n this._refreshing = true;\n this._setStatus('Loading regions\\u2026');\n try {\n const data = await this._fetchSummary(countryCode);\n this._applyMarkers(data);\n this._setStatus('');\n // Fit to region markers\n if (this.mapView?.markers?.length > 1) {\n this.mapView.fitBounds();\n }\n } catch (error) {\n console.error('LoginLocationMapView drillDown error', error);\n this._setStatus('Unable to load region data.');\n } finally {\n this._refreshing = false;\n }\n }\n\n async onActionResetDrillDown() {\n this._drillCountry = null;\n this._hideDrillBar();\n await this.refresh();\n }\n\n _showDrillBar(countryCode) {\n const bar = this.element?.querySelector('[data-region=\"drill-bar\"]');\n const label = this.element?.querySelector('[data-region=\"drill-label\"]');\n if (bar) bar.classList.replace('d-none', 'd-flex');\n if (label) label.textContent = `Regions in ${countryCode}`;\n }\n\n _hideDrillBar() {\n const bar = this.element?.querySelector('[data-region=\"drill-bar\"]');\n if (bar) bar.classList.replace('d-flex', 'd-none');\n }\n\n // ── Tab activation (for hidden-container resize + refresh) ─\n\n async onTabActivated() {\n if (this.mapView?.map) {\n this.mapView.map.resize();\n }\n await this.refresh();\n }\n\n // ── Helpers ──────────────────────────────────────\n\n _setStatus(message) {\n const el = this.element?.querySelector('[data-region=\"status\"]');\n if (!el) return;\n el.textContent = message || '';\n el.style.display = message ? 'block' : 'none';\n }\n}\n\nexport default LoginLocationMapView;\n","/**\n * AdminProfileSection - Contact, verification overrides, and account overview\n *\n * Admin can view/edit contact info (email, phone), override verification\n * status, and see account metadata (username, status, role, MFA, dates).\n * Uses model.save() against /api/user/<id> (admin endpoint).\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nexport default class AdminProfileSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-profile-section',\n template: `\n <style>\n .ap-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .ap-section-label:first-child { margin-top: 0; }\n .ap-field-row { display: flex; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #f0f0f0; }\n .ap-field-row:last-child { border-bottom: none; }\n .ap-field-label { width: 140px; font-size: 0.8rem; color: #6c757d; flex-shrink: 0; }\n .ap-field-value { flex: 1; font-size: 0.88rem; color: #212529; display: flex; align-items: center; gap: 0.4rem; }\n .ap-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .ap-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n .ap-badge-ok { font-size: 0.65rem; padding: 0.15em 0.45em; background: #d1e7dd; color: #0f5132; border-radius: 3px; }\n .ap-badge-warn { font-size: 0.65rem; padding: 0.15em 0.45em; background: #fff3cd; color: #856404; border-radius: 3px; }\n .ap-badge-muted { font-size: 0.65rem; padding: 0.15em 0.45em; background: #f0f0f0; color: #6c757d; border-radius: 3px; }\n .ap-not-set { color: #adb5bd; font-style: italic; font-size: 0.85rem; }\n </style>\n\n <!-- Contact & Verification -->\n <div class=\"ap-section-label\">Contact & Verification</div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">Email</div>\n <div class=\"ap-field-value\">\n {{model.email}}\n {{#model.is_email_verified|bool}}\n <span class=\"ap-badge-ok\">Verified</span>\n {{/model.is_email_verified|bool}}\n {{^model.is_email_verified|bool}}\n <span class=\"ap-badge-warn\">Unverified</span>\n {{/model.is_email_verified|bool}}\n </div>\n {{#model.is_email_verified|bool}}\n <button type=\"button\" class=\"ap-field-action\" data-action=\"unverify-email\" title=\"Mark as unverified\"><i class=\"bi bi-x-circle\"></i></button>\n {{/model.is_email_verified|bool}}\n {{^model.is_email_verified|bool}}\n <button type=\"button\" class=\"ap-field-action\" data-action=\"force-verify-email\" title=\"Force verify\"><i class=\"bi bi-patch-check\"></i></button>\n {{/model.is_email_verified|bool}}\n <button type=\"button\" class=\"ap-field-action\" data-action=\"change-email\" title=\"Change email\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">Phone</div>\n <div class=\"ap-field-value\">\n {{#hasPhone|bool}}\n {{model.phone_number}}\n {{#model.is_phone_verified|bool}}\n <span class=\"ap-badge-ok\">Verified</span>\n {{/model.is_phone_verified|bool}}\n {{^model.is_phone_verified|bool}}\n <span class=\"ap-badge-warn\">Unverified</span>\n {{/model.is_phone_verified|bool}}\n {{/hasPhone|bool}}\n {{^hasPhone|bool}}\n <span class=\"ap-not-set\">Not set</span>\n {{/hasPhone|bool}}\n </div>\n {{#hasPhone|bool}}\n {{#model.is_phone_verified|bool}}\n <button type=\"button\" class=\"ap-field-action\" data-action=\"unverify-phone\" title=\"Mark as unverified\"><i class=\"bi bi-x-circle\"></i></button>\n {{/model.is_phone_verified|bool}}\n {{^model.is_phone_verified|bool}}\n <button type=\"button\" class=\"ap-field-action\" data-action=\"force-verify-phone\" title=\"Force verify\"><i class=\"bi bi-patch-check\"></i></button>\n {{/model.is_phone_verified|bool}}\n <button type=\"button\" class=\"ap-field-action\" data-action=\"change-phone\" title=\"Change phone\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"ap-field-action\" data-action=\"remove-phone\" title=\"Remove phone\"><i class=\"bi bi-x-lg\"></i></button>\n {{/hasPhone|bool}}\n {{^hasPhone|bool}}\n <button type=\"button\" class=\"ap-field-action\" data-action=\"set-phone\" title=\"Set phone number\"><i class=\"bi bi-plus\"></i></button>\n {{/hasPhone|bool}}\n </div>\n\n <!-- Account -->\n <div class=\"ap-section-label\">Account</div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">Username</div>\n <div class=\"ap-field-value\">{{model.username}}</div>\n <button type=\"button\" class=\"ap-field-action\" data-action=\"edit-username\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">Status</div>\n <div class=\"ap-field-value\">\n {{#model.is_active|bool}}<span class=\"ap-badge-ok\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"ap-badge-warn\">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">Role</div>\n <div class=\"ap-field-value\">\n {{roleLabel}}\n {{#model.is_staff|bool}}<span class=\"ap-badge-muted\">Staff</span>{{/model.is_staff|bool}}\n </div>\n </div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">MFA</div>\n <div class=\"ap-field-value\">\n {{#model.requires_mfa|bool}}<span class=\"ap-badge-ok\">Required</span>{{/model.requires_mfa|bool}}\n {{^model.requires_mfa|bool}}<span class=\"ap-badge-muted\">Not required</span>{{/model.requires_mfa|bool}}\n </div>\n </div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">Member Since</div>\n <div class=\"ap-field-value\">{{model.date_joined|date}}</div>\n </div>\n <div class=\"ap-field-row\">\n <div class=\"ap-field-label\">Last Login</div>\n <div class=\"ap-field-value\">{{model.last_login|relative}}</div>\n </div>\n `,\n ...options\n });\n }\n\n // ── Computed properties ─────────────────────────\n\n get hasPhone() {\n return !!(this.model && this.model.get('phone_number'));\n }\n\n get roleLabel() {\n if (!this.model) return 'User';\n if (this.model.get('is_superuser')) return 'Superuser';\n return 'User';\n }\n\n // ── Verification overrides ──────────────────────\n\n async onActionForceVerifyEmail() {\n const confirmed = await Modal.confirm(\n `Mark <strong>${this.model.get('email')}</strong> as verified? This bypasses the normal verification flow.`,\n 'Force Verify Email'\n );\n if (!confirmed) return true;\n await this._saveField({ is_email_verified: true }, 'Email marked as verified');\n return true;\n }\n\n async onActionUnverifyEmail() {\n const confirmed = await Modal.confirm(\n 'Mark email as unverified? The user will need to re-verify their email.',\n 'Unverify Email'\n );\n if (!confirmed) return true;\n await this._saveField({ is_email_verified: false }, 'Email marked as unverified');\n return true;\n }\n\n async onActionForceVerifyPhone() {\n const confirmed = await Modal.confirm(\n `Mark <strong>${this.model.get('phone_number')}</strong> as verified? This bypasses the normal verification flow.`,\n 'Force Verify Phone'\n );\n if (!confirmed) return true;\n await this._saveField({ is_phone_verified: true }, 'Phone marked as verified');\n return true;\n }\n\n async onActionUnverifyPhone() {\n const confirmed = await Modal.confirm(\n 'Mark phone as unverified? The user will need to re-verify their phone number.',\n 'Unverify Phone'\n );\n if (!confirmed) return true;\n await this._saveField({ is_phone_verified: false }, 'Phone marked as unverified');\n return true;\n }\n\n // ── Contact editing ─────────────────────────────\n\n async onActionChangeEmail() {\n const email = await Modal.prompt(\n 'Enter the new email address for this user:',\n 'Change Email',\n { defaultValue: this.model.get('email') || '' }\n );\n if (email === null || !email.trim()) return true;\n await this._saveField({ email: email.trim() }, 'Email updated');\n return true;\n }\n\n async onActionChangePhone() {\n const phone = await Modal.prompt(\n 'Enter the new phone number for this user:',\n 'Change Phone',\n { defaultValue: this.model.get('phone_number') || '' }\n );\n if (phone === null || !phone.trim()) return true;\n await this._saveField({ phone_number: phone.trim() }, 'Phone number updated');\n return true;\n }\n\n async onActionSetPhone() {\n const phone = await Modal.prompt(\n 'Enter a phone number for this user:',\n 'Set Phone Number',\n { placeholder: '(415) 555-0123' }\n );\n if (!phone || !phone.trim()) return true;\n await this._saveField({ phone_number: phone.trim() }, 'Phone number added');\n return true;\n }\n\n async onActionRemovePhone() {\n const confirmed = await Modal.confirm(\n 'Remove this user\\'s phone number?',\n 'Remove Phone'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ phone_number: null });\n if (resp.status === 200) {\n this.model.set('is_phone_verified', false);\n this.getApp()?.toast?.success('Phone number removed');\n await this.render();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to remove phone number');\n }\n return true;\n }\n\n async onActionEditUsername() {\n const username = await Modal.prompt(\n 'Username:',\n 'Edit Username',\n { defaultValue: this.model.get('username') || '' }\n );\n if (username !== null && username.trim()) {\n await this._saveField({ username: username.trim() }, 'Username updated');\n }\n return true;\n }\n\n // ── Helpers ─────────────────────────────────────\n\n async _saveField(fields, successMsg) {\n const resp = await this.model.save(fields);\n if (resp.status === 200) {\n this.getApp()?.toast?.success(successMsg);\n await this.render();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to save');\n }\n }\n}\n","/**\n * AdminPersonalSection - Personal information editing\n *\n * Admin can view/edit name fields, DOB, timezone, and address.\n * Uses model.save() against /api/user/<id> (admin endpoint).\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nexport default class AdminPersonalSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-personal-section',\n template: `\n <style>\n .aps-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .aps-section-label:first-child { margin-top: 0; }\n .aps-field-row { display: flex; align-items: center; padding: 0.6rem 0; border-bottom: 1px solid #f0f0f0; }\n .aps-field-row:last-child { border-bottom: none; }\n .aps-field-label { width: 140px; font-size: 0.8rem; color: #6c757d; flex-shrink: 0; }\n .aps-field-value { flex: 1; font-size: 0.88rem; color: #212529; display: flex; align-items: center; gap: 0.4rem; }\n .aps-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .aps-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n .aps-badge-ok { font-size: 0.65rem; padding: 0.15em 0.45em; background: #d1e7dd; color: #0f5132; border-radius: 3px; }\n .aps-badge-warn { font-size: 0.65rem; padding: 0.15em 0.45em; background: #fff3cd; color: #856404; border-radius: 3px; }\n .aps-not-set { color: #adb5bd; font-style: italic; font-size: 0.85rem; }\n </style>\n\n <!-- Name -->\n <div class=\"aps-section-label\">Name</div>\n <div class=\"aps-field-row\">\n <div class=\"aps-field-label\">Display Name</div>\n <div class=\"aps-field-value\">\n {{#model.display_name}}{{model.display_name}}{{/model.display_name}}\n {{^model.display_name}}<span class=\"aps-not-set\">Not set</span>{{/model.display_name}}\n </div>\n <button type=\"button\" class=\"aps-field-action\" data-action=\"edit-display-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"aps-field-row\">\n <div class=\"aps-field-label\">First Name</div>\n <div class=\"aps-field-value\">\n {{#model.first_name}}{{model.first_name}}{{/model.first_name}}\n {{^model.first_name}}<span class=\"aps-not-set\">Not set</span>{{/model.first_name}}\n </div>\n <button type=\"button\" class=\"aps-field-action\" data-action=\"edit-first-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"aps-field-row\">\n <div class=\"aps-field-label\">Last Name</div>\n <div class=\"aps-field-value\">\n {{#model.last_name}}{{model.last_name}}{{/model.last_name}}\n {{^model.last_name}}<span class=\"aps-not-set\">Not set</span>{{/model.last_name}}\n </div>\n <button type=\"button\" class=\"aps-field-action\" data-action=\"edit-last-name\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n\n <!-- Details -->\n <div class=\"aps-section-label\">Details</div>\n <div class=\"aps-field-row\">\n <div class=\"aps-field-label\">Date of Birth</div>\n <div class=\"aps-field-value\">\n {{#hasDob|bool}}\n {{dobFormatted}}\n {{#model.is_dob_verified|bool}}<span class=\"aps-badge-ok\">Verified</span>{{/model.is_dob_verified|bool}}\n {{^model.is_dob_verified|bool}}<span class=\"aps-badge-warn\">Unverified</span>{{/model.is_dob_verified|bool}}\n {{/hasDob|bool}}\n {{^hasDob|bool}}<span class=\"aps-not-set\">Not set</span>{{/hasDob|bool}}\n </div>\n {{#hasDob|bool}}\n {{#model.is_dob_verified|bool}}\n <button type=\"button\" class=\"aps-field-action\" data-action=\"unverify-dob\" title=\"Mark as unverified\"><i class=\"bi bi-x-circle\"></i></button>\n {{/model.is_dob_verified|bool}}\n {{^model.is_dob_verified|bool}}\n <button type=\"button\" class=\"aps-field-action\" data-action=\"force-verify-dob\" title=\"Force verify\"><i class=\"bi bi-patch-check\"></i></button>\n {{/model.is_dob_verified|bool}}\n {{/hasDob|bool}}\n <button type=\"button\" class=\"aps-field-action\" data-action=\"edit-dob\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"aps-field-row\">\n <div class=\"aps-field-label\">Timezone</div>\n <div class=\"aps-field-value\">{{timezoneDisplay}}</div>\n <button type=\"button\" class=\"aps-field-action\" data-action=\"edit-timezone\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n\n <!-- Address -->\n <div class=\"aps-section-label\">Address</div>\n <div class=\"aps-field-row\">\n <div class=\"aps-field-label\">Address</div>\n <div class=\"aps-field-value\">\n {{#hasAddress|bool}}{{addressSummary}}{{/hasAddress|bool}}\n {{^hasAddress|bool}}<span class=\"aps-not-set\">Not set</span>{{/hasAddress|bool}}\n </div>\n <button type=\"button\" class=\"aps-field-action\" data-action=\"edit-address\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n `,\n ...options\n });\n }\n\n // ── Computed properties ─────────────────────────\n\n get hasDob() {\n return !!this.model?.get('dob');\n }\n\n get dobFormatted() {\n const dob = this.model?.get('dob');\n if (!dob) return '';\n try {\n const [year, month, day] = dob.split('-');\n return `${month}/${day}/${year}`;\n } catch {\n return dob;\n }\n }\n\n get timezoneDisplay() {\n const meta = this.model?.get('metadata') || {};\n return meta.timezone || 'Not set';\n }\n\n get hasAddress() {\n const meta = this.model?.get('metadata') || {};\n return !!(meta.street || meta.city || meta.state || meta.zip || meta.country);\n }\n\n get addressSummary() {\n const meta = this.model?.get('metadata') || {};\n return [meta.street, meta.city, meta.state, meta.zip, meta.country].filter(Boolean).join(', ');\n }\n\n // ── Verification overrides ────────────────────────\n\n async onActionForceVerifyDob() {\n const confirmed = await Modal.confirm(\n 'Mark date of birth as verified?',\n 'Force Verify DOB'\n );\n if (!confirmed) return true;\n await this._saveField({ is_dob_verified: true }, 'DOB marked as verified');\n return true;\n }\n\n async onActionUnverifyDob() {\n const confirmed = await Modal.confirm(\n 'Mark date of birth as unverified?',\n 'Unverify DOB'\n );\n if (!confirmed) return true;\n await this._saveField({ is_dob_verified: false }, 'DOB marked as unverified');\n return true;\n }\n\n // ── Action handlers ─────────────────────────────\n\n async onActionEditDisplayName() {\n const name = await Modal.prompt(\n 'Display name:',\n 'Edit Display Name',\n { defaultValue: this.model.get('display_name') || '' }\n );\n if (name !== null && name.trim()) {\n await this._saveField({ display_name: name.trim() }, 'Display name');\n }\n return true;\n }\n\n async onActionEditFirstName() {\n const name = await Modal.prompt(\n 'First name:',\n 'Edit First Name',\n { defaultValue: this.model.get('first_name') || '' }\n );\n if (name !== null) {\n await this._saveField({ first_name: name.trim() }, 'First name');\n }\n return true;\n }\n\n async onActionEditLastName() {\n const name = await Modal.prompt(\n 'Last name:',\n 'Edit Last Name',\n { defaultValue: this.model.get('last_name') || '' }\n );\n if (name !== null) {\n await this._saveField({ last_name: name.trim() }, 'Last name');\n }\n return true;\n }\n\n async onActionEditDob() {\n const data = await Modal.form({\n title: 'Date of Birth',\n size: 'sm',\n fields: [{ name: 'dob', type: 'date', label: 'Date of Birth', cols: 12 }],\n data: { dob: this.model.get('dob') || '' }\n });\n if (!data) return true;\n await this._saveField({ dob: data.dob || null }, 'Date of birth');\n return true;\n }\n\n async onActionEditTimezone() {\n const meta = this.model.get('metadata') || {};\n const data = await Modal.form({\n title: 'Change Timezone',\n size: 'sm',\n fields: [{\n name: 'timezone',\n type: 'select',\n label: 'Timezone',\n cols: 12,\n options: [\n { value: 'America/New_York', text: 'Eastern Time (ET)' },\n { value: 'America/Chicago', text: 'Central Time (CT)' },\n { value: 'America/Denver', text: 'Mountain Time (MT)' },\n { value: 'America/Los_Angeles', text: 'Pacific Time (PT)' },\n { value: 'America/Anchorage', text: 'Alaska Time (AKT)' },\n { value: 'Pacific/Honolulu', text: 'Hawaii Time (HT)' },\n { value: 'UTC', text: 'UTC' },\n { value: 'Europe/London', text: 'London (GMT/BST)' },\n { value: 'Europe/Paris', text: 'Paris (CET/CEST)' },\n { value: 'Europe/Berlin', text: 'Berlin (CET/CEST)' },\n { value: 'Asia/Tokyo', text: 'Tokyo (JST)' },\n { value: 'Asia/Shanghai', text: 'Shanghai (CST)' },\n { value: 'Australia/Sydney', text: 'Sydney (AEST)' }\n ]\n }],\n data: { timezone: meta.timezone || '' }\n });\n if (!data) return true;\n await this._saveField({ metadata: { ...meta, timezone: data.timezone } }, 'Timezone');\n return true;\n }\n\n async onActionEditAddress() {\n const meta = this.model.get('metadata') || {};\n const data = await Modal.form({\n title: 'Edit Address',\n size: 'md',\n fields: [\n { name: 'street', type: 'text', label: 'Street', placeholder: '123 Main St', cols: 12 },\n { name: 'city', type: 'text', label: 'City', cols: 6 },\n { name: 'state', type: 'text', label: 'State / Province', cols: 6 },\n { name: 'zip', type: 'text', label: 'Zip / Postal Code', cols: 6 },\n { name: 'country', type: 'text', label: 'Country', cols: 6 }\n ],\n data: { street: meta.street || '', city: meta.city || '', state: meta.state || '', zip: meta.zip || '', country: meta.country || '' }\n });\n if (!data) return true;\n\n const updatedMeta = { ...meta, street: data.street || '', city: data.city || '', state: data.state || '', zip: data.zip || '', country: data.country || '' };\n await this._saveField({ metadata: updatedMeta }, 'Address');\n return true;\n }\n\n // ── Helpers ─────────────────────────────────────\n\n async _saveField(fields, label) {\n const resp = await this.model.save(fields);\n if (resp.status === 200) {\n this.getApp()?.toast?.success(`${label} updated`);\n await this.render();\n } else {\n this.getApp()?.toast?.error(resp.message || `Failed to update ${label.toLowerCase()}`);\n }\n }\n}\n","/**\n * AdminSecuritySection - Admin security management for a user\n *\n * Admin powers: force password reset, enable/disable MFA,\n * revoke all sessions, view/delete passkeys, view recovery codes.\n * Uses admin endpoints and model.save() against /api/user/<id>.\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport rest from '@core/Rest.js';\nimport { PasskeyList, PasskeyForms } from '@core/models/Passkeys.js';\n\nexport default class AdminSecuritySection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-security-section',\n template: `\n <style>\n .as-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.75rem; }\n .as-section-label:first-child { margin-top: 0; }\n .as-item { display: flex; align-items: center; gap: 0.85rem; padding: 0.85rem 1rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.5rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; }\n .as-item:hover { border-color: #dee2e6; background: #fafbfd; }\n .as-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; }\n .as-info { flex: 1; min-width: 0; }\n .as-title { font-weight: 600; font-size: 0.88rem; }\n .as-desc { font-size: 0.78rem; color: #6c757d; }\n .as-badge { font-size: 0.72rem; padding: 0.15em 0.5em; border-radius: 3px; flex-shrink: 0; }\n .as-chevron { color: #ced4da; font-size: 0.8rem; flex-shrink: 0; }\n </style>\n\n <div class=\"as-section-label\">Authentication</div>\n\n <div class=\"as-item\" data-action=\"send-password-reset\">\n <div class=\"as-icon bg-primary bg-opacity-10 text-primary\"><i class=\"bi bi-envelope\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Send Password Reset</div>\n <div class=\"as-desc\">Send a password reset email to {{model.email}}</div>\n </div>\n <span class=\"as-badge bg-light text-muted border\">Send</span>\n </div>\n\n <div class=\"as-item\" data-action=\"send-magic-link\">\n <div class=\"as-icon\" style=\"background: rgba(13,110,253,0.1); color: #0d6efd;\"><i class=\"bi bi-link-45deg\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Send Magic Login Link</div>\n <div class=\"as-desc\">Send a one-click login link to {{model.email}}</div>\n </div>\n <span class=\"as-badge bg-light text-muted border\">Send</span>\n </div>\n\n {{^model.is_email_verified|bool}}\n <div class=\"as-item\" data-action=\"send-email-verification\">\n <div class=\"as-icon\" style=\"background: rgba(25,135,84,0.1); color: #198754;\"><i class=\"bi bi-envelope-check\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Send Email Verification</div>\n <div class=\"as-desc\">Send a verification email to {{model.email}}</div>\n </div>\n <span class=\"as-badge bg-light text-muted border\">Send</span>\n </div>\n {{/model.is_email_verified|bool}}\n\n <div class=\"as-item\" data-action=\"set-password\">\n <div class=\"as-icon bg-warning bg-opacity-10 text-warning\"><i class=\"bi bi-key\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Set Password</div>\n <div class=\"as-desc\">Set a new password directly for this user</div>\n </div>\n <span class=\"as-badge bg-light text-muted border\">Set</span>\n </div>\n\n <div class=\"as-section-label\">Multi-Factor Authentication</div>\n\n <div class=\"as-item\" data-action=\"toggle-mfa\">\n <div class=\"as-icon\" style=\"background: rgba(111,66,193,0.1); color: #6f42c1;\"><i class=\"bi bi-shield-lock\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">MFA Requirement</div>\n <div class=\"as-desc\">\n {{#model.requires_mfa|bool}}User is required to use MFA{{/model.requires_mfa|bool}}\n {{^model.requires_mfa|bool}}MFA is not required for this user{{/model.requires_mfa|bool}}\n </div>\n </div>\n {{#model.requires_mfa|bool}}\n <span class=\"as-badge bg-success bg-opacity-10 text-success border\">Enabled</span>\n {{/model.requires_mfa|bool}}\n {{^model.requires_mfa|bool}}\n <span class=\"as-badge bg-light text-muted border\">Disabled</span>\n {{/model.requires_mfa|bool}}\n </div>\n\n <div class=\"as-item\" data-action=\"manage-passkeys\">\n <div class=\"as-icon bg-success bg-opacity-10 text-success\"><i class=\"bi bi-fingerprint\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Passkeys</div>\n <div class=\"as-desc\">View and manage registered passkeys</div>\n </div>\n <i class=\"bi bi-chevron-right as-chevron\"></i>\n </div>\n\n {{#model.requires_mfa|bool}}\n <div class=\"as-item\" data-action=\"view-recovery-codes\">\n <div class=\"as-icon\" style=\"background: rgba(111,66,193,0.1); color: #6f42c1;\"><i class=\"bi bi-file-earmark-lock\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Recovery Codes</div>\n <div class=\"as-desc\">View remaining recovery codes</div>\n </div>\n <i class=\"bi bi-chevron-right as-chevron\"></i>\n </div>\n\n <div class=\"as-item\" data-action=\"disable-totp\">\n <div class=\"as-icon\" style=\"background: rgba(220,53,69,0.1); color: #dc3545;\"><i class=\"bi bi-shield-x\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Disable Authenticator</div>\n <div class=\"as-desc\">Remove TOTP requirement for this user</div>\n </div>\n </div>\n {{/model.requires_mfa|bool}}\n\n <div class=\"as-section-label\">Sessions</div>\n\n <div class=\"as-item\" data-action=\"revoke-all-sessions\">\n <div class=\"as-icon\" style=\"background: rgba(220,53,69,0.1); color: #dc3545;\"><i class=\"bi bi-box-arrow-right\"></i></div>\n <div class=\"as-info\">\n <div class=\"as-title\">Revoke All Sessions</div>\n <div class=\"as-desc\">Force sign-out from all devices</div>\n </div>\n </div>\n `,\n ...options\n });\n }\n\n // ── Password actions ────────────────────────────\n\n async onActionSendPasswordReset() {\n const app = this.getApp();\n const email = this.model.get('email');\n\n const confirmed = await Modal.confirm(\n `Send a password reset email to <strong>${email}</strong>?`,\n 'Send Password Reset'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/auth/password/reset', { email });\n if (resp.success) {\n app?.toast?.success('Password reset email sent');\n } else {\n app?.toast?.error(resp.message || 'Failed to send password reset');\n }\n return true;\n }\n\n async onActionSendEmailVerification() {\n const app = this.getApp();\n const email = this.model.get('email');\n\n const confirmed = await Modal.confirm(\n `Send a verification email to <strong>${email}</strong>?`,\n 'Send Email Verification'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/auth/email/verify', { email });\n if (resp.success) {\n app?.toast?.success('Verification email sent');\n } else {\n app?.toast?.error(resp.message || 'Failed to send verification email');\n }\n return true;\n }\n\n async onActionSendMagicLink() {\n const app = this.getApp();\n const email = this.model.get('email');\n\n const confirmed = await Modal.confirm(\n `Send a magic login link to <strong>${email}</strong>? They will be able to sign in with one click.`,\n 'Send Magic Login Link'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/auth/magic-link', { email });\n if (resp.success) {\n app?.toast?.success('Magic login link sent');\n } else {\n app?.toast?.error(resp.message || 'Failed to send magic link');\n }\n return true;\n }\n\n async onActionSetPassword() {\n const app = this.getApp();\n const data = await Modal.form({\n title: 'Set Password',\n size: 'sm',\n fields: [\n { name: 'password', type: 'password', label: 'New Password', required: true, cols: 12, help: 'Set a new password for this user.' },\n { name: 'confirm', type: 'password', label: 'Confirm Password', required: true, cols: 12 }\n ]\n });\n if (!data) return true;\n\n if (data.password !== data.confirm) {\n app?.toast?.error('Passwords do not match');\n return true;\n }\n\n const resp = await this.model.save({ password: data.password });\n if (resp.status === 200) {\n app?.toast?.success('Password updated');\n } else {\n app?.toast?.error(resp.message || 'Failed to set password');\n }\n return true;\n }\n\n // ── MFA actions ─────────────────────────────────\n\n async onActionToggleMfa() {\n const app = this.getApp();\n const currentMfa = this.model.get('requires_mfa');\n const action = currentMfa ? 'disable' : 'enable';\n\n const confirmed = await Modal.confirm(\n `${currentMfa ? 'Disable' : 'Enable'} MFA requirement for this user?`,\n `${currentMfa ? 'Disable' : 'Enable'} MFA`\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ requires_mfa: !currentMfa });\n if (resp.status === 200) {\n app?.toast?.success(`MFA ${action}d`);\n await this.render();\n } else {\n app?.toast?.error(resp.message || `Failed to ${action} MFA`);\n }\n return true;\n }\n\n async onActionDisableTotp() {\n const app = this.getApp();\n const confirmed = await Modal.confirm(\n 'Disable the authenticator app for this user? They will no longer need a TOTP code to sign in.',\n 'Disable Authenticator'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE(`/api/user/${this.model.id}/totp`);\n if (resp.success) {\n this.model.set('requires_mfa', false);\n app?.toast?.success('Authenticator disabled');\n await this.render();\n } else {\n app?.toast?.error(resp.message || 'Failed to disable authenticator');\n }\n return true;\n }\n\n async onActionManagePasskeys() {\n const collection = new PasskeyList({ params: { user: this.model.id } });\n try {\n await collection.fetch();\n } catch (e) {\n // ignore\n }\n\n const items = collection.models || [];\n const view = new View({\n template: `\n <style>\n .pk-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 0.75rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.4rem; }\n .pk-icon { width: 32px; height: 32px; background: #e7f1ff; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #0d6efd; font-size: 0.9rem; flex-shrink: 0; }\n .pk-info { flex: 1; min-width: 0; }\n .pk-name { font-weight: 600; font-size: 0.85rem; }\n .pk-meta { font-size: 0.73rem; color: #6c757d; }\n .pk-actions .btn { padding: 0.2rem 0.4rem; font-size: 0.75rem; }\n .pk-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .pk-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n {{#passkeys}}\n <div class=\"pk-row\">\n <div class=\"pk-icon\"><i class=\"bi bi-fingerprint\"></i></div>\n <div class=\"pk-info\">\n <div class=\"pk-name\">{{.friendly_name|default:'Unnamed Passkey'}}</div>\n <div class=\"pk-meta\">Created {{.created|date}} · Last used {{.last_used|relative|default:'never'}} · {{.sign_count}} uses</div>\n </div>\n <div class=\"pk-actions\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"edit-passkey\" data-id=\"{{.id}}\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"delete-passkey\" data-id=\"{{.id}}\" title=\"Delete\"><i class=\"bi bi-trash\"></i></button>\n </div>\n </div>\n {{/passkeys}}\n {{^passkeys|bool}}\n <div class=\"pk-empty\">\n <i class=\"bi bi-fingerprint\"></i>\n No passkeys registered\n </div>\n {{/passkeys|bool}}\n `\n });\n view.passkeys = items.map(p => p.toJSON ? p.toJSON() : p);\n\n view.onActionEditPasskey = async (event, el) => {\n const id = el.dataset.id;\n const passkey = items.find(p => String(p.id) === String(id));\n if (passkey) {\n await Modal.modelForm({ title: 'Edit Passkey', model: passkey, fields: PasskeyForms.edit.fields, size: 'sm' });\n }\n return true;\n };\n\n view.onActionDeletePasskey = async (event, el) => {\n const id = el.dataset.id;\n const confirmed = await Modal.confirm('Delete this passkey?', 'Delete Passkey');\n if (confirmed) {\n const passkey = items.find(p => String(p.id) === String(id));\n if (passkey) {\n await passkey.destroy();\n this.getApp()?.toast?.success('Passkey deleted');\n }\n }\n return true;\n };\n\n await Modal.dialog({\n title: 'Passkeys',\n body: view,\n size: 'md',\n buttons: [{ text: 'Close', class: 'btn-outline-secondary', dismiss: true }]\n });\n return true;\n }\n\n async onActionViewRecoveryCodes() {\n const app = this.getApp();\n const resp = await rest.GET(`/api/user/${this.model.id}/totp/recovery-codes`, {}, { dataOnly: true });\n if (!resp.success || !resp.data) {\n app?.toast?.error(resp.message || 'Failed to load recovery codes');\n return true;\n }\n\n const { remaining, codes } = resp.data;\n const view = new View({\n template: `\n <style>\n .rc-info { font-size: 0.82rem; color: #6c757d; margin-bottom: 1rem; }\n .rc-remaining { font-weight: 600; color: #495057; }\n .rc-list { display: grid; grid-template-columns: 1fr 1fr; gap: 0.3rem; }\n .rc-code { font-family: monospace; font-size: 0.85rem; padding: 0.35rem 0.6rem; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; text-align: center; }\n </style>\n <div class=\"rc-info\"><span class=\"rc-remaining\">{{remaining}}</span> recovery codes remaining</div>\n <div class=\"rc-list\">\n {{#codes}}<div class=\"rc-code\">{{.}}</div>{{/codes}}\n </div>\n `\n });\n view.remaining = remaining;\n view.codes = codes || [];\n\n await Modal.dialog({\n title: 'Recovery Codes',\n body: view,\n size: 'sm',\n buttons: [{ text: 'Close', class: 'btn-outline-secondary', dismiss: true }]\n });\n return true;\n }\n\n // ── Session actions ─────────────────────────────\n\n async onActionRevokeAllSessions() {\n const app = this.getApp();\n const confirmed = await Modal.confirm(\n 'Revoke all sessions for this user? They will be signed out of all devices immediately.',\n 'Revoke All Sessions'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST(`/api/user/${this.model.id}/sessions/revoke`);\n if (resp.success) {\n app?.toast?.success('All sessions revoked');\n } else {\n app?.toast?.error(resp.message || 'Failed to revoke sessions');\n }\n return true;\n }\n}\n","/**\n * AdminConnectedSection - Admin view of a user's OAuth connections\n *\n * View linked providers and unlink on behalf of user.\n * Uses admin-scoped endpoints.\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport rest from '@core/Rest.js';\n\nconst PROVIDER_ICONS = {\n google: 'bi-google',\n github: 'bi-github',\n microsoft: 'bi-microsoft',\n apple: 'bi-apple',\n facebook: 'bi-facebook',\n twitter: 'bi-twitter-x',\n linkedin: 'bi-linkedin'\n};\n\nexport default class AdminConnectedSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-connected-section',\n template: `\n <style>\n .ac-row { display: flex; align-items: center; gap: 0.85rem; padding: 0.85rem 1rem; border: 1px solid #f0f0f0; border-radius: 8px; margin-bottom: 0.5rem; }\n .ac-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; background: #f0f0f0; color: #495057; }\n .ac-info { flex: 1; min-width: 0; }\n .ac-provider { font-weight: 600; font-size: 0.88rem; text-transform: capitalize; }\n .ac-meta { font-size: 0.78rem; color: #6c757d; }\n .ac-actions .btn { font-size: 0.75rem; padding: 0.25rem 0.5rem; }\n .ac-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .ac-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n\n {{#connections}}\n <div class=\"ac-row\">\n <div class=\"ac-icon\"><i class=\"bi {{.icon}}\"></i></div>\n <div class=\"ac-info\">\n <div class=\"ac-provider\">{{.provider}}</div>\n <div class=\"ac-meta\">{{.email}} · Connected {{.created|relative}}</div>\n </div>\n <div class=\"ac-actions\">\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"unlink\" data-id=\"{{.id}}\" title=\"Unlink\"><i class=\"bi bi-x-lg me-1\"></i>Unlink</button>\n </div>\n </div>\n {{/connections}}\n {{^connections|bool}}\n <div class=\"ac-empty\">\n <i class=\"bi bi-plug\"></i>\n No connected accounts\n </div>\n {{/connections|bool}}\n `,\n ...options\n });\n this.connections = [];\n }\n\n async onBeforeRender() {\n try {\n const resp = await rest.GET('/api/account/oauth_connection', { user: this.model.id });\n const results = resp?.data?.results || resp?.data || [];\n this.connections = results.map(c => ({\n ...c,\n icon: PROVIDER_ICONS[c.provider] || 'bi-link-45deg'\n }));\n } catch (e) {\n this.connections = [];\n }\n }\n\n async onActionUnlink(event, el) {\n const id = el.dataset.id;\n const connection = this.connections.find(c => String(c.id) === String(id));\n const provider = connection?.provider || 'this account';\n\n const confirmed = await Modal.confirm(\n `Unlink ${provider} for this user?`,\n 'Unlink Account'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE(`/api/account/oauth_connection/${id}`);\n if (resp.success) {\n this.getApp()?.toast?.success(`${provider} account unlinked`);\n await this.render();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to unlink account');\n }\n return true;\n }\n}\n","/**\n * AdminNotificationsSection - Admin view/edit of user notification preferences\n *\n * Per-kind, per-channel toggle grid. Uses admin-scoped endpoints.\n */\nimport View from '@core/View.js';\nimport rest from '@core/Rest.js';\n\nconst CHANNEL_LABELS = {\n in_app: 'In-App',\n email: 'Email',\n push: 'Push'\n};\n\nconst CHANNELS = ['in_app', 'email', 'push'];\n\nexport default class AdminNotificationsSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-notifications-section',\n template: `\n <style>\n .an-table { width: 100%; border-collapse: collapse; }\n .an-table th { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; padding: 0.5rem 0.75rem; border-bottom: 2px solid #e9ecef; }\n .an-table th:first-child { text-align: left; }\n .an-table th:not(:first-child) { text-align: center; width: 80px; }\n .an-table td { padding: 0.65rem 0.75rem; border-bottom: 1px solid #f0f0f0; }\n .an-table td:first-child { font-size: 0.88rem; font-weight: 500; text-transform: capitalize; }\n .an-table td:not(:first-child) { text-align: center; }\n .an-table tr:last-child td { border-bottom: none; }\n .an-empty { text-align: center; padding: 2rem 1rem; color: #6c757d; }\n .an-empty i { font-size: 2rem; color: #ced4da; display: block; margin-bottom: 0.5rem; }\n </style>\n\n {{#hasPreferences|bool}}\n <table class=\"an-table\">\n <thead>\n <tr>\n <th>Type</th>\n {{#channels}}\n <th>{{.label}}</th>\n {{/channels}}\n </tr>\n </thead>\n <tbody>\n {{#preferenceRows}}\n <tr>\n <td>{{.kindLabel}}</td>\n {{#.toggles}}\n <td>\n <input type=\"checkbox\" class=\"form-check-input\"\n data-action=\"toggle-pref\"\n data-kind=\"{{.kind}}\"\n data-channel=\"{{.channel}}\"\n {{#.checked}}checked{{/.checked}}>\n </td>\n {{/.toggles}}\n </tr>\n {{/preferenceRows}}\n </tbody>\n </table>\n {{/hasPreferences|bool}}\n {{^hasPreferences|bool}}\n <div class=\"an-empty\">\n <i class=\"bi bi-bell\"></i>\n No notification preferences configured\n </div>\n {{/hasPreferences|bool}}\n `,\n ...options\n });\n this.preferences = {};\n }\n\n get channels() {\n return CHANNELS.map(ch => ({ key: ch, label: CHANNEL_LABELS[ch] || ch }));\n }\n\n get hasPreferences() {\n return Object.keys(this.preferences).length > 0;\n }\n\n get preferenceRows() {\n return Object.keys(this.preferences).sort().map(kind => ({\n kind,\n kindLabel: kind.replace(/[_-]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),\n toggles: CHANNELS.map(channel => ({\n kind,\n channel,\n checked: this.preferences[kind]?.[channel] !== false\n }))\n }));\n }\n\n async onBeforeRender() {\n try {\n const resp = await rest.GET('/api/account/notification/preferences', { user: this.model.id }, { dataOnly: true });\n this.preferences = resp?.data?.preferences || resp?.data || {};\n } catch (e) {\n this.preferences = {};\n }\n }\n\n async onActionTogglePref(event, el) {\n const kind = el.dataset.kind;\n const channel = el.dataset.channel;\n const checked = el.checked;\n\n if (!this.preferences[kind]) {\n this.preferences[kind] = {};\n }\n this.preferences[kind][channel] = checked;\n\n try {\n const resp = await rest.POST('/api/account/notification/preferences', {\n user: this.model.id,\n preferences: { [kind]: { [channel]: checked } }\n });\n if (!resp.success) {\n this.getApp()?.toast?.error(resp.message || 'Failed to update preference');\n el.checked = !checked;\n }\n } catch (e) {\n this.getApp()?.toast?.error('Failed to update preference');\n el.checked = !checked;\n }\n return true;\n }\n}\n","/**\n * AdminApiKeysSection - Admin view/manage of a user's API keys\n *\n * View existing keys, generate new keys on behalf of user, and revoke keys.\n * Uses standard CRUD endpoints with user filter.\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport rest from '@core/Rest.js';\n\nexport default class AdminApiKeysSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-api-keys-section',\n template: `\n <style>\n .aak-list { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }\n .aak-item { display: flex; align-items: center; padding: 0.75rem 1rem; border-bottom: 1px solid #f0f0f0; gap: 1rem; }\n .aak-item:last-child { border-bottom: none; }\n .aak-item-icon { color: #6c757d; font-size: 1.1rem; flex-shrink: 0; }\n .aak-item-info { flex: 1; min-width: 0; }\n .aak-item-name { font-weight: 600; font-size: 0.85rem; display: flex; align-items: center; gap: 0.5rem; }\n .aak-item-meta { font-size: 0.75rem; color: #6c757d; display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 0.15rem; }\n .aak-item-meta i { margin-right: 0.2rem; }\n .aak-empty { padding: 2rem; text-align: center; color: #6c757d; font-size: 0.85rem; }\n .aak-empty i { font-size: 1.5rem; display: block; margin-bottom: 0.5rem; }\n .aak-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n .aak-header h6 { margin: 0; font-weight: 600; }\n .aak-result { padding: 1rem; background: #d1e7dd; border: 1px solid #badbcc; border-radius: 8px; margin-bottom: 1rem; }\n .aak-result-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #0f5132; margin-bottom: 0.5rem; }\n .aak-token-wrap { display: flex; gap: 0.5rem; align-items: center; }\n .aak-token { flex: 1; font-family: monospace; font-size: 0.78rem; padding: 0.5rem 0.75rem; background: #fff; border: 1px solid #dee2e6; border-radius: 4px; word-break: break-all; max-height: 80px; overflow-y: auto; }\n .aak-token-warning { font-size: 0.75rem; color: #dc3545; margin-top: 0.5rem; font-weight: 600; }\n </style>\n\n <div id=\"aak-new-token\" style=\"display: none;\">\n <div class=\"aak-result\">\n <div class=\"aak-result-label\">Generated API Key</div>\n <div class=\"aak-token-wrap\">\n <div class=\"aak-token\" id=\"aak-token-display\"></div>\n <button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" data-action=\"copy-token\" title=\"Copy\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n </div>\n <div class=\"aak-token-warning\">\n <i class=\"bi bi-exclamation-circle me-1\"></i>This token will not be shown again. Copy it now.\n </div>\n </div>\n </div>\n\n <div class=\"aak-header\">\n <h6>API Keys</h6>\n <button type=\"button\" class=\"btn btn-primary btn-sm\" data-action=\"generate-key\">\n <i class=\"bi bi-plus-lg me-1\"></i>Generate Key\n </button>\n </div>\n\n <div id=\"aak-keys-list\"></div>\n `,\n ...options\n });\n this.apiKeys = [];\n this.generatedToken = null;\n }\n\n async onBeforeRender() {\n await this._loadKeys();\n }\n\n async _loadKeys() {\n try {\n const resp = await rest.GET('/api/account/api_keys', { user: this.model.id }, {}, { dataOnly: true });\n this.apiKeys = resp.success && Array.isArray(resp.data) ? resp.data : [];\n } catch (e) {\n this.apiKeys = [];\n }\n }\n\n onAfterRender() {\n this._renderKeysList();\n }\n\n _renderKeysList() {\n const container = this.element?.querySelector('#aak-keys-list');\n if (!container) return;\n\n if (!this.apiKeys.length) {\n container.innerHTML = `\n <div class=\"aak-list\">\n <div class=\"aak-empty\">\n <i class=\"bi bi-key\"></i>\n No API keys for this user\n </div>\n </div>`;\n return;\n }\n\n const rows = this.apiKeys.map(key => {\n const name = key.name || 'API Key';\n const created = key.created ? new Date(key.created * 1000).toLocaleDateString() : '';\n const expires = key.expires ? new Date(key.expires * 1000).toLocaleDateString() : 'Never';\n const lastUsed = key.last_used ? new Date(key.last_used * 1000).toLocaleDateString() : 'Never';\n const ips = key.allowed_ips?.length ? key.allowed_ips.join(', ') : 'Any';\n const isActive = key.is_active !== false;\n const statusBadge = isActive\n ? '<span class=\"badge bg-success\">Active</span>'\n : '<span class=\"badge bg-secondary\">Inactive</span>';\n const tokenPreview = key.token_prefix ? `${key.token_prefix}...` : '••••••••';\n\n return `\n <div class=\"aak-item\">\n <div class=\"aak-item-icon\"><i class=\"bi bi-key\"></i></div>\n <div class=\"aak-item-info\">\n <div class=\"aak-item-name\">${name} ${statusBadge}</div>\n <div class=\"aak-item-meta\">\n <span><i class=\"bi bi-code-square\"></i>${tokenPreview}</span>\n <span><i class=\"bi bi-calendar\"></i>Created ${created}</span>\n <span><i class=\"bi bi-clock\"></i>Expires ${expires}</span>\n <span><i class=\"bi bi-activity\"></i>Last used ${lastUsed}</span>\n <span><i class=\"bi bi-globe\"></i>IPs: ${ips}</span>\n </div>\n </div>\n <div>\n <button type=\"button\" class=\"btn btn-outline-danger btn-sm\" data-action=\"revoke-key\" data-id=\"${key.id}\" title=\"Revoke\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n </div>`;\n }).join('');\n\n container.innerHTML = `<div class=\"aak-list\">${rows}</div>`;\n }\n\n async onActionGenerateKey() {\n const data = await Modal.form({\n title: `Generate API Key for ${this.model.get('display_name') || this.model.get('email')}`,\n icon: 'bi-key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Key Name',\n placeholder: 'e.g., CI/CD Pipeline, Mobile App',\n required: true,\n help: 'A descriptive name to identify this key.'\n },\n {\n name: 'allowed_ips',\n type: 'text',\n label: 'Allowed IPs',\n placeholder: 'e.g., 203.0.113.0/24, 10.0.0.1',\n help: 'Optional. Comma-separated IP addresses or CIDR ranges.'\n },\n {\n name: 'expire_days',\n type: 'select',\n label: 'Expiration',\n value: '90',\n options: [\n { value: '30', label: '30 days' },\n { value: '60', label: '60 days' },\n { value: '90', label: '90 days' },\n { value: '180', label: '180 days' },\n { value: '360', label: '360 days' }\n ]\n }\n ]\n });\n if (!data) return true;\n\n const body = {\n uid: this.model.id,\n name: data.name,\n expire_days: parseInt(data.expire_days || '90', 10)\n };\n\n const ipsStr = (data.allowed_ips || '').trim();\n if (ipsStr) {\n body.allowed_ips = ipsStr.split(',').map(ip => ip.trim()).filter(Boolean);\n }\n\n const resp = await rest.POST('/api/auth/manage/generate_api_key', body, {}, { dataOnly: true });\n\n if (resp.success && resp.data?.token) {\n this.generatedToken = resp.data.token;\n const resultEl = this.element.querySelector('#aak-new-token');\n const tokenEl = this.element.querySelector('#aak-token-display');\n if (resultEl && tokenEl) {\n tokenEl.textContent = this.generatedToken;\n resultEl.style.display = 'block';\n }\n this.getApp()?.toast?.success('API key generated');\n await this._loadKeys();\n this._renderKeysList();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to generate API key');\n }\n return true;\n }\n\n async onActionRevokeKey(event, el) {\n const id = el.dataset.id;\n if (!id) return true;\n\n const confirmed = await Modal.confirm(\n 'Revoke this API key? Any applications using it will lose access immediately.',\n 'Revoke API Key'\n );\n if (!confirmed) return true;\n\n const resp = await rest.DELETE(`/api/account/api_keys/${id}`, {}, {}, { dataOnly: true });\n if (resp.success) {\n this.getApp()?.toast?.success('API key revoked');\n const resultEl = this.element?.querySelector('#aak-new-token');\n if (resultEl) resultEl.style.display = 'none';\n await this._loadKeys();\n this._renderKeysList();\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to revoke API key');\n }\n return true;\n }\n\n async onActionCopyToken() {\n if (this.generatedToken) {\n try {\n await navigator.clipboard.writeText(this.generatedToken);\n this.getApp()?.toast?.success('Token copied to clipboard');\n } catch {\n this.getApp()?.toast?.error('Failed to copy token');\n }\n }\n return true;\n }\n}\n","/**\n * AdminMetadataSection - View/edit metadata on any model with a metadata field\n *\n * Displays metadata as a key-value list with add/edit/remove capability.\n * Saves changes via standard model CRUD (model.save()).\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nexport default class AdminMetadataSection extends View {\n constructor(options = {}) {\n super({\n className: 'admin-metadata-section',\n template: `\n <style>\n .amd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n .amd-header h6 { margin: 0; font-weight: 600; }\n .amd-list { border: 1px solid #e9ecef; border-radius: 8px; overflow: hidden; }\n .amd-item { display: flex; align-items: center; padding: 0.6rem 1rem; border-bottom: 1px solid #f0f0f0; gap: 0.75rem; }\n .amd-item:last-child { border-bottom: none; }\n .amd-key { font-family: ui-monospace, monospace; font-size: 0.82rem; font-weight: 600; color: #495057; min-width: 120px; flex-shrink: 0; }\n .amd-value { flex: 1; font-size: 0.85rem; color: #212529; word-break: break-word; min-width: 0; }\n .amd-actions { flex-shrink: 0; display: flex; gap: 0.25rem; }\n .amd-actions .btn { font-size: 0.7rem; padding: 0.15rem 0.35rem; }\n .amd-empty { padding: 2rem; text-align: center; color: #6c757d; font-size: 0.85rem; }\n .amd-empty i { font-size: 1.5rem; display: block; margin-bottom: 0.5rem; }\n </style>\n\n <div class=\"amd-header\">\n <h6>Metadata</h6>\n <button type=\"button\" class=\"btn btn-primary btn-sm\" data-action=\"add-entry\">\n <i class=\"bi bi-plus-lg me-1\"></i>Add\n </button>\n </div>\n\n <div id=\"amd-entries\"></div>\n `,\n ...options\n });\n }\n\n onAfterRender() {\n this._renderEntries();\n }\n\n _getMetadata() {\n return this.model?.get('metadata') || {};\n }\n\n _renderEntries() {\n const container = this.element?.querySelector('#amd-entries');\n if (!container) return;\n\n const metadata = this._getMetadata();\n const keys = Object.keys(metadata).sort();\n\n if (!keys.length) {\n container.innerHTML = `\n <div class=\"amd-list\">\n <div class=\"amd-empty\">\n <i class=\"bi bi-braces\"></i>\n No metadata entries\n </div>\n </div>`;\n return;\n }\n\n const rows = keys.map(key => {\n const val = metadata[key];\n const display = typeof val === 'object' ? JSON.stringify(val) : String(val);\n return `\n <div class=\"amd-item\">\n <div class=\"amd-key\">${this._escapeHtml(key)}</div>\n <div class=\"amd-value\">${this._escapeHtml(display)}</div>\n <div class=\"amd-actions\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"edit-entry\" data-key=\"${this._escapeHtml(key)}\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n <button type=\"button\" class=\"btn btn-outline-danger\" data-action=\"remove-entry\" data-key=\"${this._escapeHtml(key)}\" title=\"Remove\"><i class=\"bi bi-trash\"></i></button>\n </div>\n </div>`;\n }).join('');\n\n container.innerHTML = `<div class=\"amd-list\">${rows}</div>`;\n }\n\n _escapeHtml(str) {\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n }\n\n async onActionAddEntry() {\n const data = await Modal.form({\n title: 'Add Metadata Entry',\n icon: 'bi-braces',\n size: 'sm',\n fields: [\n { name: 'key', type: 'text', label: 'Key', required: true, placeholder: 'e.g., timezone' },\n { name: 'value', type: 'text', label: 'Value', required: true, placeholder: 'e.g., America/New_York' }\n ]\n });\n if (!data) return true;\n\n const metadata = { ...this._getMetadata() };\n // Try to parse JSON values\n try {\n metadata[data.key] = JSON.parse(data.value);\n } catch {\n metadata[data.key] = data.value;\n }\n\n const resp = await this.model.save({ metadata });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata entry added');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to save metadata');\n }\n return true;\n }\n\n async onActionEditEntry(event, el) {\n const key = el.dataset.key;\n if (!key) return true;\n\n const metadata = this._getMetadata();\n const currentValue = metadata[key];\n const displayValue = typeof currentValue === 'object' ? JSON.stringify(currentValue) : String(currentValue);\n\n const data = await Modal.form({\n title: `Edit \"${key}\"`,\n icon: 'bi-braces',\n size: 'sm',\n fields: [\n { name: 'value', type: 'text', label: 'Value', required: true, value: displayValue }\n ]\n });\n if (!data) return true;\n\n const updated = { ...metadata };\n try {\n updated[key] = JSON.parse(data.value);\n } catch {\n updated[key] = data.value;\n }\n\n const resp = await this.model.save({ metadata: updated });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata updated');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to save metadata');\n }\n return true;\n }\n\n async onActionRemoveEntry(event, el) {\n const key = el.dataset.key;\n if (!key) return true;\n\n const confirmed = await Modal.confirm(\n `Remove metadata key \"<strong>${this._escapeHtml(key)}</strong>\"?`,\n 'Remove Entry'\n );\n if (!confirmed) return true;\n\n const updated = { ...this._getMetadata() };\n delete updated[key];\n\n const resp = await this.model.save({ metadata: updated });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Metadata entry removed');\n this._renderEntries();\n } else {\n this.getApp()?.toast?.error('Failed to remove metadata entry');\n }\n return true;\n }\n}\n","/**\n * UserView - Comprehensive user management interface\n *\n * Features:\n * - Header with avatar, name, contact info, status, and context menu\n * - Left-nav sidebar for section switching (Profile, Permissions, Groups, etc.)\n * - Integrated with DataView, TableView, FormView, and SideNavView\n * - Clean Bootstrap 5 styling\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport TableRow from '@core/views/table/TableRow.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport rest from '@core/Rest.js';\nimport { User, UserDataView, UserForms, UserDeviceList, UserDeviceLocationList } from '@core/models/User.js';\nimport { LoginEventList } from '@ext/admin/models/LoginEvent.js';\nimport { LogList } from '@core/models/Log.js';\nimport { IncidentEventList } from '@ext/admin/models/Incident.js';\nimport { MemberList } from '@core/models/Member.js';\nimport { PushDeviceList } from '@ext/admin/models/Push.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport FormView from '@core/forms/FormView.js';\nimport LoginLocationMapView from '../devices/LoginLocationMapView.js';\nimport AdminProfileSection from './sections/AdminProfileSection.js';\nimport AdminPersonalSection from './sections/AdminPersonalSection.js';\nimport AdminSecuritySection from './sections/AdminSecuritySection.js';\nimport AdminConnectedSection from './sections/AdminConnectedSection.js';\nimport AdminNotificationsSection from './sections/AdminNotificationsSection.js';\nimport AdminApiKeysSection from './sections/AdminApiKeysSection.js';\nimport AdminMetadataSection from '../../shared/AdminMetadataSection.js';\n// DeviceView and GeoIPView are opened automatically via VIEW_CLASS on row click\n\n\n// ── Custom TableRow classes for rich multiline rows ──\n\nclass DeviceRow extends TableRow {\n get deviceIcon() {\n const dev = this.model?.get('device_info')?.device || {};\n const os = this.model?.get('device_info')?.os || {};\n const isMobile = ['iPhone', 'Android'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n return isMobile ? 'bi-phone' : 'bi-laptop';\n }\n get deviceName() {\n const dev = this.model?.get('device_info')?.device || {};\n return `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown Device';\n }\n get deviceModel() {\n return this.model?.get('device_info')?.device?.model || '';\n }\n get browserName() {\n const ua = this.model?.get('device_info')?.user_agent || {};\n return ua.family ? `${ua.family} ${ua.major || ''}`.trim() : '';\n }\n get osName() {\n const os = this.model?.get('device_info')?.os || {};\n return os.family ? `${os.family} ${os.major || ''}`.trim() : '';\n }\n get deviceMeta() {\n return [this.browserName, this.osName].filter(Boolean).join(' · ') || '—';\n }\n}\n\nclass LocationRow extends TableRow {\n get deviceIcon() {\n const dev = this.model?.get('user_device')?.device_info?.device || {};\n const os = this.model?.get('user_device')?.device_info?.os || {};\n const isMobile = ['iPhone', 'Android'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n return isMobile ? 'bi-phone' : 'bi-laptop';\n }\n get browserName() {\n const ua = this.model?.get('user_device')?.device_info?.user_agent || {};\n return ua.family ? `${ua.family} ${ua.major || ''}`.trim() : 'Unknown';\n }\n get deviceName() {\n const dev = this.model?.get('user_device')?.device_info?.device || {};\n return `${dev.brand || ''} ${dev.family || ''}`.trim() || 'Unknown';\n }\n get locationText() {\n const geo = this.model?.get('geolocation') || {};\n return [geo.city, geo.region].filter(Boolean).join(', ') || geo.country_name || '—';\n }\n get countryName() {\n return this.model?.get('geolocation')?.country_name || '';\n }\n get threatFlags() {\n const geo = this.model?.get('geolocation') || {};\n const flags = [];\n if (geo.is_vpn) flags.push('<span class=\"badge bg-warning text-dark\" style=\"font-size:0.6rem;\">VPN</span>');\n if (geo.is_tor) flags.push('<span class=\"badge bg-danger\" style=\"font-size:0.6rem;\">Tor</span>');\n if (geo.is_proxy) flags.push('<span class=\"badge bg-warning text-dark\" style=\"font-size:0.6rem;\">Proxy</span>');\n return flags.join(' ');\n }\n get hasThreatFlags() {\n const geo = this.model?.get('geolocation') || {};\n return !!(geo.is_vpn || geo.is_tor || geo.is_proxy);\n }\n}\n\nclass UserView extends View {\n constructor(options = {}) {\n super({\n className: 'user-view',\n ...options\n });\n\n // User model instance\n this.model = options.model || new User(options.data || {});\n\n // Section views\n this.sideNavView = null;\n\n // Set template\n this.template = `\n <div class=\"user-view-container\">\n <!-- User Header + Context Menu -->\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <div data-container=\"user-header\" style=\"flex: 1;\"></div>\n <div data-container=\"user-context-menu\" class=\"ms-3 flex-shrink-0\"></div>\n </div>\n <!-- Side Nav Container -->\n <div data-container=\"user-sidenav\" style=\"min-height: 400px;\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // ── Header ──────────────────────────────────\n this.header = new View({\n containerId: 'user-header',\n template: `\n <div class=\"d-flex justify-content-between align-items-start\">\n <!-- Left Side: Primary Identity -->\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('icon-only')}}}\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 <!-- Right Side: Status -->\n <div class=\"text-end\">\n <div class=\"d-flex align-items-center justify-content-end gap-3\">\n <span class=\"d-inline-flex align-items-center gap-1\" title=\"{{model.is_online|boolean('Online','Offline')}}\">\n <i class=\"bi bi-circle-fill {{model.is_online|boolean('text-success','text-secondary')}}\" style=\"font-size: 0.5rem;\"></i>\n <span class=\"small\">{{model.is_online|boolean('Online','Offline')}}</span>\n </span>\n <span class=\"d-inline-flex align-items-center gap-1\" style=\"cursor: pointer;\"\n data-action=\"toggle-active\"\n title=\"{{model.is_active|boolean('Click to deactivate','Click to activate')}}\">\n <i class=\"bi {{model.is_active|boolean('bi-toggle-on text-success','bi-toggle-off text-secondary')}}\" style=\"font-size: 1.1rem;\"></i>\n <span class=\"small\">{{model.is_active|boolean('Active','Inactive')}}</span>\n </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>`\n });\n\n this.header.setModel(this.model);\n this.addChild(this.header);\n\n // ── Section views ───────────────────────────\n\n const profileView = new AdminProfileSection({ model: this.model });\n const personalView = new AdminPersonalSection({ model: this.model });\n const securityView = new AdminSecuritySection({ model: this.model });\n const connectedView = new AdminConnectedSection({ model: this.model });\n const notificationsView = new AdminNotificationsSection({ model: this.model });\n const apiKeysView = new AdminApiKeysSection({ model: this.model });\n const metadataView = new AdminMetadataSection({ model: this.model });\n\n const permsView = new FormView({\n fields: User.CATEGORY_PERMISSION_FIELDS,\n model: this.model,\n autosaveModelField: true\n });\n\n const advPermsView = new FormView({\n fields: User.GRANULAR_PERMISSION_FIELDS,\n model: this.model,\n autosaveModelField: true\n });\n\n // Groups\n const membersCollection = new MemberList({\n params: { user: this.model.get('id'), size: 5 }\n });\n const groupsView = new TableView({\n collection: membersCollection,\n hideActivePillNames: ['user'],\n columns: [\n { key: 'created', label: 'Date Joined', formatter: 'date', sortable: true },\n { key: 'group.name', label: 'Group Name', sortable: true },\n { key: 'permissions|keys|badge', label: 'Permissions' }\n ]\n });\n\n // Events\n const eventsCollection = new IncidentEventList({\n params: { size: 5, model_name: \"account.User\", model_id: this.model.get('id') }\n });\n const eventsTable = new TableView({\n containerId: 'events-table',\n collection: eventsCollection,\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n { key: 'id', label: 'ID', sortable: true, width: '40px' },\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '150px' },\n { key: 'category|badge', label: 'Category' },\n { key: 'title', label: 'Event' }\n ]\n });\n const eventsView = new View({\n template: `\n <div class=\"mb-2\">\n <h6 class=\"fw-semibold mb-1\">Security & Account Events</h6>\n <p class=\"text-muted small mb-3\">Incidents and account actions associated with this user.</p>\n </div>\n <div data-container=\"events-table\"></div>`\n });\n eventsView.addChild(eventsTable);\n\n // Devices — multiline rows with detail dialog\n const devicesView = new TableView({\n collection: new UserDeviceList({ params: { size: 10, user: this.model.get('id') } }),\n hideActivePillNames: ['user'],\n clickAction: 'view',\n itemClass: DeviceRow,\n columns: [\n {\n key: 'device_info',\n label: 'Device',\n template: `\n <div style=\"font-size:0.85rem; font-weight:500;\">\n <i class=\"bi {{deviceIcon}} text-muted me-1\" style=\"font-size:1.1rem; vertical-align:middle;\"></i>{{deviceName}}\n {{#deviceModel}} <span class=\"text-muted fw-normal\">({{deviceModel}})</span>{{/deviceModel}}\n </div>\n <div style=\"font-size:0.73rem; color:#6c757d; margin-top:0.15rem;\">\n {{deviceMeta}}\n {{#model.last_ip}} <span class=\"text-muted mx-1\">·</span> {{model.last_ip}}{{/model.last_ip}}\n </div>`\n },\n { key: 'first_seen', label: 'First Seen', formatter: 'epoch|relative', width: '120px' },\n { key: 'last_seen', label: 'Last Seen', formatter: 'epoch|relative', width: '120px' }\n ]\n });\n\n // Locations — TabView with login map + login events table\n const loginMapView = new LoginLocationMapView({\n userId: this.model.get('id'),\n height: 300,\n mapStyle: 'dark'\n });\n\n const loginEventsTable = new TableView({\n collection: new LoginEventList({ params: { user: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['user'],\n columns: [\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '160px' },\n { key: 'ip_address', label: 'IP Address' },\n { key: 'city', label: 'City', formatter: \"default('—')\" },\n { key: 'region', label: 'Region', formatter: \"default('—')\" },\n { key: 'country_code', label: 'Country', sortable: true },\n { key: 'source', label: 'Source', sortable: true }\n ]\n });\n loginEventsTable.onTabActivated = async () => {\n await loginEventsTable.collection?.fetch();\n };\n\n const locationsView = new TabView({\n tabs: {\n 'Map': loginMapView,\n 'Logins': loginEventsTable\n },\n activeTab: 'Map'\n });\n\n // Push Devices\n const pushDevices = new PushDeviceList({\n params: { size: 5, user: this.model.get('id') }\n });\n const pushDevicesView = new TableView({\n collection: pushDevices,\n hideActivePillNames: ['user'],\n columns: [\n { key: 'duid|truncate_middle(16)', label: 'Device ID', sortable: true },\n { key: 'device_info.user_agent.family', label: 'Browser', formatter: \"default('—')\" },\n { key: 'device_info.os.family', label: 'OS', formatter: \"default('—')\" },\n { key: 'first_seen', label: 'First Seen', formatter: \"epoch|datetime\" },\n { key: 'last_seen', label: 'Last Seen', formatter: \"epoch|datetime\" }\n ],\n size: 5\n });\n\n // Logs (model-scoped)\n const logsCollection = new LogList({\n params: { size: 5, model_name: \"account.User\", model_id: this.model.get('id') }\n });\n const logsTable = new TableView({\n containerId: 'logs-table',\n collection: logsCollection,\n permissions: 'view_logs',\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n {\n key: 'created', label: 'Timestamp', sortable: true, formatter: \"epoch|datetime\",\n 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 ' }\n },\n {\n key: 'level', label: 'Level', sortable: true,\n filter: { type: 'select', options: [{ value: 'info', label: 'Info' }, { value: 'warning', label: 'Warning' }, { value: 'error', label: 'Error' }] }\n },\n { key: 'kind', label: 'Event Type', filter: { type: 'text' } },\n { name: 'log', label: 'Details' }\n ]\n });\n const logsView = new View({\n template: `\n <div class=\"mb-2\">\n <h6 class=\"fw-semibold mb-1\">Object Logs</h6>\n <p class=\"text-muted small mb-3\">System log entries about changes to this user's record.</p>\n </div>\n <div data-container=\"logs-table\"></div>`\n });\n logsView.addChild(logsTable);\n\n // Activity (user-scoped logs)\n const activityCollection = new LogList({\n params: { size: 5, uid: this.model.get('id') }\n });\n const activityTable = new TableView({\n containerId: 'activity-table',\n collection: activityCollection,\n hideActivePillNames: ['uid'],\n permissions: 'view_logs',\n columns: [\n {\n key: 'created', label: 'Timestamp', sortable: true, formatter: \"epoch|datetime\",\n 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 ' }\n },\n {\n key: 'level', label: 'Level', sortable: true,\n filter: { type: 'select', options: [{ value: 'info', label: 'Info' }, { value: 'warning', label: 'Warning' }, { value: 'error', label: 'Error' }] }\n },\n { key: 'kind', label: 'Event Type', filter: { type: 'text' } },\n { name: 'path', label: 'Request Path' }\n ]\n });\n const activityView = new View({\n template: `\n <div class=\"mb-2\">\n <h6 class=\"fw-semibold mb-1\">Activity Log</h6>\n <p class=\"text-muted small mb-3\">API and request activity performed by this user.</p>\n </div>\n <div data-container=\"activity-table\"></div>`\n });\n activityView.addChild(activityTable);\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'user-sidenav',\n activeSection: 'profile',\n navWidth: 200,\n contentPadding: '1.25rem 2rem',\n enableResponsive: true,\n minWidth: 500,\n sections: [\n { key: 'profile', label: 'Profile', icon: 'bi-person', view: profileView },\n { key: 'personal', label: 'Personal', icon: 'bi-person-vcard', view: personalView },\n { key: 'security', label: 'Security', icon: 'bi-shield-lock', view: securityView },\n { key: 'connected', label: 'OAuth Accounts', icon: 'bi-plug', view: connectedView },\n { type: 'divider', label: 'Access' },\n { key: 'permissions', label: 'Permissions', icon: 'bi-shield-check', view: permsView },\n { key: 'adv_permissions', label: 'Adv Permissions', icon: 'bi-shield-plus', view: advPermsView },\n { key: 'groups', label: 'Groups', icon: 'bi-people', view: groupsView },\n { key: 'api_keys', label: 'API Keys', icon: 'bi-key', view: apiKeysView },\n { type: 'divider', label: 'Activity' },\n { key: 'events', label: 'Events', icon: 'bi-calendar-event', view: eventsView },\n { key: 'activity', label: 'Activity Log', icon: 'bi-clock-history', view: activityView, permissions: 'view_logs' },\n { key: 'logs', label: 'Object Logs', icon: 'bi-journal-text', view: logsView, permissions: 'view_logs' },\n { type: 'divider', label: 'Devices' },\n { key: 'devices', label: 'Devices', icon: 'bi-laptop', view: devicesView },\n { key: 'locations', label: 'Locations', icon: 'bi-geo-alt', view: locationsView },\n { key: 'push_devices', label: 'Push Devices', icon: 'bi-phone', view: pushDevicesView },\n { type: 'divider', label: 'Settings' },\n { key: 'notifications', label: 'Notifications', icon: 'bi-bell', view: notificationsView },\n { key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: metadataView }\n ]\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n const userMenu = new ContextMenu({\n containerId: 'user-context-menu',\n className: \"context-menu-view header-menu-absolute\",\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit User', action: 'edit-user', icon: 'bi-pencil' },\n ...(this.model.get('avatar')\n ? [{ label: 'Clear Avatar', action: 'clear-avatar', icon: 'bi-person-x' }]\n : []),\n { type: 'divider' },\n { label: 'Send Password Reset', action: 'send-password-reset', icon: 'bi-envelope' },\n { label: 'Send Magic Login Link', action: 'send-magic-link', icon: 'bi-link-45deg' },\n { label: 'Revoke All Sessions', action: 'revoke-all-sessions', icon: 'bi-box-arrow-right' },\n { type: 'divider' },\n ...(this.model.get('is_email_verified')\n ? []\n : [\n { label: 'Send Email Verification', action: 'send-email-verification', icon: 'bi-envelope-check' },\n { label: 'Force Verify Email', action: 'force-verify-email', icon: 'bi-patch-check' },\n ]),\n ...(this.model.get('phone_number') && !this.model.get('is_phone_verified')\n ? [{ label: 'Force Verify Phone', action: 'force-verify-phone', icon: 'bi-patch-check' }]\n : []),\n { type: 'divider' },\n this.model.get('is_active')\n ? { label: 'Deactivate User', action: 'deactivate-user', icon: 'bi-person-dash' }\n : { label: 'Activate User', action: 'activate-user', icon: 'bi-person-check' },\n ]\n }\n });\n this.addChild(userMenu);\n }\n\n // ── Context menu actions ────────────────────────\n\n async onActionEditUser() {\n await Modal.modelForm({\n title: `EDIT - #${this.model.id} ${this.options.modelName}`,\n model: this.model,\n formConfig: UserForms.edit,\n });\n }\n\n async onActionClearAvatar() {\n const confirmed = await Modal.confirm(\n 'Remove this user\\'s avatar? They will see the default placeholder.',\n 'Clear Avatar'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ avatar: null });\n if (resp.status === 200) {\n this.getApp().toast.success('Avatar cleared');\n } else {\n this.getApp().toast.error('Failed to clear avatar');\n }\n return true;\n }\n\n async onActionSendPasswordReset() {\n const email = this.model.get('email');\n const confirmed = await Modal.confirm(\n `Send a password reset email to <strong>${email}</strong>?`,\n 'Send Password Reset'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/auth/password/reset', { email });\n if (resp.success) {\n this.getApp().toast.success('Password reset email sent');\n } else {\n this.getApp().toast.error(resp.message || 'Failed to send password reset');\n }\n return true;\n }\n\n async onActionSendMagicLink() {\n const email = this.model.get('email');\n const confirmed = await Modal.confirm(\n `Send a magic login link to <strong>${email}</strong>?`,\n 'Send Magic Login Link'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/auth/magic-link', { email });\n if (resp.success) {\n this.getApp().toast.success('Magic login link sent');\n } else {\n this.getApp().toast.error(resp.message || 'Failed to send magic link');\n }\n return true;\n }\n\n async onActionRevokeAllSessions() {\n const confirmed = await Modal.confirm(\n 'Revoke all sessions? The user will be signed out of all devices immediately.',\n 'Revoke All Sessions'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST(`/api/user/${this.model.id}/sessions/revoke`);\n if (resp.success) {\n this.getApp().toast.success('All sessions revoked');\n } else {\n this.getApp().toast.error(resp.message || 'Failed to revoke sessions');\n }\n return true;\n }\n\n async onActionSendEmailVerification() {\n const email = this.model.get('email');\n const confirmed = await Modal.confirm(\n `Send a verification email to <strong>${email}</strong>?`,\n 'Send Email Verification'\n );\n if (!confirmed) return true;\n\n const resp = await rest.POST('/api/auth/email/verify', { email });\n if (resp.success) {\n this.getApp().toast.success('Verification email sent');\n } else {\n this.getApp().toast.error(resp.message || 'Failed to send verification email');\n }\n return true;\n }\n\n async onActionForceVerifyEmail() {\n const confirmed = await Modal.confirm(\n `Mark <strong>${this.model.get('email')}</strong> as verified?`,\n 'Force Verify Email'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_email_verified: true });\n if (resp.status === 200) {\n this.getApp().toast.success('Email marked as verified');\n } else {\n this.getApp().toast.error('Failed to verify email');\n }\n return true;\n }\n\n async onActionForceVerifyPhone() {\n const confirmed = await Modal.confirm(\n `Mark <strong>${this.model.get('phone_number')}</strong> as verified?`,\n 'Force Verify Phone'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_phone_verified: true });\n if (resp.status === 200) {\n this.getApp().toast.success('Phone marked as verified');\n } else {\n this.getApp().toast.error('Failed to verify phone');\n }\n return true;\n }\n\n async onActionToggleActive() {\n if (this.model.get('is_active')) {\n return this.onActionDeactivateUser();\n } else {\n return this.onActionActivateUser();\n }\n }\n\n async onActionDeactivateUser() {\n const confirmed = await Modal.confirm(\"Are you sure you want to deactivate this user?\");\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: false });\n if (resp.status === 200) {\n this.getApp().toast.success(\"User deactivated\");\n } else {\n this.getApp().toast.error(\"Failed to deactivate user\");\n }\n return true;\n }\n\n async onActionActivateUser() {\n const confirmed = await Modal.confirm(\"Are you sure you want to activate this user?\");\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: true });\n if (resp.status === 200) {\n this.getApp().toast.success(\"User activated\");\n } else {\n this.getApp().toast.error(\"Failed to activate user\");\n }\n return true;\n }\n\n async showSection(sectionName) {\n if (this.sideNavView) {\n await this.sideNavView.showSection(sectionName);\n }\n }\n\n getActiveSection() {\n return this.sideNavView ? this.sideNavView.getActiveSection() : null;\n }\n\n // Legacy API compatibility\n async showTab(tabName) {\n return this.showSection(tabName);\n }\n\n getActiveTab() {\n return this.getActiveSection();\n }\n\n _onModelChange() {\n // do nothing, we do not want model changes to render this entire view\n }\n\n // Static factory method\n static create(options = {}) {\n return new UserView(options);\n }\n}\n\nUser.VIEW_CLASS = UserView;\n\nexport default UserView;\n","/**\n * UsersPage - Simple Todo List using TablePage component\n * Demonstrates clean usage of TablePage framework features\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport {UserList, UserForms} from '@core/models/User.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport MOJOUtils from '@core/utils/MOJOUtils.js';\nimport UserView from './UserView.js';\n\n\nclass UserTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_users',\n pageName: 'Manage Users',\n router: \"admin/users\",\n Collection: UserList,\n\n viewDialogOptions: { header: false },\n\n defaultQuery: {\n sort: '-last_activity',\n is_active: true\n },\n\n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n sortable: true,\n class: 'text-muted'\n },\n // {\n // label: 'Avatar',\n // key: 'avatar|avatar(\"sm\")',\n // sortable: false,\n // visibility: 'md'\n // },\n {\n key: 'display_name|tooltip:model.username',\n label: 'Display Name',\n },\n {\n label: 'Info',\n key: 'permissions.manage_users',\n 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 `,\n sortable: false,\n },\n {\n key: 'email',\n label: 'Email',\n visibility: 'xl',\n className: 'text-muted fs-8',\n },\n // {\n // key: 'username',\n // label: 'Username',\n // visibility: 'xl',\n // className: 'text-muted fs-8',\n // },\n {\n key: 'last_activity',\n label: 'Last Activity',\n formatter: \"relative\",\n className: 'text-muted fs-8',\n }\n ],\n\n filters: [\n {\n key: 'is_active',\n label: 'Active',\n type: 'boolean',\n defaultValue: true,\n },\n {\n key: 'email',\n label: 'Email',\n type: 'text',\n defaultValue: '',\n },\n {\n key: 'username',\n label: 'Username',\n type: 'text',\n defaultValue: '',\n },\n {\n key: 'locations__ip_address',\n label: 'IP Address',\n type: 'text',\n defaultValue: '',\n },\n {\n key: 'last_activity',\n type: 'daterange',\n startName: 'dr_start',\n endName: 'dr_end',\n fieldName: 'dr_field',\n label: 'Date Range',\n format: 'YYYY-MM-DD',\n displayFormat: 'MMM DD, YYYY',\n separator: ' to '\n }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // TablePage toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No users found. Click \"Add\" to create a new user.',\n\n // Context menu configuration\n contextMenu: [\n {\n icon: 'bi-pencil',\n action: 'edit',\n label: \"Edit Profile\"\n },\n {\n icon: 'bi-shield-check',\n action: 'edit-permissions',\n label: \"Edit Permissions\"\n },\n {\n icon: 'bi-shield',\n action: 'change-password',\n label: \"Change Password\",\n },\n { separator: true },\n {\n icon: 'bi-envelope',\n action: 'send-invite',\n label: \"Send Invite\"\n }\n ],\n\n // Table display options (for HTML table styling)\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionEditPermissions(event, element) {\n\n event.preventDefault();\n const item = this.collection.get(element.dataset.id);\n const result = await Modal.modelForm({\n model: item,\n size: 'lg',\n title: `Edit Permissions for \"${item._.username}\"`,\n fields: UserForms.permissions.fields\n });\n }\n\n async onActionChangePassword(event, element) {\n // Implement password change logic here\n const item = this.collection.get(element.dataset.id);\n const data = await Modal.form({\n title: `Change Password for \"${item._.username}\"`,\n fields: [\n {\n type: 'text', // Change from 'hidden' to 'text'\n name: 'username',\n value: item.get('email') || item.get('username'),\n attributes: {\n autocomplete: 'username',\n readonly: 'readonly',\n tabindex: '-1',\n style: 'position: absolute; left: -9999px; opacity: 0; height: 0; width: 0;'\n }\n },\n {\n name: 'new_password',\n label: 'New Password',\n type: 'password',\n passwordUsage: 'new',\n required: true,\n showToggle: true,\n attributes: {\n autocomplete: 'new-password' // Make sure this isn't being overridden\n }\n }\n ]\n });\n\n if (data && data.new_password) {\n // Basic password validation\n const result = MOJOUtils.checkPasswordStrength(data.new_password);\n if (result.score < 5) {\n 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');\n await this.onActionChangePassword(event, element);\n return;\n }\n const resp = await item.save({new_password: data.new_password});\n if (!this.onPasswordChange(resp)) {\n await this.onActionChangePassword(event, element);\n }\n }\n }\n\n onPasswordChange(resp) {\n if (resp.success) {\n this.getApp().toast.success('Password changed successfully');\n return true;\n } else {\n if (resp.data && resp.data.error) {\n this.getApp().toast.error(resp.data.error);\n } else {\n this.getApp().toast.error('Failed to change password');\n }\n }\n return false;\n }\n\n async onActionSendInvite(event, element) {\n const item = this.collection.get(element.dataset.id);\n const resp = await item.save({send_invite: true});\n if (resp.success) {\n this.getApp().toast.success('Invite sent successfully');\n return true;\n } else {\n if (resp.data && resp.data.error) {\n this.getApp().toast.error(resp.data.error);\n } else {\n this.getApp().toast.error('Failed to send invite');\n }\n }\n return false;\n }\n\n}\n\nexport default UserTablePage;\n","/**\n * MemberView - Modern membership detail view\n *\n * Features:\n * - Header with avatar, user name, group name, role badge, active toggle\n * - SideNavView: Details (with clickable user/group), Permissions, Logs\n * - Context menu: Edit, View User, View Group, Remove\n * - Click user → opens UserView, click group → opens GroupView\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport FormView from '@core/forms/FormView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Member, MemberForms } from '@core/models/Member.js';\nimport { LogList } from '@core/models/Log.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass MemberView extends View {\n constructor(options = {}) {\n super({\n className: 'member-view',\n ...options\n });\n\n this.model = options.model || new Member(options.data || {});\n\n this.template = `\n <div class=\"member-view-container\">\n <!-- Header + Context Menu -->\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <div data-container=\"member-header\" style=\"flex: 1;\"></div>\n <div data-container=\"member-context-menu\" class=\"ms-3 flex-shrink-0\"></div>\n </div>\n <!-- Side Nav -->\n <div data-container=\"member-sidenav\" style=\"min-height: 300px;\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // ── Header ──────────────────────────────────\n this.header = new View({\n containerId: 'member-header',\n template: `\n <div class=\"d-flex justify-content-between align-items-start\">\n <!-- Left: Avatar + Identity -->\n <div class=\"d-flex align-items-center gap-3\">\n {{{model.user.avatar|avatar('md','rounded-circle')}}}\n <div>\n <h4 class=\"mb-0\">\n <a href=\"#\" data-action=\"view-user\" class=\"text-decoration-none text-body\">{{model.user.display_name}}</a>\n </h4>\n <div class=\"text-muted small mt-1\">\n <i class=\"bi bi-people me-1\"></i>\n <a href=\"#\" data-action=\"view-group\" class=\"text-decoration-none\">{{model.group.name}}</a>\n {{#model.group.kind}}\n <span class=\"badge bg-light text-muted border ms-1\" style=\"font-size: 0.65rem;\">{{model.group.kind|capitalize}}</span>\n {{/model.group.kind}}\n </div>\n </div>\n </div>\n\n <!-- Right: Status -->\n <div class=\"text-end\">\n <div class=\"d-flex align-items-center gap-2\">\n {{#model.metadata.role}}\n <span class=\"badge bg-primary bg-opacity-10 text-primary\" style=\"font-size: 0.72rem;\">{{model.metadata.role}}</span>\n {{/model.metadata.role}}\n <span class=\"d-inline-flex align-items-center gap-1\" style=\"cursor: pointer;\"\n data-action=\"toggle-active\"\n title=\"{{model.is_active|boolean('Click to deactivate','Click to activate')}}\">\n <i class=\"bi {{model.is_active|boolean('bi-toggle-on text-success','bi-toggle-off text-secondary')}}\" style=\"font-size: 1.1rem;\"></i>\n <span class=\"small\">{{model.is_active|boolean('Active','Inactive')}}</span>\n </span>\n </div>\n {{#model.created}}\n <div class=\"text-muted small mt-1\">Joined {{model.created|date}}</div>\n {{/model.created}}\n </div>\n </div>`\n });\n this.header.setModel(this.model);\n this.addChild(this.header);\n\n // ── Details section ─────────────────────────\n const detailsView = new View({\n model: this.model,\n template: `\n <style>\n .mv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .mv-section-label:first-child { margin-top: 0; }\n .mv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .mv-field-row:last-child { border-bottom: none; }\n .mv-field-label { width: 130px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .mv-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .mv-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .mv-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n </style>\n\n <div class=\"mv-section-label\">User</div>\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Name</div>\n <div class=\"mv-field-value\">\n <a href=\"#\" data-action=\"view-user\" class=\"text-decoration-none\">{{model.user.display_name}}</a>\n </div>\n </div>\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Email</div>\n <div class=\"mv-field-value\">{{model.user.email}}</div>\n </div>\n\n <div class=\"mv-section-label\">Group</div>\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Name</div>\n <div class=\"mv-field-value\">\n <a href=\"#\" data-action=\"view-group\" class=\"text-decoration-none\">{{model.group.name}}</a>\n </div>\n </div>\n {{#model.group.kind}}\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Kind</div>\n <div class=\"mv-field-value\"><span class=\"badge bg-primary bg-opacity-10 text-primary\">{{model.group.kind|capitalize}}</span></div>\n </div>\n {{/model.group.kind}}\n\n <div class=\"mv-section-label\">Membership</div>\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Role</div>\n <div class=\"mv-field-value\">{{model.metadata.role|default('—')}}</div>\n <button type=\"button\" class=\"mv-field-action\" data-action=\"edit-membership\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Status</div>\n <div class=\"mv-field-value\">\n {{#model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#d1e7dd; color:#0f5132; border-radius:3px;\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#fff3cd; color:#856404; border-radius:3px;\">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Member ID</div>\n <div class=\"mv-field-value\" style=\"font-family: ui-monospace, monospace; font-size: 0.82rem;\">{{model.id}}</div>\n </div>\n <div class=\"mv-field-row\">\n <div class=\"mv-field-label\">Joined</div>\n <div class=\"mv-field-value\">{{model.created|datetime|default('—')}}</div>\n </div>\n `\n });\n\n // ── Permissions section — editable switches ──\n const permissionsView = new FormView({\n fields: Member.PERMISSION_FIELDS,\n model: this.model,\n autosaveModelField: true\n });\n\n // ── Logs section ────────────────────────────\n const logsView = new TableView({\n collection: new LogList({\n params: { size: 10, model_name: 'account.Member', model_id: this.model.get('id') }\n }),\n permissions: 'view_logs',\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n {\n key: 'created', label: 'Timestamp', sortable: true, formatter: 'epoch|datetime',\n 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 ' }\n },\n { key: 'level', label: 'Level', sortable: true },\n { key: 'kind', label: 'Kind' },\n { name: 'log', label: 'Log' }\n ]\n });\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'member-sidenav',\n activeSection: 'details',\n navWidth: 160,\n contentPadding: '1rem 1.5rem',\n enableResponsive: true,\n minWidth: 450,\n sections: [\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView },\n { key: 'permissions', label: 'Permissions', icon: 'bi-shield-check', view: permissionsView },\n { type: 'divider', label: 'Activity' },\n { key: 'logs', label: 'Logs', icon: 'bi-journal-text', view: logsView, permissions: 'view_logs' }\n ]\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n const memberMenu = new ContextMenu({\n containerId: 'member-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit Membership', action: 'edit-membership', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'View User', action: 'view-user', icon: 'bi-person' },\n { label: 'View Group', action: 'view-group', icon: 'bi-people' },\n { type: 'divider' },\n this.model.get('is_active')\n ? { label: 'Deactivate Member', action: 'deactivate-member', icon: 'bi-toggle-off' }\n : { label: 'Activate Member', action: 'activate-member', icon: 'bi-toggle-on' },\n { label: 'Remove From Group', action: 'remove-member', icon: 'bi-person-dash', danger: true }\n ]\n }\n });\n this.addChild(memberMenu);\n }\n\n // ── Actions ─────────────────────────────────\n\n async onActionEditMembership() {\n await Modal.modelForm({\n title: 'Edit Membership',\n model: this.model,\n formConfig: MemberForms.edit,\n });\n }\n\n async onActionViewUser() {\n const userId = this.model.get('user')?.id;\n if (!userId) return true;\n const { User } = await import('@core/models/User.js');\n await Modal.showModelById(User, userId);\n return true;\n }\n\n async onActionViewGroup() {\n const groupId = this.model.get('group')?.id;\n if (!groupId) return true;\n\n const { Group } = await import('@core/models/Group.js');\n await Modal.showModelById(Group, groupId);\n return true;\n }\n\n async onActionToggleActive() {\n if (this.model.get('is_active')) {\n return this.onActionDeactivateMember();\n } else {\n return this.onActionActivateMember();\n }\n }\n\n async onActionDeactivateMember() {\n const confirmed = await Modal.confirm(\n `Deactivate <strong>${this.model.get('user.display_name')}</strong>'s membership in <strong>${this.model.get('group.name')}</strong>?`,\n 'Deactivate Member'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: false });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Member deactivated');\n } else {\n this.getApp()?.toast?.error('Failed to deactivate member');\n }\n return true;\n }\n\n async onActionActivateMember() {\n const confirmed = await Modal.confirm(\n `Activate <strong>${this.model.get('user.display_name')}</strong>'s membership in <strong>${this.model.get('group.name')}</strong>?`,\n 'Activate Member'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: true });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Member activated');\n } else {\n this.getApp()?.toast?.error('Failed to activate member');\n }\n return true;\n }\n\n async onActionRemoveMember() {\n const confirmed = await Modal.confirm(\n `Remove <strong>${this.model.get('user.display_name')}</strong> from <strong>${this.model.get('group.name')}</strong>? This cannot be undone.`,\n 'Remove Member'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.destroy();\n if (resp.success) {\n this.getApp()?.toast?.success('Member removed');\n this.emit('member:removed', { model: this.model });\n } else {\n this.getApp()?.toast?.error('Failed to remove member');\n }\n return true;\n }\n\n _onModelChange() {\n // Prevent full re-render on model changes\n }\n\n static create(options = {}) {\n return new MemberView(options);\n }\n}\n\nMember.VIEW_CLASS = MemberView;\n\nexport default MemberView;\n","/**\n * MemberTablePage - Member management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { MemberList, MemberForms } from '@core/models/Member.js';\nimport MemberView from './MemberView.js';\n\nclass MemberTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_members',\n pageName: 'Manage Members',\n router: \"admin/members\",\n Collection: MemberList,\n \n formEdit: MemberForms.edit,\n itemViewClass: MemberView,\n \n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n \n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n width: '60px',\n sortable: true,\n class: 'text-muted'\n },\n {\n key: 'user.display_name',\n label: 'User',\n formatter: \"default('Unknown User')\"\n },\n {\n key: 'user.email',\n label: 'Email',\n formatter: \"default('No Email')\"\n },\n {\n key: 'group.name',\n label: 'Group',\n formatter: \"default('Unknown Group')\"\n },\n {\n key: 'role',\n label: 'Role',\n formatter: \"badge\"\n },\n {\n key: 'status',\n label: 'Status',\n formatter: \"badge\"\n },\n {\n key: 'created',\n label: 'Added',\n formatter: \"epoch|datetime\"\n }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No members found. Click \"Add Member\" to add users to groups.',\n\n // Batch actions\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Remove\", icon: \"bi bi-person-dash\", action: \"batch-remove\" },\n { label: \"Export\", icon: \"bi bi-download\", action: \"batch-export\" },\n { label: \"Change Role\", icon: \"bi bi-person-gear\", action: \"batch-role\" },\n { label: \"Activate\", icon: \"bi bi-check-circle\", action: \"batch-activate\" },\n { label: \"Deactivate\", icon: \"bi bi-x-circle\", action: \"batch-deactivate\" }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default MemberTablePage;","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/**\n * ApiKey - Group-scoped API key for external integrations and services.\n * Maps to REST endpoints under /api/group/apikey\n *\n * Key properties:\n * - Scoped to a single group\n * - Carries only explicitly granted permissions (least-privilege)\n * - sys.* permissions always denied\n * - No IP restriction (unlike User Auth Tokens)\n * - Header format: Authorization: apikey <token>\n *\n * The raw token is only returned at creation time — it is never shown again.\n *\n * Endpoints:\n * GET /api/group/apikey - List keys (filter by ?group=<id>)\n * POST /api/group/apikey - Create a key\n * GET /api/group/apikey/<id> - Get key details\n * POST /api/group/apikey/<id> - Update name, permissions, limits, is_active\n * DELETE /api/group/apikey/<id> - Delete key\n */\nclass ApiKey extends Model {\n constructor(data = {}, options = {}) {\n super(data, {\n endpoint: '/api/group/apikey',\n ...options\n });\n }\n}\n\n/**\n * ApiKeyList - Collection of ApiKey records.\n * Filter by group: new ApiKeyList({ params: { group: groupId } })\n */\nclass ApiKeyList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ApiKey,\n endpoint: '/api/group/apikey',\n size: 25,\n ...options\n });\n }\n}\n\n/**\n * Forms configuration for ApiKey\n */\nconst ApiKeyForms = {\n create: {\n title: 'Create API Key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Name',\n placeholder: 'Mobile App v2',\n required: true,\n columns: 12,\n help: 'A descriptive name to identify this key.'\n },\n {\n name: 'group',\n type: 'number',\n label: 'Group ID',\n required: true,\n columns: 12,\n help: 'The group this key is scoped to.'\n },\n {\n name: 'permissions',\n type: 'textarea',\n label: 'Permissions (JSON)',\n placeholder: '{\"view_orders\": true, \"create_orders\": true}',\n columns: 12,\n help: 'JSON dict of permissions to grant. Leave empty for no permissions.'\n }\n ]\n },\n\n edit: {\n title: 'Edit API Key',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Name',\n required: true,\n columns: 12\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Active',\n columns: 12,\n help: 'Deactivate to revoke access without deleting the key.'\n },\n {\n name: 'permissions',\n type: 'textarea',\n label: 'Permissions (JSON)',\n columns: 12,\n help: 'JSON dict of granted permissions.'\n }\n ]\n }\n};\n\nexport { ApiKey, ApiKeyList, ApiKeyForms };\n","/**\n * GroupView - Modern group management interface\n *\n * Features:\n * - Clean header with avatar, name, kind badge, parent link, active/online status\n * - SideNavView with: Details, Members, Children, Events, Logs\n * - Expanded context menu with quick actions\n * - Clean Bootstrap 5 styling matching UserView patterns\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Group, GroupList, GroupForms } from '@core/models/Group.js';\nimport { MemberList } from '@core/models/Member.js';\nimport { IncidentEventList } from '@ext/admin/models/Incident.js';\nimport { LogList } from '@core/models/Log.js';\nimport { ApiKeyList, ApiKeyForms } from '@core/models/ApiKey.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport AdminMetadataSection from '../../shared/AdminMetadataSection.js';\n\nclass GroupView extends View {\n constructor(options = {}) {\n super({\n className: 'group-view',\n ...options\n });\n\n this.model = options.model || new Group(options.data || {});\n\n this.template = `\n <div class=\"group-view-container\">\n <!-- Header -->\n <div data-container=\"group-header\"></div>\n <!-- Side Nav -->\n <div data-container=\"group-sidenav\" style=\"min-height: 400px;\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // ── Header ──────────────────────────────────\n this.header = new View({\n containerId: 'group-header',\n template: `\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <!-- Left Side: Identity -->\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=\"d-flex align-items-center justify-content-center rounded bg-light\" style=\"width: 56px; height: 56px;\">\n <i class=\"bi bi-people text-secondary\" style=\"font-size: 1.5rem;\"></i>\n </div>\n {{/model.avatar}}\n <div>\n <h3 class=\"mb-0\">{{model.name|default('Unnamed Group')}}</h3>\n <div class=\"d-flex align-items-center gap-2 mt-1\">\n <span class=\"badge bg-primary bg-opacity-10 text-primary\" style=\"font-size: 0.72rem;\">{{model.kind|capitalize}}</span>\n {{#model.parent}}\n <span class=\"text-muted small\">\n <i class=\"bi bi-diagram-3 me-1\"></i>\n <a href=\"#\" data-action=\"view-parent\" data-id=\"{{model.parent.id}}\" class=\"text-decoration-none\">{{model.parent.name}}</a>\n </span>\n {{/model.parent}}\n </div>\n {{#model.metadata.timezone}}\n <div class=\"text-muted small mt-1\"><i class=\"bi bi-clock me-1\"></i>{{model.metadata.timezone}}</div>\n {{/model.metadata.timezone}}\n </div>\n </div>\n\n <!-- Right Side: Status & Actions -->\n <div class=\"d-flex align-items-start gap-4\">\n <div class=\"text-end\">\n <div class=\"d-flex align-items-center justify-content-end gap-3\">\n <span class=\"d-inline-flex align-items-center gap-1\" title=\"{{model.is_active|boolean('Group Active','Group Inactive')}}\">\n <i class=\"bi {{model.is_active|boolean('bi-toggle-on text-success','bi-toggle-off text-secondary')}}\" style=\"font-size: 1.1rem;\"></i>\n <span class=\"small\">{{model.is_active|boolean('Active','Inactive')}}</span>\n </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 {{#model.created}}\n <div class=\"text-muted small mt-1\">Created {{model.created|date}}</div>\n {{/model.created}}\n </div>\n <div data-container=\"group-context-menu\"></div>\n </div>\n </div>`\n });\n this.header.setModel(this.model);\n this.addChild(this.header);\n\n // ── Details section ─────────────────────────\n const detailsView = new View({\n model: this.model,\n template: `\n <style>\n .gv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .gv-section-label:first-child { margin-top: 0; }\n .gv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .gv-field-row:last-child { border-bottom: none; }\n .gv-field-label { width: 140px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .gv-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .gv-field-action { color: #6c757d; cursor: pointer; font-size: 0.8rem; margin-left: auto; padding: 0.15rem 0.4rem; border-radius: 4px; background: none; border: none; }\n .gv-field-action:hover { background: #f0f0f0; color: #0d6efd; }\n </style>\n\n <div class=\"gv-section-label\">Group</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Name</div>\n <div class=\"gv-field-value\">{{model.name}}</div>\n <button type=\"button\" class=\"gv-field-action\" data-action=\"edit-group\" title=\"Edit\"><i class=\"bi bi-pencil\"></i></button>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Kind</div>\n <div class=\"gv-field-value\"><span class=\"badge bg-primary bg-opacity-10 text-primary\">{{model.kind|capitalize}}</span></div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Status</div>\n <div class=\"gv-field-value\">\n {{#model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#d1e7dd; color:#0f5132; border-radius:3px;\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span style=\"font-size:0.65rem; padding:0.15em 0.45em; background:#fff3cd; color:#856404; border-radius:3px;\">Inactive</span>{{/model.is_active|bool}}\n </div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">ID</div>\n <div class=\"gv-field-value\" style=\"font-family: ui-monospace, monospace; font-size: 0.82rem;\">{{model.id}}</div>\n </div>\n\n <div class=\"gv-section-label\">Hierarchy</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Parent</div>\n <div class=\"gv-field-value\">\n {{#model.parent}}\n <a href=\"#\" data-action=\"view-parent\" data-id=\"{{model.parent.id}}\" class=\"text-decoration-none\">{{model.parent.name}}</a>\n <span class=\"text-muted small ms-1\">({{model.parent.kind|capitalize}})</span>\n {{/model.parent}}\n {{^model.parent}}<span style=\"color:#adb5bd; font-style:italic; font-size:0.85rem;\">None — top-level group</span>{{/model.parent}}\n </div>\n </div>\n\n <div class=\"gv-section-label\">Settings</div>\n {{#model.metadata.timezone}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Timezone</div>\n <div class=\"gv-field-value\">{{model.metadata.timezone}}</div>\n </div>\n {{/model.metadata.timezone}}\n {{#model.metadata.domain}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Domain</div>\n <div class=\"gv-field-value\">{{model.metadata.domain}}</div>\n </div>\n {{/model.metadata.domain}}\n {{#model.metadata.portal}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Portal URL</div>\n <div class=\"gv-field-value\"><a href=\"{{model.metadata.portal}}\" target=\"_blank\" class=\"text-decoration-none\">{{model.metadata.portal}}</a></div>\n </div>\n {{/model.metadata.portal}}\n {{#model.metadata.eod_hour}}\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">End of Day</div>\n <div class=\"gv-field-value\">{{model.metadata.eod_hour}}:00</div>\n </div>\n {{/model.metadata.eod_hour}}\n\n <div class=\"gv-section-label\">Dates</div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Created</div>\n <div class=\"gv-field-value\">{{model.created|datetime|default('—')}}</div>\n </div>\n <div class=\"gv-field-row\">\n <div class=\"gv-field-label\">Modified</div>\n <div class=\"gv-field-value\">{{model.modified|datetime|default('—')}}</div>\n </div>\n `\n });\n\n // ── Members ─────────────────────────────────\n const membersView = new TableView({\n collection: new MemberList({ params: { group: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['group'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Invite',\n onAdd: (event) => this.onInviteClick(event),\n columns: [\n { key: 'user.display_name', label: 'User', sortable: true },\n { key: 'user.email', label: 'Email', sortable: true },\n { key: 'permissions|keys|badge', label: 'Permissions' },\n { key: 'created', label: 'Joined', formatter: 'date', sortable: true }\n ]\n });\n\n // ── Children (sub-groups) ───────────────────\n const childrenView = new TableView({\n collection: new GroupList({ params: { parent: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['parent'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Add Group',\n onAdd: () => this.onActionAddChildGroup(),\n columns: [\n { key: 'name', label: 'Name', sortable: true },\n { key: 'kind', label: 'Kind', formatter: 'badge' },\n {\n key: 'is_active', label: 'Status', width: '80px',\n template: `\n {{#model.is_active|bool}}<span class=\"badge bg-success bg-opacity-10 text-success\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"badge bg-secondary bg-opacity-10 text-secondary\">Inactive</span>{{/model.is_active|bool}}`\n },\n { key: 'created', label: 'Created', formatter: 'date', sortable: true }\n ]\n });\n\n // ── Events ──────────────────────────────────\n const eventsView = new TableView({\n collection: new IncidentEventList({\n params: { size: 10, model_name: 'account.Group', model_id: this.model.get('id') }\n }),\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '150px' },\n { key: 'category|badge', label: 'Category' },\n { key: 'title', label: 'Title' }\n ]\n });\n\n // ── API Keys ──────────────────────────────────\n const apiKeysView = new TableView({\n collection: new ApiKeyList({ params: { group: this.model.get('id'), size: 10 } }),\n hideActivePillNames: ['group'],\n clickAction: 'view',\n showAdd: true,\n addButtonLabel: 'Create Key',\n addFormConfig: {\n ...ApiKeyForms.create,\n defaults: { group: this.model.get('id') }\n },\n columns: [\n { key: 'name', label: 'Name', sortable: true },\n {\n key: 'is_active', label: 'Status', width: '80px',\n template: `\n {{#model.is_active|bool}}<span class=\"badge bg-success bg-opacity-10 text-success\">Active</span>{{/model.is_active|bool}}\n {{^model.is_active|bool}}<span class=\"badge bg-secondary bg-opacity-10 text-secondary\">Inactive</span>{{/model.is_active|bool}}`\n },\n { key: 'permissions|keys|badge', label: 'Permissions' },\n { key: 'created', label: 'Created', formatter: 'datetime', sortable: true }\n ]\n });\n\n // ── Metadata ─────────────────────────────────\n const metadataView = new AdminMetadataSection({ model: this.model });\n\n // ── Logs ────────────────────────────────────\n const logsView = new TableView({\n collection: new LogList({\n params: { size: 10, model_name: 'account.Group', model_id: this.model.get('id') }\n }),\n permissions: 'view_logs',\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n {\n key: 'created', label: 'Timestamp', sortable: true, formatter: 'epoch|datetime',\n 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 ' }\n },\n {\n key: 'level', label: 'Level', sortable: true,\n filter: { type: 'select', options: [{ value: 'info', label: 'Info' }, { value: 'warning', label: 'Warning' }, { value: 'error', label: 'Error' }] }\n },\n { key: 'kind', label: 'Kind', filter: { type: 'text' } },\n { name: 'log', label: 'Log' }\n ]\n });\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'group-sidenav',\n activeSection: 'details',\n navWidth: 180,\n contentPadding: '1.25rem 2rem',\n enableResponsive: true,\n minWidth: 500,\n sections: [\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView },\n { key: 'members', label: 'Members', icon: 'bi-people', view: membersView },\n { key: 'children', label: 'Sub-Groups', icon: 'bi-diagram-3', view: childrenView },\n { key: 'api_keys', label: 'API Keys', icon: 'bi-key', view: apiKeysView },\n { type: 'divider', label: 'Activity' },\n { key: 'events', label: 'Events', icon: 'bi-calendar-event', view: eventsView },\n { key: 'logs', label: 'Logs', icon: 'bi-journal-text', view: logsView, permissions: 'view_logs' },\n { type: 'divider', label: 'Settings' },\n { key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: metadataView }\n ]\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n const groupMenu = new ContextMenu({\n containerId: 'group-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit Group', action: 'edit-group', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'Invite Member', action: 'invite-member', icon: 'bi-person-plus' },\n { label: 'Add Sub-Group', action: 'add-child-group', icon: 'bi-diagram-3' },\n { type: 'divider' },\n this.model.get('is_active')\n ? { label: 'Deactivate Group', action: 'deactivate-group', icon: 'bi-toggle-off' }\n : { label: 'Activate Group', action: 'activate-group', icon: 'bi-toggle-on' },\n ]\n }\n });\n this.addChild(groupMenu);\n }\n\n // ── Actions ─────────────────────────────────\n\n async onActionEditGroup() {\n const resp = await Modal.modelForm({\n title: `Edit Group — ${this.model.get('name')}`,\n model: this.model,\n size: 'lg',\n formConfig: GroupForms.detailed,\n });\n if (resp) {\n await this.render();\n }\n }\n\n async onActionInviteMember() {\n return this.onInviteClick(new Event('click'));\n }\n\n async onInviteClick(event) {\n if (event?.preventDefault) {\n event.preventDefault();\n event.stopPropagation();\n }\n const data = await Modal.form({\n title: `Invite User to ${this.model.get('name')}`,\n size: 'sm',\n fields: [\n { type: 'email', name: 'email', label: 'Email', required: true, cols: 12 }\n ]\n });\n if (!data?.email) return true;\n\n const app = this.getApp();\n const resp = await app.rest.POST('/api/group/member/invite', {\n group: this.model.id,\n email: data.email\n });\n if (resp.success) {\n app.toast.success('User invited successfully');\n // Refresh members if on that section\n if (this.sideNavView?.getActiveSection() === 'members') {\n await this.sideNavView.showSection('members');\n }\n } else {\n app.toast.error(resp.message || 'Failed to invite user');\n }\n return true;\n }\n\n async onActionAddChildGroup() {\n const data = await Modal.form({\n title: `Add Sub-Group to ${this.model.get('name')}`,\n size: 'sm',\n fields: GroupForms.create.fields.filter(f => f.name !== 'parent')\n });\n if (!data) return true;\n\n data.parent = this.model.id;\n const newGroup = new Group(data);\n const resp = await newGroup.save();\n if (resp.status === 200 || resp.status === 201) {\n this.getApp()?.toast?.success('Sub-group created');\n if (this.sideNavView?.getActiveSection() === 'children') {\n await this.sideNavView.showSection('children');\n }\n } else {\n this.getApp()?.toast?.error(resp.message || 'Failed to create sub-group');\n }\n return true;\n }\n\n async onActionDeactivateGroup() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to deactivate <strong>${this.model.get('name')}</strong>?`,\n 'Deactivate Group'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: false });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Group deactivated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to deactivate group');\n }\n return true;\n }\n\n async onActionActivateGroup() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to activate <strong>${this.model.get('name')}</strong>?`,\n 'Activate Group'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.save({ is_active: true });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Group activated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to activate group');\n }\n return true;\n }\n\n async onActionViewParent(event, element) {\n const parentId = element?.dataset?.id;\n if (!parentId) return true;\n\n const parent = new Group({ id: parentId });\n await parent.fetch();\n if (parent.id) {\n Modal.dialog({\n title: false,\n size: 'lg',\n body: new GroupView({ model: parent }),\n buttons: [{ text: 'Close', class: 'btn-secondary', dismiss: true }]\n });\n }\n return true;\n }\n\n // ── Navigation helpers ──────────────────────\n\n async showSection(sectionName) {\n if (this.sideNavView) {\n await this.sideNavView.showSection(sectionName);\n }\n }\n\n getActiveSection() {\n return this.sideNavView ? this.sideNavView.getActiveSection() : null;\n }\n\n _onModelChange() {\n // Prevent full re-render on model changes\n }\n\n static create(options = {}) {\n return new GroupView(options);\n }\n}\n\nGroup.VIEW_CLASS = GroupView;\n\nexport default GroupView;\n","/**\n * GroupTablePage - Group management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { GroupList, GroupForms, Group } from '@core/models/Group.js';\nimport GroupView from './GroupView.js';\n\nclass GroupTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_groups',\n pageName: 'Manage Groups',\n router: \"admin/groups\",\n Collection: GroupList,\n\n formCreate: GroupForms.create,\n formEdit: GroupForms.edit,\n itemViewClass: GroupView,\n\n viewDialogOptions: {\n header: false\n },\n\n defaultQuery: {\n sort: '-id',\n is_active: 1,\n },\n\n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n sortable: true,\n class: 'text-muted'\n },\n\n {\n key: 'name',\n label: 'Display Name'\n },\n {\n key: 'kind|badge',\n label: 'Kind',\n filter: {\n type: \"select\",\n options: Group.GroupKindOptions\n }\n },\n {\n key: 'is_active|yesnoicon',\n label: 'Enabled',\n visibility: 'lg'\n },\n {\n key: 'parent.name',\n label: 'Parent',\n formatter: \"default('-')\",\n visibility: 'md',\n class: 'text-muted fs-8'\n },\n {\n key: 'created',\n label: 'Created',\n className: 'text-muted fs-8',\n formatter: \"epoch|datetime\",\n visibility: 'lg'\n },\n {\n key: 'last_activity',\n label: 'Activity',\n className: 'text-muted fs-8',\n formatter: \"relative\",\n visibility: 'lg'\n }\n ],\n\n filters: [\n {\n key: 'is_active',\n label: 'Active',\n type: 'select',\n options: [\n { label: 'Active', value: true },\n { label: 'Inactive', value: false }\n ]\n }\n ],\n\n contextMenu: [\n {\n icon: 'bi-pencil',\n action: 'edit',\n label: \"Edit Group\"\n },\n {\n icon: 'bi-bullseye',\n action: 'make-active',\n label: \"Make Active Group\"\n }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No groups found. Click \"Add Group\" to create your first one.',\n\n // Batch actions\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Delete\", icon: \"bi bi-trash\", action: \"batch-delete\" },\n { label: \"Export\", icon: \"bi bi-download\", action: \"batch-export\" },\n { label: \"Activate\", icon: \"bi bi-check-circle\", action: \"batch-activate\" },\n { label: \"Deactivate\", icon: \"bi bi-x-circle\", action: \"batch-deactivate\" },\n { label: \"Move\", icon: \"bi bi-arrow-right\", action: \"batch-move\" }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n },\n });\n }\n\n onActionMakeActive(event, element) {\n const item = this.collection.get(element.dataset.id);\n this.getApp().setActiveGroup(item);\n }\n\n}\n\nexport default GroupTablePage;\n","/**\n * DeviceView - Clean, modern user device detail view\n *\n * SideNavView layout with:\n * - Details: browser, OS, device, user agent in clean field rows\n * - Locations: multiline table of all locations this device connected from\n * - Header: smart browser/OS icon, device summary, last seen, context menu\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport TableRow from '@core/views/table/TableRow.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { UserDevice, UserDeviceLocationList } from '@core/models/User.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\n// ── Location row for multiline table ──\n\nclass DeviceLocationRow extends TableRow {\n get locationText() {\n const geo = this.model?.get('geolocation') || {};\n return [geo.city, geo.region].filter(Boolean).join(', ') || geo.country_name || '—';\n }\n get countryName() {\n return this.model?.get('geolocation')?.country_name || '';\n }\n get ispName() {\n const geo = this.model?.get('geolocation') || {};\n return geo.isp || geo.asn_org || '';\n }\n get threatFlags() {\n const geo = this.model?.get('geolocation') || {};\n const flags = [];\n if (geo.is_vpn) flags.push('<span class=\"badge bg-warning text-dark\" style=\"font-size:0.6rem;\">VPN</span>');\n if (geo.is_tor) flags.push('<span class=\"badge bg-danger\" style=\"font-size:0.6rem;\">Tor</span>');\n if (geo.is_proxy) flags.push('<span class=\"badge bg-warning text-dark\" style=\"font-size:0.6rem;\">Proxy</span>');\n return flags.join(' ');\n }\n get hasThreatFlags() {\n const geo = this.model?.get('geolocation') || {};\n return !!(geo.is_vpn || geo.is_tor || geo.is_proxy);\n }\n}\n\nclass DeviceView extends View {\n constructor(options = {}) {\n super({\n className: 'device-view',\n ...options\n });\n\n this.model = options.model || new UserDevice(options.data || {});\n this.deviceInfo = this.model.get('device_info') || {};\n this.deviceIcon = this._getIcon(this.deviceInfo);\n\n // Computed properties for template\n this.browserFull = this._getBrowser();\n this.osFull = this._getOS();\n this.deviceFull = this._getDevice();\n this.isMobile = this._isMobile();\n\n this.template = `\n <style>\n .dv-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; }\n .dv-identity { display: flex; align-items: center; gap: 1rem; }\n .dv-icon-wrap { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.4rem; flex-shrink: 0; }\n .dv-title { font-size: 1.15rem; font-weight: 600; margin: 0; line-height: 1.3; }\n .dv-subtitle { font-size: 0.8rem; color: #6c757d; margin-top: 0.15rem; }\n .dv-status { text-align: right; display: flex; align-items: flex-start; gap: 0.75rem; }\n .dv-last-seen-label { font-size: 0.7rem; color: #adb5bd; text-transform: uppercase; letter-spacing: 0.04em; }\n .dv-last-seen-value { font-size: 0.88rem; font-weight: 500; }\n .dv-last-seen-ip { font-size: 0.75rem; color: #6c757d; margin-top: 0.1rem; }\n </style>\n\n <div class=\"dv-header\">\n <div class=\"dv-identity\">\n <div class=\"dv-icon-wrap bg-primary bg-opacity-10 text-primary\">\n <i class=\"bi {{deviceIcon}}\"></i>\n </div>\n <div>\n <h4 class=\"dv-title\">{{browserFull}} <span class=\"fw-normal text-muted\">on</span> {{osFull}}</h4>\n <div class=\"dv-subtitle\">\n {{deviceFull}}\n {{#model.user.display_name}}\n <span class=\"text-muted mx-1\">·</span>\n <a href=\"#\" data-action=\"view-user\" class=\"text-decoration-none\">{{model.user.display_name}}</a>\n {{/model.user.display_name}}\n </div>\n </div>\n </div>\n <div class=\"dv-status\">\n <div>\n <div class=\"dv-last-seen-label\">Last Seen</div>\n <div class=\"dv-last-seen-value\">{{model.last_seen|relative}}</div>\n {{#model.last_ip}}<div class=\"dv-last-seen-ip\">{{model.last_ip}}</div>{{/model.last_ip}}\n </div>\n <div data-container=\"device-context-menu\"></div>\n </div>\n </div>\n\n <div data-container=\"device-sidenav\" style=\"min-height: 300px;\"></div>\n `;\n }\n\n // ── Computed getters ──────────────────────────\n\n _getBrowser() {\n const ua = this.deviceInfo?.user_agent || {};\n const parts = [ua.family, ua.major].filter(Boolean);\n return parts.length ? parts.join(' ') : 'Unknown Browser';\n }\n\n _getOS() {\n const os = this.deviceInfo?.os || {};\n const ver = [os.major, os.minor].filter(Boolean).join('.');\n return os.family ? `${os.family} ${ver}`.trim() : 'Unknown OS';\n }\n\n _getDevice() {\n const dev = this.deviceInfo?.device || {};\n const parts = [dev.brand, dev.family].filter(Boolean);\n const name = parts.length ? parts.join(' ') : 'Unknown Device';\n return dev.model ? `${name} (${dev.model})` : name;\n }\n\n _isMobile() {\n const dev = this.deviceInfo?.device || {};\n const os = this.deviceInfo?.os || {};\n return ['iPhone', 'Android', 'iPad'].some(m =>\n (dev.family || '').includes(m) || (os.family || '').includes(m)\n );\n }\n\n _getIcon(deviceInfo) {\n const os = deviceInfo?.os?.family?.toLowerCase() || '';\n const browser = deviceInfo?.user_agent?.family?.toLowerCase() || '';\n const device = deviceInfo?.device?.family?.toLowerCase() || '';\n\n if (browser.includes('chrome')) return 'bi-browser-chrome';\n if (browser.includes('firefox')) return 'bi-browser-firefox';\n if (browser.includes('safari')) return 'bi-browser-safari';\n if (browser.includes('edge')) return 'bi-browser-edge';\n if (os.includes('mac') || os.includes('ios')) return 'bi-apple';\n if (os.includes('windows')) return 'bi-windows';\n if (os.includes('android')) return 'bi-android2';\n if (os.includes('linux')) return 'bi-ubuntu';\n if (device.includes('iphone')) return 'bi-phone';\n if (device.includes('ipad')) return 'bi-tablet';\n return 'bi-laptop';\n }\n\n async onInit() {\n // ── Details section ─────────────────────────\n const detailsView = new View({\n model: this.model,\n template: `\n <style>\n .dv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .dv-section-label:first-child { margin-top: 0; }\n .dv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .dv-field-row:last-child { border-bottom: none; }\n .dv-field-label { width: 130px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .dv-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .dv-ua-string { font-family: ui-monospace, monospace; font-size: 0.73rem; color: #6c757d; word-break: break-all; line-height: 1.5; padding: 0.5rem 0.75rem; background: #f8f9fa; border-radius: 6px; margin-top: 0.25rem; }\n </style>\n\n <div class=\"dv-section-label\">Browser</div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Name</div>\n <div class=\"dv-field-value\">{{model.device_info.user_agent.family|default('—')}}</div>\n </div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Version</div>\n <div class=\"dv-field-value\">{{model.device_info.user_agent.major|default('—')}}{{#model.device_info.user_agent.minor}}.{{model.device_info.user_agent.minor}}{{/model.device_info.user_agent.minor}}{{#model.device_info.user_agent.patch}}.{{model.device_info.user_agent.patch}}{{/model.device_info.user_agent.patch}}</div>\n </div>\n\n <div class=\"dv-section-label\">Operating System</div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Name</div>\n <div class=\"dv-field-value\">{{model.device_info.os.family|default('—')}}</div>\n </div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Version</div>\n <div class=\"dv-field-value\">{{model.device_info.os.major|default('—')}}{{#model.device_info.os.minor}}.{{model.device_info.os.minor}}{{/model.device_info.os.minor}}{{#model.device_info.os.patch}}.{{model.device_info.os.patch}}{{/model.device_info.os.patch}}</div>\n </div>\n\n <div class=\"dv-section-label\">Hardware</div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Brand</div>\n <div class=\"dv-field-value\">{{model.device_info.device.brand|default('—')}}</div>\n </div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Family</div>\n <div class=\"dv-field-value\">{{model.device_info.device.family|default('—')}}</div>\n </div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Model</div>\n <div class=\"dv-field-value\">{{model.device_info.device.model|default('—')}}</div>\n </div>\n\n <div class=\"dv-section-label\">Identification</div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Device ID</div>\n <div class=\"dv-field-value\" style=\"font-family: ui-monospace, monospace; font-size: 0.78rem;\">{{model.duid|truncate_middle(32)}}</div>\n </div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Last IP</div>\n <div class=\"dv-field-value\">{{model.last_ip|default('—')}}</div>\n </div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">First Seen</div>\n <div class=\"dv-field-value\">{{model.first_seen|epoch|datetime|default('—')}}</div>\n </div>\n <div class=\"dv-field-row\">\n <div class=\"dv-field-label\">Last Seen</div>\n <div class=\"dv-field-value\">{{model.last_seen|epoch|datetime|default('—')}}</div>\n </div>\n\n {{#model.device_info.string}}\n <div class=\"dv-section-label\">User Agent String</div>\n <div class=\"dv-ua-string\">{{model.device_info.string}}</div>\n {{/model.device_info.string}}\n `\n });\n\n // ── Locations section — multiline rows ──────\n const locationsView = new TableView({\n collection: new UserDeviceLocationList({\n params: { user_device: this.model.get('id'), size: 10 }\n }),\n hideActivePillNames: ['user_device'],\n clickAction: 'view',\n itemClass: DeviceLocationRow,\n selectable: false,\n columns: [\n {\n key: 'ip_address',\n label: 'Location',\n template: `\n <div style=\"font-size:0.85rem; font-weight:500;\">\n <i class=\"bi bi-geo-alt text-muted me-1\" style=\"font-size:0.95rem; vertical-align:middle;\"></i>{{locationText}}\n {{#countryName}} <span class=\"text-muted fw-normal\">· {{countryName}}</span>{{/countryName}}\n </div>\n <div style=\"font-size:0.73rem; color:#6c757d; margin-top:0.15rem;\">\n {{model.ip_address}}\n {{#ispName}} <span class=\"text-muted mx-1\">·</span> {{ispName}}{{/ispName}}\n {{#hasThreatFlags|bool}} <span class=\"ms-1\">{{{threatFlags}}}</span>{{/hasThreatFlags|bool}}\n </div>`\n },\n { key: 'first_seen', label: 'First Seen', formatter: 'epoch|relative', width: '110px' },\n { key: 'last_seen', label: 'Last Seen', formatter: 'epoch|relative', width: '110px' }\n ]\n });\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'device-sidenav',\n activeSection: 'details',\n navWidth: 160,\n contentPadding: '1rem 1.5rem',\n enableResponsive: true,\n minWidth: 450,\n sections: [\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView },\n { key: 'locations', label: 'Locations', icon: 'bi-geo-alt', view: locationsView }\n ]\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n const deviceMenu = new ContextMenu({\n containerId: 'device-context-menu',\n className: \"context-menu-view header-menu-absolute\",\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'View User', action: 'view-user', icon: 'bi-person' },\n { label: 'Block Device', action: 'block-device', icon: 'bi-shield-slash', disabled: true },\n { type: 'divider' },\n { label: 'Delete Record', action: 'delete-device', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(deviceMenu);\n }\n\n async onActionViewUser() {\n this.emit('view-user', { userId: this.model.get('user')?.id });\n }\n\n async onActionDeleteDevice() {\n const confirmed = await Modal.confirm(\n 'Are you sure you want to delete this device record?',\n 'Delete Device'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.destroy();\n if (resp.success) {\n this.emit('device:deleted', { model: this.model });\n }\n return true;\n }\n\n static async show(duid) {\n const model = await UserDevice.getByDuid(duid);\n if (model) {\n return Modal.dialog({\n title: false,\n size: 'lg',\n body: new DeviceView({ model }),\n buttons: [{ text: 'Close', class: 'btn-secondary', dismiss: true }]\n });\n }\n Modal.alert({ message: `Could not find device with DUID: ${duid}`, type: 'warning' });\n return null;\n }\n}\n\nUserDevice.VIEW_CLASS = DeviceView;\nexport default DeviceView;\n","/**\n * UserDeviceTablePage - User device management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { UserDeviceList } from '@core/models/User.js';\nimport DeviceView from './DeviceView.js';\n\nclass UserDeviceTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_user_devices',\n pageName: 'User Devices',\n router: \"admin/user/devices\",\n Collection: UserDeviceList,\n\n // itemViewClass: DeviceView,\n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n\n // Column definitions\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'duid', label: 'Device ID', sortable: true, formatter: 'truncate_middle(16)' },\n { key: 'user.display_name', label: 'User', sortable: true, formatter: \"default('—')\" },\n { key: 'device_info.user_agent.family', label: 'Browser', formatter: \"default('—')\" },\n { key: 'device_info.os.family', label: 'OS', formatter: \"default('—')\" },\n { key: 'last_ip', label: 'Last IP', sortable: true },\n { key: 'first_seen', label: 'First Seen', formatter: \"epoch|datetime\" },\n { key: 'last_seen', label: 'Last Seen', formatter: \"epoch|datetime\" }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No user devices found.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default UserDeviceTablePage;\n","/**\n * UserDeviceLocationTablePage - Login locations with map and table tabs\n *\n * Tab 1 \"Map\": Interactive login location map (system-wide summary)\n * Tab 2 \"Logins\": Paginated login events table\n */\n\nimport Page from '@core/Page.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport { LoginEventList } from '@ext/admin/models/LoginEvent.js';\nimport LoginLocationMapView from './LoginLocationMapView.js';\n\nclass UserDeviceLocationTablePage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n pageName: 'Device Locations',\n className: 'device-locations-page'\n });\n\n this.name = options.name || 'admin_user_device_locations';\n this.route = options.router || 'admin/user/device-locations';\n }\n\n async getTemplate() {\n return `\n <div class=\"container-fluid\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div>\n <h4 class=\"mb-1\">Device Locations</h4>\n <p class=\"text-muted mb-0 small\">Login locations across all users</p>\n </div>\n </div>\n <div data-container=\"tabs\"></div>\n </div>\n `;\n }\n\n async onInit() {\n const loginMapView = new LoginLocationMapView({\n height: 400,\n mapStyle: 'dark'\n });\n\n const loginEventsTable = new TableView({\n collection: new LoginEventList({ params: { size: 20 } }),\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n showRefresh: true,\n showExport: true,\n columns: [\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '160px' },\n { key: 'user.display_name', label: 'User', sortable: true },\n { key: 'ip_address', label: 'IP Address', sortable: true },\n { key: 'city', label: 'City', formatter: \"default('—')\" },\n { key: 'region', label: 'Region', formatter: \"default('—')\" },\n { key: 'country_code', label: 'Country', sortable: true },\n { key: 'source', label: 'Source', sortable: true },\n { key: 'is_new_country', label: 'New Country', formatter: 'boolean', sortable: true, width: '110px' }\n ]\n });\n loginEventsTable.onTabActivated = async () => {\n await loginEventsTable.collection?.fetch();\n };\n\n this.tabView = new TabView({\n containerId: 'tabs',\n tabs: {\n 'Map': loginMapView,\n 'Logins': loginEventsTable\n },\n activeTab: 'Map'\n });\n this.addChild(this.tabView);\n }\n}\n\nexport default UserDeviceLocationTablePage;\n","/**\n * GeoIPView - Detailed view for a GeoLocatedIP record\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport MapView from '@ext/map/MapView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { GeoLocatedIP } from '@core/models/System.js';\nimport { IncidentEventList } from '@ext/admin/models/Incident.js';\nimport { LogList } from '@core/models/Log.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass GeoIPView extends View {\n constructor(options = {}) {\n super({\n className: 'geoip-view',\n ...options\n });\n\n this.model = options.model || new GeoLocatedIP(options.data || {});\n this.hasCoordinates = this.model.get('latitude') && this.model.get('longitude');\n\n this.template = `\n <div class=\"geoip-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-center mb-4\">\n <!-- Left Side: Icon & Info -->\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 <!-- Right Side: Risk Summary + Actions -->\n <div class=\"d-flex align-items-start gap-4\">\n <!-- Risk summary -->\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 <!-- Actions: context menu aligned to top (not vertically centered) -->\n <div class=\"d-flex align-items-start\">\n <div data-container=\"geoip-context-menu\"></div>\n </div>\n </div>\n </div>\n\n <!-- Content -->\n <div data-container=\"geoip-sidenav\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // Location section\n this.detailsView = new DataView({\n model: this.model,\n className: \"p-3\",\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'ip_address', label: 'IP Address', cols: 4 },\n { name: 'subnet', label: 'Subnet', cols: 4 },\n { name: 'country_name', label: 'Country', cols: 4 },\n { name: 'country_code', label: 'Country Code', cols: 4 },\n { name: 'region', label: 'Region', cols: 4 },\n { name: 'city', label: 'City', cols: 4 },\n { name: 'postal_code', label: 'Postal Code', cols: 4 },\n { name: 'timezone', label: 'Timezone', cols: 4 },\n { name: 'latitude', label: 'Latitude', cols: 4 },\n { name: 'longitude', label: 'Longitude', cols: 4 },\n ]\n });\n\n // Network section\n this.networkView = new DataView({\n model: this.model,\n className: \"p-3\",\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'is_tor', label: 'TOR Exit Node', formatter: 'yesnoicon', cols: 4 },\n { name: 'is_vpn', label: 'VPN', formatter: 'yesnoicon', cols: 4 },\n { name: 'is_proxy', label: 'Proxy', formatter: 'yesnoicon', cols: 4 },\n { name: 'is_cloud', label: 'Cloud Provider', formatter: 'yesnoicon', cols: 4 },\n { name: 'is_datacenter', label: 'Datacenter', formatter: 'yesnoicon', cols: 4 },\n { name: 'is_mobile', label: 'Mobile', formatter: 'yesnoicon', cols: 4 },\n { name: 'mobile_carrier', label: 'Mobile Carrier', cols: 8 },\n { name: 'asn', label: 'ASN', cols: 4 },\n { name: 'asn_org', label: 'ASN Organization', cols: 8 },\n { name: 'isp', label: 'ISP', cols: 12 },\n { name: 'connection_type', label: 'Connection Type', cols: 6 }\n ]\n });\n\n // Risk & Reputation section\n this.riskView = new DataView({\n model: this.model,\n className: \"p-3\",\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'threat_level', label: 'Threat Level', formatter: 'capitalize', cols: 6 },\n { name: 'risk_score', label: 'Risk Score', cols: 6 },\n { name: 'is_threat', label: 'Threat', formatter: 'yesnoicon', cols: 6 },\n { name: 'is_suspicious', label: 'Suspicious', formatter: 'yesnoicon', cols: 6 },\n { name: 'is_known_attacker', label: 'Known Attacker', formatter: 'yesnoicon', cols: 6 },\n { name: 'is_known_abuser', label: 'Known Abuser', formatter: 'yesnoicon', cols: 6 }\n ]\n });\n\n // Block & Whitelist section\n this.blockView = new DataView({\n model: this.model,\n className: \"p-3\",\n showEmptyValues: false,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'is_blocked', label: 'Blocked', formatter: 'yesnoicon', cols: 6 },\n { name: 'block_count', label: 'Block Count', cols: 6 },\n { name: 'blocked_reason', label: 'Block Reason', cols: 12 },\n { name: 'blocked_at', label: 'Blocked At', formatter: 'datetime', cols: 6 },\n { name: 'blocked_until', label: 'Blocked Until', formatter: 'datetime', cols: 6 },\n { name: 'is_whitelisted', label: 'Whitelisted', formatter: 'yesnoicon', cols: 6 },\n { name: 'whitelisted_reason', label: 'Whitelist Reason', cols: 6 }\n ]\n });\n\n // Metadata section\n this.metadataView = new DataView({\n model: this.model,\n className: \"p-3\",\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'id', label: 'Record ID', cols: 6 },\n { name: 'provider', label: 'Data Provider', formatter: 'capitalize', cols: 6 },\n { name: 'created', label: 'Created', formatter: 'datetime', cols: 6 },\n { name: 'modified', label: 'Last Modified', formatter: 'datetime', cols: 6 },\n { name: 'last_seen', label: 'Last Seen', formatter: 'datetime', cols: 6 },\n { name: 'expires_at', label: 'Expires', formatter: 'datetime', cols: 6 }\n ]\n });\n\n // Events section\n const eventsCollection = new IncidentEventList({\n params: {\n size: 5,\n source_ip: this.model.get(\"ip_address\")\n }\n });\n this.eventsView = new TableView({\n collection: eventsCollection,\n hideActivePillNames: ['source_ip'],\n columns: [\n { key: 'id', label: 'ID', sortable: true, width: '40px' },\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '150px' },\n { key: 'category|badge', label: 'Category' },\n { key: 'title', label: 'Title' }\n ]\n });\n\n // Traffic section — request logs from this IP\n const trafficCollection = new LogList({\n params: {\n size: 5,\n ip: this.model.get('ip_address')\n }\n });\n this.trafficView = new TableView({\n collection: trafficCollection,\n permissions: 'view_logs',\n hideActivePillNames: ['ip'],\n columns: [\n {\n key: 'created',\n label: 'Timestamp',\n sortable: true,\n formatter: \"epoch|datetime\",\n filter: {\n name: \"created\",\n type: 'daterange',\n startName: 'dr_start',\n endName: 'dr_end',\n fieldName: 'dr_field',\n label: 'Date Range',\n format: 'YYYY-MM-DD',\n displayFormat: 'MMM DD, YYYY',\n separator: ' to '\n }\n },\n {\n key: 'level',\n label: 'Level',\n sortable: true,\n filter: {\n type: 'select',\n options: [\n { value: 'info', label: 'Info' },\n { value: 'warning', label: 'Warning' },\n { value: 'error', label: 'Error' }\n ]\n }\n },\n { key: 'kind', label: 'Kind', filter: { type: 'text' } },\n { name: 'log', label: 'Log' }\n ]\n });\n\n // Logs section — audit logs for this GeoIP record\n const logsCollection = new LogList({\n params: {\n size: 5,\n model_name: 'account.GeoLocatedIP',\n model_id: this.model.get('id')\n }\n });\n this.logsView = new TableView({\n collection: logsCollection,\n permissions: 'view_logs',\n hideActivePillNames: ['model_name', 'model_id'],\n columns: [\n {\n key: 'created',\n label: 'Timestamp',\n sortable: true,\n formatter: \"epoch|datetime\"\n },\n {\n key: 'level',\n label: 'Level',\n sortable: true,\n filter: {\n type: 'select',\n options: [\n { value: 'info', label: 'Info' },\n { value: 'warning', label: 'Warning' },\n { value: 'error', label: 'Error' }\n ]\n }\n },\n { key: 'kind', label: 'Kind', filter: { type: 'text' } },\n { name: 'log', label: 'Log' }\n ]\n });\n\n // Build sections\n const sections = [];\n\n // Add Map section if coordinates exist\n if (this.hasCoordinates) {\n const lat = this.model.get('latitude');\n const lng = this.model.get('longitude');\n const city = this.model.get('city') || 'Unknown';\n const region = this.model.get('region') || '';\n const country = this.model.get('country_name') || '';\n const locationStr = [city, region, country].filter(Boolean).join(', ');\n\n this.mapView = new MapView({\n markers: [{\n lat: lat,\n lng: lng,\n popup: `<strong>${this.model.get('ip_address')}</strong><br>${locationStr}`\n }],\n tileLayer: \"light\",\n zoom: 4,\n height: 450\n });\n sections.push({ key: 'map', label: 'Map', icon: 'bi-map', view: this.mapView });\n }\n\n sections.push(\n { key: 'location', label: 'Location', icon: 'bi-geo-alt', view: this.detailsView },\n { key: 'network', label: 'Network', icon: 'bi-diagram-3', view: this.networkView },\n { key: 'risk', label: 'Risk & Reputation', icon: 'bi-shield-exclamation', view: this.riskView },\n { key: 'block', label: 'Block & Whitelist', icon: 'bi-slash-circle', view: this.blockView },\n { type: 'divider', label: 'Activity' },\n { key: 'events', label: 'Events', icon: 'bi-calendar-event', view: this.eventsView },\n { key: 'traffic', label: 'Traffic', icon: 'bi-arrow-left-right', view: this.trafficView, permissions: 'view_logs' },\n { key: 'logs', label: 'Logs', icon: 'bi-journal-text', view: this.logsView, permissions: 'view_logs' },\n { type: 'divider', label: 'Record' },\n { key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: this.metadataView }\n );\n\n this.sideNavView = new SideNavView({\n containerId: 'geoip-sidenav',\n activeSection: this.hasCoordinates ? 'map' : 'location',\n navWidth: 180,\n contentPadding: '1.25rem 2rem',\n enableResponsive: true,\n minWidth: 500,\n sections\n });\n this.addChild(this.sideNavView);\n\n // ContextMenu\n const menuItems = [\n { label: 'Edit Location', action: 'edit-location', icon: 'bi-geo-alt' },\n { label: 'Edit Security', action: 'edit-security', icon: 'bi-shield-lock' },\n { label: 'Edit Network', action: 'edit-network', icon: 'bi-diagram-3' },\n { type: 'divider' },\n { label: 'Refresh Geolocation', action: 'refresh-geoip', icon: 'bi-arrow-clockwise' },\n ];\n\n if (this.hasCoordinates) {\n menuItems.push({\n label: 'View on Map',\n action: 'view-on-map',\n icon: 'bi-map'\n });\n }\n\n // Firewall actions\n menuItems.push(\n { type: 'divider' },\n { label: 'Block IP', action: 'block-ip', icon: 'bi-slash-circle', class: 'text-danger' },\n { label: 'Unblock IP', action: 'unblock-ip', icon: 'bi-unlock', class: 'text-success' },\n { label: 'Whitelist IP', action: 'whitelist-ip', icon: 'bi-check-circle', class: 'text-primary' },\n { label: 'Remove Whitelist', action: 'unwhitelist-ip', icon: 'bi-x-circle' },\n { label: 'Refresh Threat Data', action: 'threat-analysis', icon: 'bi-shield-exclamation' }\n );\n\n menuItems.push(\n { type: 'divider' },\n { label: 'Delete Record', action: 'delete-geoip', icon: 'bi-trash', danger: true }\n );\n\n const geoIPMenu = new ContextMenu({\n containerId: 'geoip-context-menu',\n className: \"context-menu-view header-menu-absolute\",\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: menuItems\n }\n });\n this.addChild(geoIPMenu);\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Initialize Bootstrap tooltips for header icons/badges\n if (window.bootstrap && window.bootstrap.Tooltip && this.element) {\n const tooltipTriggerList = this.element.querySelectorAll('[data-bs-toggle=\"tooltip\"]');\n tooltipTriggerList.forEach(el => {\n // Dispose any existing instance (in case of re-render)\n const existing = window.bootstrap.Tooltip.getInstance(el);\n if (existing && typeof existing.dispose === 'function') {\n existing.dispose();\n }\n new window.bootstrap.Tooltip(el);\n });\n }\n }\n\n async onActionEditLocation() {\n const resp = await Modal.modelForm({\n title: `Edit Location - ${this.model.get('ip_address')}`,\n model: this.model,\n formConfig: GeoLocatedIP.EDIT_LOCATION_FORM,\n });\n\n if (resp) {\n await this.render();\n this.getApp()?.toast?.success('Location updated successfully');\n }\n }\n\n async onActionEditSecurity() {\n const resp = await Modal.modelForm({\n title: `Edit Security - ${this.model.get('ip_address')}`,\n model: this.model,\n formConfig: GeoLocatedIP.EDIT_SECURITY_FORM,\n });\n\n if (resp) {\n await this.render();\n this.getApp()?.toast?.success('Security settings updated successfully');\n }\n }\n\n async onActionEditNetwork() {\n const resp = await Modal.modelForm({\n title: `Edit Network - ${this.model.get('ip_address')}`,\n model: this.model,\n formConfig: GeoLocatedIP.EDIT_NETWORK_FORM,\n });\n\n if (resp) {\n await this.render();\n this.getApp()?.toast?.success('Network information updated successfully');\n }\n }\n\n async onActionRefreshGeoip() {\n // Placeholder for refresh logic, e.g., a POST request to a refresh endpoint\n await this.model.save({ refresh: true });\n this.getApp()?.toast?.info('Refresh request sent for ' + this.model.get('ip_address'));\n }\n\n async onActionBlockIp() {\n const data = await Modal.form({\n title: 'Block IP',\n icon: 'bi-slash-circle',\n size: 'sm',\n fields: [\n { name: 'reason', type: 'text', label: 'Reason', required: true, placeholder: 'e.g., Suspicious activity' },\n {\n name: 'ttl', type: 'select', label: 'Duration',\n options: [\n { value: 3600, label: '1 hour' },\n { value: 21600, label: '6 hours' },\n { value: 86400, label: '24 hours' },\n { value: 604800, label: '7 days' },\n { value: 2592000, label: '30 days' },\n { value: 0, label: 'Permanent' }\n ],\n value: 86400\n }\n ]\n });\n if (!data) return true;\n\n const resp = await this.model.save({\n block: { reason: data.reason, ttl: parseInt(data.ttl) }\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('IP blocked successfully');\n await this.model.fetch();\n } else {\n this.getApp()?.toast?.error('Failed to block IP');\n }\n return true;\n }\n\n async onActionUnblockIp() {\n const data = await Modal.form({\n title: 'Unblock IP',\n icon: 'bi-unlock',\n size: 'sm',\n fields: [\n { name: 'reason', type: 'text', label: 'Reason', placeholder: 'e.g., False positive' }\n ]\n });\n if (!data) return true;\n\n const resp = await this.model.save({\n unblock: data.reason || 'Unblocked from admin'\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('IP unblocked successfully');\n await this.model.fetch();\n } else {\n this.getApp()?.toast?.error('Failed to unblock IP');\n }\n return true;\n }\n\n async onActionWhitelistIp() {\n const data = await Modal.form({\n title: 'Whitelist IP',\n icon: 'bi-check-circle',\n size: 'sm',\n fields: [\n { name: 'reason', type: 'text', label: 'Reason', required: true, placeholder: 'e.g., Known office IP' }\n ]\n });\n if (!data) return true;\n\n const resp = await this.model.save({\n whitelist: data.reason\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('IP whitelisted successfully');\n await this.model.fetch();\n } else {\n this.getApp()?.toast?.error('Failed to whitelist IP');\n }\n return true;\n }\n\n async onActionUnwhitelistIp() {\n const confirmed = await Modal.confirm('Remove this IP from the whitelist?', 'Remove Whitelist');\n if (!confirmed) return true;\n\n const resp = await this.model.save({\n unwhitelist: 1\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('IP removed from whitelist');\n await this.model.fetch();\n } else {\n this.getApp()?.toast?.error('Failed to remove from whitelist');\n }\n return true;\n }\n\n async onActionThreatAnalysis(event, element) {\n try {\n if (element) element.disabled = true;\n const resp = await this.model.save({\n threat_analysis: 1\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('Threat data refreshed');\n await this.model.fetch();\n } else {\n this.getApp()?.toast?.error('Failed to refresh threat data');\n }\n } finally {\n if (element) element.disabled = false;\n }\n return true;\n }\n\n async onActionViewOnMap() {\n if (this.hasCoordinates) {\n const lat = this.model.get('latitude');\n const lon = this.model.get('longitude');\n const url = `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`;\n window.open(url, '_blank');\n }\n }\n\n async onActionDeleteGeoip() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete the GeoIP record for \"${this.model.get('ip_address')}\"?`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n if (confirmed) {\n const resp = await this.model.destroy();\n if (resp.success) {\n this.emit('geoip:deleted', { model: this.model });\n }\n }\n }\n\n static async show(ip) {\n const model = await GeoLocatedIP.lookup(ip);\n if (model) {\n const view = new GeoIPView({ model });\n await Modal.show(view, { size: 'lg', header: false });\n return;\n }\n Modal.alert({ message: `Could not find geolocation data for IP: ${ip}`, type: 'warning' });\n }\n}\n\nGeoLocatedIP.VIEW_CLASS = GeoIPView;\n\nexport default GeoIPView;\n","/**\n * GeoLocatedIPTablePage - GeoIP cache management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { GeoLocatedIPList, GeoLocatedIP } from '@core/models/System.js';\nimport GeoIPView from './GeoIPView.js';\n\nclass GeoLocatedIPTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_system_geoip',\n pageName: 'GeoIP Cache',\n router: \"admin/system/geoip\",\n Collection: GeoLocatedIPList,\n\n itemView: GeoIPView,\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n // Column definitions\n columns: [\n { key: 'ip_address', label: 'IP Address', sortable: true },\n { key: 'city', label: 'City', sortable: true, formatter: \"default('—')\" },\n { key: 'region', label: 'Region', sortable: true, formatter: \"default('—')\" },\n { key: 'country_name', label: 'Country', sortable: true, formatter: \"default('—')\" },\n { key: 'isp', label: 'ISP', sortable: true, formatter: \"default('—')\" },\n { key: 'threat_level', label: 'Threat', formatter: \"default('—')\"}\n ],\n\n // Filters\n filters: [\n {\n key: 'country_code',\n label: 'Country',\n type: 'text'\n },\n {\n key: 'threat_level',\n label: 'Threat Level',\n type: 'select',\n options: [\n { label: 'None', value: 'none' },\n { label: 'Low', value: 'low' },\n { label: 'Medium', value: 'medium' },\n { label: 'High', value: 'high' },\n { label: 'Critical', value: 'critical' }\n ]\n },\n {\n key: 'isp__icontains',\n label: 'ISP',\n type: 'text'\n },\n {\n key: 'is_blocked',\n label: 'Blocked',\n type: 'select',\n options: [\n { label: 'Yes', value: 'true' },\n { label: 'No', value: 'false' }\n ]\n },\n {\n key: 'is_vpn',\n label: 'VPN',\n type: 'select',\n options: [\n { label: 'Yes', value: 'true' },\n { label: 'No', value: 'false' }\n ]\n },\n {\n key: 'is_tor',\n label: 'TOR',\n type: 'select',\n options: [\n { label: 'Yes', value: 'true' },\n { label: 'No', value: 'false' }\n ]\n }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Actions\n // actions: ['view', 'edit', 'delete'],\n clickAction: 'view',\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No GeoIP records found.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n },\n tableViewOptions: {\n addButtonLabel: \"Lookup IP\",\n onAdd: (evt) => {\n evt.preventDefault();\n // Implement the logic for adding a new record\n this.onLookup();\n }\n }\n });\n }\n\n async onLookup() {\n // Implement the logic for adding a new record\n const data = await this.getApp().showForm({\n title: \"Lookup IP\",\n fields: [\n {\n name: 'ip',\n type: 'text',\n required: true\n }\n ]\n });\n if (data && data.ip) {\n const model = await GeoLocatedIP.lookup(data.ip);\n if (model) {\n this.tableView._onRowView({ model });\n }\n }\n }\n}\n\nexport default GeoLocatedIPTablePage;\n","/**\n * ApiKeyView - Group-scoped API key detail and management interface\n *\n * Shows key metadata, permissions, and provides actions to edit, toggle\n * active state, and delete. The raw token is only displayed at creation\n * time and is not shown here.\n */\n\nimport View from '@core/View.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { ApiKey, ApiKeyForms } from '@core/models/ApiKey.js';\n\nclass ApiKeyView extends View {\n constructor(options = {}) {\n super({\n className: 'api-key-view',\n ...options\n });\n\n this.model = options.model || new ApiKey(options.data || {});\n\n this.template = `\n <div class=\"api-key-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <!-- Left: Icon & Identity -->\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"fs-1 text-primary\">\n <i class=\"bi bi-key\"></i>\n </div>\n <div>\n <h3 class=\"mb-1\">{{model.name|default('Unnamed Key')}}</h3>\n <div class=\"text-muted small\">\n ID: {{model.id}}\n <span class=\"mx-2\">|</span>\n Group: {{model.group.name|default(model.group)}}\n </div>\n <div class=\"mt-1\">\n <span class=\"badge {{model.is_active|boolean('bg-success','bg-secondary')}}\">\n {{model.is_active|boolean('Active','Inactive')}}\n </span>\n </div>\n </div>\n </div>\n\n <!-- Right: Meta & Actions -->\n <div class=\"d-flex align-items-start gap-4\">\n <div class=\"text-end\">\n <div class=\"text-muted small\">Created</div>\n <div>{{model.created|datetime}}</div>\n </div>\n <div data-container=\"apikey-context-menu\"></div>\n </div>\n </div>\n\n <!-- Details -->\n <div class=\"list-group mb-3\">\n <div class=\"list-group-item\">\n <h6 class=\"mb-1 text-muted\">Token Preview</h6>\n <p class=\"mb-1 font-monospace small text-muted\">\n The raw token is only shown once at creation time.\n </p>\n </div>\n <div class=\"list-group-item\">\n <h6 class=\"mb-1 text-muted\">Permissions</h6>\n {{#model.permissions}}\n <pre class=\"mb-0 small\">{{model.permissions|json}}</pre>\n {{/model.permissions}}\n {{^model.permissions}}\n <span class=\"text-muted small\">No permissions granted</span>\n {{/model.permissions}}\n </div>\n {{#model.limits}}\n <div class=\"list-group-item\">\n <h6 class=\"mb-1 text-muted\">Rate Limit Overrides</h6>\n <pre class=\"mb-0 small\">{{model.limits|json}}</pre>\n </div>\n {{/model.limits}}\n <div class=\"list-group-item\">\n <h6 class=\"mb-1 text-muted\">Usage</h6>\n <p class=\"mb-0 small text-muted\">\n Include in requests as:\n <code>Authorization: apikey <token></code>\n </p>\n </div>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n const isActive = this.model.get('is_active');\n\n const apiKeyMenu = new ContextMenu({\n containerId: 'apikey-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit', action: 'edit-key', icon: 'bi-pencil' },\n isActive\n ? { label: 'Deactivate', action: 'deactivate-key', icon: 'bi-x-circle' }\n : { label: 'Activate', action: 'activate-key', icon: 'bi-check-circle' },\n { type: 'divider' },\n { label: 'Delete Key', action: 'delete-key', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(apiKeyMenu);\n }\n\n async onActionEditKey() {\n const app = this.getApp();\n const resp = await app.showModelForm({\n title: `Edit API Key — ${this.model.get('name')}`,\n model: this.model,\n formConfig: ApiKeyForms.edit,\n });\n if (resp) {\n this.render();\n }\n }\n\n async onActionDeactivateKey() {\n const app = this.getApp();\n const confirmed = await app.confirm({\n title: 'Deactivate API Key',\n message: `Deactivate \"${this.model.get('name')}\"? Requests using this key will be rejected.`,\n confirmLabel: 'Deactivate',\n confirmClass: 'btn-warning'\n });\n if (!confirmed) return;\n\n app.showLoading();\n const resp = await this.model.save({ is_active: false });\n app.hideLoading();\n if (resp && resp.success !== false) {\n app.toast.success('API key deactivated');\n this.render();\n } else {\n app.toast.error('Failed to deactivate key');\n }\n }\n\n async onActionActivateKey() {\n const app = this.getApp();\n app.showLoading();\n const resp = await this.model.save({ is_active: true });\n app.hideLoading();\n if (resp && resp.success !== false) {\n app.toast.success('API key activated');\n this.render();\n } else {\n app.toast.error('Failed to activate key');\n }\n }\n\n async onActionDeleteKey() {\n const app = this.getApp();\n const confirmed = await app.confirm({\n title: 'Delete API Key',\n message: `Permanently delete \"${this.model.get('name')}\"? This cannot be undone.`,\n confirmLabel: 'Delete',\n confirmClass: 'btn-danger'\n });\n if (!confirmed) return;\n\n app.showLoading();\n const resp = await this.model.delete();\n app.hideLoading();\n if (resp && resp.success !== false) {\n app.toast.success('API key deleted');\n this.emit('deleted', { model: this.model });\n } else {\n app.toast.error('Failed to delete key');\n }\n }\n}\n\nApiKey.VIEW_CLASS = ApiKeyView;\nexport default ApiKeyView;\n","/**\n * ApiKeyTablePage - Group API key management using TablePage component\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { ApiKey, ApiKeyList, ApiKeyForms } from '@core/models/ApiKey.js';\nimport ApiKeyView from './ApiKeyView.js';\n\n// Register the add/edit forms on the model class so TableView can find them automatically\nApiKey.ADD_FORM = ApiKeyForms.create;\nApiKey.EDIT_FORM = ApiKeyForms.edit;\n\nclass ApiKeyTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_api_keys',\n pageName: 'API Keys',\n router: 'admin/api-keys',\n Collection: ApiKeyList,\n\n itemViewClass: ApiKeyView,\n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'name', label: 'Name', sortable: true },\n { key: 'group.name', label: 'Group', sortable: true, formatter: \"default('—')\" },\n {\n key: 'is_active',\n label: 'Status',\n formatter: \"boolean('Active|bg-success','Inactive|bg-secondary')|badge\",\n width: '100px'\n },\n { key: 'created', label: 'Created', formatter: 'datetime', sortable: true }\n ],\n\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n showRefresh: true,\n showAdd: true,\n showExport: false,\n\n addButtonLabel: 'New API Key',\n\n emptyMessage: 'No API keys found.',\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n // Override to intercept and show the one-time token after model.save()\n async onActionAdd() {\n const app = this.getApp();\n const model = new ApiKey();\n const result = await app.showForm({\n model,\n ...ApiKeyForms.create\n });\n if (!result) return;\n\n const resp = await model.save(result);\n if (!resp?.data?.status) {\n app.showError(resp?.data?.error || 'Failed to create API key');\n return;\n }\n\n // Token is only present in the creation response — show it once\n const token = resp.data?.data?.token;\n await app.showAlert({\n title: 'API Key Created — Save Your Token',\n message: token\n ? `Copy this token now. It will not be shown again.\\n\\n${token}`\n : 'API key created successfully.',\n type: token ? 'warning' : 'success',\n size: 'lg'\n });\n\n this.collection.add(model);\n this.tableView?.refresh();\n }\n}\n\nexport default ApiKeyTablePage;\n","/**\n * CloudWatchChart - MetricsChart configured for CloudWatch endpoints\n *\n * Extends MetricsChart with:\n * - CloudWatch endpoint defaults (/api/aws/cloudwatch/fetch)\n * - `stat` parameter support (avg, max, min, sum)\n * - Response format normalization (periods → labels, {slug,values} → {slug: values})\n *\n * NOTE: The CloudWatch API currently returns a slightly different format\n * than the standard metrics API (periods vs labels, array vs dict).\n * processMetricsData normalizes this until the backend aligns.\n */\nimport MetricsChart from '@ext/charts/MetricsChart.js';\n\nexport default class CloudWatchChart extends MetricsChart {\n constructor(options = {}) {\n super({\n endpoint: '/api/aws/cloudwatch/fetch',\n account: options.resourceType || options.account || 'ec2',\n category: options.category || null,\n slugs: options.slugs || (options.slug ? [options.slug] : null),\n granularity: options.granularity || 'hours',\n title: options.title || 'CloudWatch',\n defaultDateRange: options.defaultDateRange || '24h',\n showDateRange: false,\n ...options\n });\n\n this.stat = options.stat || 'avg';\n this.resourceType = options.resourceType || options.account || 'ec2';\n }\n\n buildApiParams() {\n const params = super.buildApiParams();\n // CloudWatch uses 'stat' parameter\n params.stat = this.stat;\n return params;\n }\n\n setStat(stat) {\n this.stat = stat;\n return this.fetchData();\n }\n}\n","/**\n * CloudWatchDashboardPage — AWS CloudWatch monitoring dashboard.\n *\n * 2-column grid of MetricsCharts showing key metrics across all\n * resources. Each chart auto-plots all instances for its\n * account+category via /api/aws/cloudwatch/fetch.\n *\n * Performance:\n * - The page paints immediately. Only the first 4 charts (top two rows)\n * fetch on first paint. The remaining 8 are lazy-mounted and fetch\n * when scrolled into view.\n * - All charts share a 5-minute refresh tier; the page-level Refresh\n * button triggers `runScheduledRefreshes()` to refresh whatever's\n * currently mounted.\n */\nimport Page from '@core/Page.js';\nimport CloudWatchChart from './CloudWatchChart.js';\n\nconst DASHBOARD_CHARTS = [\n { account: 'ec2', category: 'cpu', title: 'EC2 CPU', unit: '%' },\n { account: 'ec2', category: 'net_out', title: 'EC2 Network Out', unit: 'bytes' },\n { account: 'ec2', category: 'memory', title: 'EC2 Memory', unit: '%' },\n { account: 'ec2', category: 'disk', title: 'EC2 Disk', unit: '%' },\n { account: 'rds', category: 'cpu', title: 'RDS CPU', unit: '%' },\n { account: 'rds', category: 'conns', title: 'RDS Connections', unit: '' },\n { account: 'rds', category: 'read_latency', title: 'RDS Read Latency', unit: 's' },\n { account: 'rds', category: 'write_latency', title: 'RDS Write Latency', unit: 's' },\n { account: 'redis', category: 'cpu', title: 'Redis CPU', unit: '%' },\n { account: 'redis', category: 'conns', title: 'Redis Connections', unit: '' },\n { account: 'redis', category: 'cache_misses', title: 'Redis Cache Misses', unit: '' },\n { account: 'redis', category: 'cache_hits', title: 'Redis Cache Hits', unit: '' }\n];\n\n// First N charts mount eagerly (above the fold on a typical desktop:\n// 2-column grid → 2 rows). Everything below lazy-mounts to keep first\n// paint fast and avoid hammering the CloudWatch API with N parallel\n// fetches.\nconst EAGER_CHART_COUNT = 4;\n\nfunction yAxisForUnit(unit) {\n if (unit === '%') return { label: '%', beginAtZero: true, max: 100 };\n if (unit === 'bytes') return { label: 'Bytes', beginAtZero: true };\n if (unit === 's') return { label: 'Seconds', beginAtZero: true };\n return { beginAtZero: true };\n}\n\nexport default class CloudWatchDashboardPage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n title: 'CloudWatch Monitoring',\n className: 'cloudwatch-dashboard-page'\n });\n this.pageSubtitle = 'AWS CloudWatch resource monitoring';\n }\n\n async getTemplate() {\n return `\n <style>\n .cw-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }\n @media (max-width: 992px) { .cw-grid { grid-template-columns: 1fr; } }\n </style>\n <div class=\"container-fluid\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div>\n <h1 class=\"h3 mb-1\">CloudWatch Monitoring</h1>\n <p class=\"text-muted mb-0\">{{pageSubtitle}}</p>\n </div>\n <button type=\"button\"\n class=\"btn btn-outline-secondary btn-sm\"\n data-action=\"refresh-all\"\n title=\"Refresh all charts\">\n <i class=\"bi bi-arrow-clockwise\"></i> Refresh\n </button>\n </div>\n <div class=\"cw-grid\" id=\"cw-grid\">\n ${DASHBOARD_CHARTS.map((_, i) => `<div id=\"cw-chart-${i}\"></div>`).join('')}\n </div>\n </div>\n `;\n }\n\n async onInit() {\n this.charts = [];\n for (let i = 0; i < DASHBOARD_CHARTS.length; i++) {\n const def = DASHBOARD_CHARTS[i];\n const chart = new CloudWatchChart({\n containerId: `cw-chart-${i}`,\n account: def.account,\n category: def.category,\n title: def.title,\n height: 160,\n yAxis: yAxisForUnit(def.unit),\n responsive: true,\n showGranularity: true,\n showDateRange: true,\n defaultDateRange: '24h',\n granularity: 'hours'\n });\n this.addChild(chart, { lazyMount: i >= EAGER_CHART_COUNT });\n this.charts.push(chart);\n }\n }\n\n async onEnter() {\n await super.onEnter();\n // Refresh every 5 minutes — CloudWatch metrics don't change second-\n // to-second, and the API rate limit is generous-but-not-infinite.\n // The handler refreshes only charts that have actually mounted.\n this.scheduleRefresh(() => this._refreshMounted(), 300_000, { tier: 'slow' });\n }\n\n async _refreshMounted() {\n // Refresh eager charts always, plus lazy charts that have actually\n // mounted (._lazyTriggered is set by the framework once the chart\n // scrolls into view). Lazy charts that haven't mounted yet are\n // skipped — refreshing them would defeat the lazy-mount pattern by\n // forcing an early fetch.\n await Promise.allSettled(\n this.charts\n .filter(c => c && (!c._lazyMount || c._lazyTriggered) && typeof c.refresh === 'function')\n .map(c => c.refresh())\n );\n }\n\n async onActionRefreshAll(event, element) {\n const button = element || event?.currentTarget || null;\n const icon = button?.querySelector?.('i');\n icon?.classList.add('bi-spin');\n if (button) button.disabled = true;\n try {\n await this.runScheduledRefreshes();\n } finally {\n icon?.classList.remove('bi-spin');\n if (button) button.disabled = false;\n }\n }\n}\n","/**\n * StatusStripPanel — the top \"pulse\" row.\n *\n * Wraps KPIStrip with the dashboard's specific tile spec list. Six\n * metric tiles fed by /api/metrics/series?with_delta=true plus two\n * REST count tiles (open incidents, active firewall blocks).\n *\n * Tile click is wired to open a per-slug history modal; this panel\n * forwards `tile:click` events from the strip and lets the dashboard\n * page (or a parent) decide what to do.\n */\n\nimport View from '@core/View.js';\nimport KPIStrip from '@ext/charts/KPIStrip.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport MetricsChart from '@ext/charts/MetricsChart.js';\n\nclass StatusStripPanel extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-status-strip-panel ${options.className || ''}`.trim()\n });\n }\n\n async getTemplate() {\n return `\n <div class=\"sd-section-head\">\n <h2 class=\"sd-eyebrow\">Pulse</h2>\n <span class=\"sd-section-meta text-muted small\">Today · vs yesterday</span>\n </div>\n <div data-container=\"strip\"></div>\n `;\n }\n\n async onInit() {\n this.strip = new KPIStrip({\n containerId: 'strip',\n account: 'incident',\n granularity: 'days',\n sparklineDays: 7,\n tiles: [\n {\n rest: { endpoint: '/api/incident/incident', params: { status: 'new', _mode: 'count' } },\n sparklineSlug: 'incidents', // borrow the trail from the incidents metric series\n key: 'new-incidents',\n label: 'New Incidents',\n severity: 'critical',\n tone: 'bad'\n },\n { slug: 'auth:failures', label: 'Failed Auth', tone: 'bad', severity: 'warn' },\n { slug: 'incidents', label: 'Incidents', tone: 'bad' },\n { slug: 'incident_events', label: 'Events', tone: 'bad' },\n { slug: 'firewall:blocks', label: 'Firewall Blocks', tone: 'bad' },\n { slug: 'bouncer:blocks', label: 'Bouncer Blocks', tone: 'bad' },\n { slug: 'login:new_country', label: 'New-Country Logins', tone: 'bad' },\n {\n rest: { endpoint: '/api/system/geoip', params: { is_blocked: true, _mode: 'count' } },\n key: 'active-blocks',\n label: 'Active Blocks',\n tone: 'bad'\n }\n ]\n });\n this.strip.on?.('tile:click', (data) => this._onTileClick(data));\n this.addChild(this.strip);\n }\n\n refresh() {\n return this.strip?.refresh();\n }\n\n _onTileClick({ slug, key }) {\n // REST-count tiles navigate to the matching filtered list page.\n // Metric tiles open a per-slug history-chart drawer.\n if (key === 'new-incidents') {\n this.getApp()?.showPage?.('system/incidents', { status: 'new' });\n return;\n }\n if (key === 'active-blocks') {\n // GeoLocatedIPTablePage at system/system/geoip is the canonical\n // \"all blocked IPs\" view. system/security/blocked-ips is a more\n // recent BlockedIPs table that focuses on firewall rules; use\n // the geoip page since the count is sourced from /api/system/geoip.\n this.getApp()?.showPage?.('system/system/geoip', { is_blocked: 'true' });\n return;\n }\n if (slug) {\n this._openHistoryDrawer(slug);\n }\n }\n\n _openHistoryDrawer(slug) {\n // Drawer owns the title + eyebrow + meta block. The chart is\n // pure data — suppress its own header (no \"Metrics\" h5, no cog,\n // no type switch) and hide the legend (single series would just\n // say \"Default\" or the raw slug).\n const chart = new MetricsChart({\n slugs: [slug],\n account: 'incident',\n granularity: 'days',\n defaultDateRange: '30d',\n chartType: 'line',\n compactHeader: true,\n showGranularity: false,\n showTypeSwitch: false,\n showDateRange: false,\n showLegend: false,\n height: 280\n });\n Modal.drawer({\n eyebrow: 'Metric History',\n title: this._humanizeSlug(slug),\n meta: [\n { icon: 'bi bi-calendar3', text: 'Last 30 days' },\n { icon: 'bi bi-bar-chart-line', text: 'Daily buckets' }\n ],\n view: chart,\n size: 'lg'\n });\n }\n\n _humanizeSlug(slug) {\n return String(slug)\n .split(/[:_]/)\n .map(p => p.charAt(0).toUpperCase() + p.slice(1))\n .join(' ');\n }\n}\n\nexport default StatusStripPanel;\n","/**\n * StackTraceView - Display formatted and color-coded stack traces\n */\n\nimport View from '@core/View.js';\n\nclass StackTraceView extends View {\n constructor(options = {}) {\n super({\n className: 'stack-trace-view',\n ...options\n });\n\n this.stackTrace = options.stackTrace || '';\n \n 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 `;\n }\n\n async onBeforeRender() {\n this.formattedStackTrace = this.formatStackTrace(this.stackTrace);\n }\n\n formatStackTrace(stackTrace) {\n if (!stackTrace) {\n return '<div class=\"text-muted p-3\">No stack trace available</div>';\n }\n\n // Convert to string if it's an object\n const traceStr = typeof stackTrace === 'string' ? stackTrace : JSON.stringify(stackTrace, null, 2);\n \n const lines = traceStr.split('\\n');\n let html = '';\n\n lines.forEach((line, index) => {\n if (!line.trim()) {\n html += '<div class=\"stack-trace-line\"> </div>';\n return;\n }\n\n // Detect error message (usually the first line)\n if (index === 0 && (line.includes('Error:') || line.includes('Exception:'))) {\n html += `<div class=\"stack-trace-line stack-trace-error\">${this.escapeHtml(line)}</div>`;\n return;\n }\n\n // Detect file paths with line numbers\n // Pattern: at functionName (file.js:123:45) or file.js:123:45\n const filePattern = /(.+?)\\s*\\(([^:]+):(\\d+):(\\d+)\\)/;\n const simpleFilePattern = /^\\s*at\\s+([^:]+):(\\d+):(\\d+)/;\n \n let match = line.match(filePattern);\n if (match) {\n const [, funcName, filePath, lineNum, colNum] = match;\n html += `<div class=\"stack-trace-line stack-trace-file\">\n <span class=\"stack-trace-function\">${this.escapeHtml(funcName.trim())}</span>\n <span class=\"stack-trace-location\"> (${this.escapeHtml(filePath)}:<span class=\"stack-trace-line-number\">${lineNum}</span>:${colNum})</span>\n </div>`;\n return;\n }\n\n match = line.match(simpleFilePattern);\n if (match) {\n const [, filePath, lineNum, colNum] = match;\n html += `<div class=\"stack-trace-line stack-trace-file\">\n <span class=\"stack-trace-location\">at ${this.escapeHtml(filePath)}:<span class=\"stack-trace-line-number\">${lineNum}</span>:${colNum}</span>\n </div>`;\n return;\n }\n\n // Python-style stack trace: File \"...\", line X, in function\n const pythonPattern = /File\\s+\"([^\"]+)\",\\s+line\\s+(\\d+),\\s+in\\s+(.+)/;\n match = line.match(pythonPattern);\n if (match) {\n const [, filePath, lineNum, funcName] = match;\n html += `<div class=\"stack-trace-line stack-trace-file\">\n <span class=\"stack-trace-location\">File \"${this.escapeHtml(filePath)}\", line <span class=\"stack-trace-line-number\">${lineNum}</span>, in </span>\n <span class=\"stack-trace-function\">${this.escapeHtml(funcName)}</span>\n </div>`;\n return;\n }\n\n // Check if line starts with \"at \" (stack frame indicator)\n if (line.trim().startsWith('at ')) {\n html += `<div class=\"stack-trace-line stack-trace-file\">${this.escapeHtml(line)}</div>`;\n return;\n }\n\n // Default: context line\n html += `<div class=\"stack-trace-line stack-trace-context\">${this.escapeHtml(line)}</div>`;\n });\n\n return html;\n }\n\n updateStackTrace(newStackTrace) {\n this.stackTrace = newStackTrace;\n this.render();\n }\n}\n\nexport default StackTraceView;\n","/**\n * HandlerBuilderView - Guided handler string builder for incident rules\n *\n * Replaces the plain-text handler input with a dropdown to select handler\n * type and per-type configuration fields. Generates the handler URI string\n * automatically (e.g., block://?ttl=86400, email://perm@manage_security).\n */\n\nimport View from '@core/View.js';\n\nconst HANDLER_TYPES = [\n {\n value: 'block',\n label: 'Block IP',\n icon: 'bi-slash-circle',\n description: 'Block the source IP address for a specified duration',\n fields: [\n {\n name: 'ttl', type: 'select', label: 'Duration',\n options: [\n { value: '3600', label: '1 hour' },\n { value: '21600', label: '6 hours' },\n { value: '86400', label: '24 hours' },\n { value: '604800', label: '7 days' },\n { value: '2592000', label: '30 days' },\n { value: '0', label: 'Permanent' },\n ],\n default: '86400'\n }\n ],\n build: (opts) => `block://?ttl=${opts.ttl || 86400}`,\n preview: (opts) => {\n const labels = { '3600': '1 hour', '21600': '6 hours', '86400': '24 hours', '604800': '7 days', '2592000': '30 days', '0': 'permanently' };\n return `Block source IP for ${labels[opts.ttl] || opts.ttl + 's'}`;\n }\n },\n {\n value: 'email',\n label: 'Email',\n icon: 'bi-envelope',\n description: 'Send an email notification to users with a permission',\n fields: [\n { name: 'target', type: 'text', label: 'Target (perm@permission or key@name)', default: 'perm@manage_security' }\n ],\n build: (opts) => `email://${opts.target || 'perm@manage_security'}`,\n preview: (opts) => `Email notification to ${opts.target || 'perm@manage_security'}`\n },\n {\n value: 'sms',\n label: 'SMS',\n icon: 'bi-chat-dots',\n description: 'Send an SMS notification to users with a permission',\n fields: [\n { name: 'target', type: 'text', label: 'Target (perm@permission)', default: 'perm@manage_security' }\n ],\n build: (opts) => `sms://${opts.target || 'perm@manage_security'}`,\n preview: (opts) => `SMS notification to ${opts.target || 'perm@manage_security'}`\n },\n {\n value: 'notify',\n label: 'Push Notification',\n icon: 'bi-bell',\n description: 'Send a push notification to users with a permission',\n fields: [\n { name: 'target', type: 'text', label: 'Target (perm@permission)', default: 'perm@manage_security' }\n ],\n build: (opts) => `notify://${opts.target || 'perm@manage_security'}`,\n preview: (opts) => `Push notification to ${opts.target || 'perm@manage_security'}`\n },\n {\n value: 'ticket',\n label: 'Create Ticket',\n icon: 'bi-ticket-detailed',\n description: 'Automatically create a support ticket',\n fields: [\n {\n name: 'priority', type: 'select', label: 'Priority',\n options: [\n { value: '1', label: '1 - Low' },\n { value: '3', label: '3 - Normal' },\n { value: '5', label: '5 - Medium' },\n { value: '8', label: '8 - High' },\n { value: '10', label: '10 - Critical' },\n ],\n default: '5'\n }\n ],\n build: (opts) => `ticket://?priority=${opts.priority || 5}`,\n preview: (opts) => {\n const labels = { '1': 'Low', '3': 'Normal', '5': 'Medium', '8': 'High', '10': 'Critical' };\n return `Create ticket with ${labels[opts.priority] || 'priority ' + opts.priority} priority`;\n }\n },\n {\n value: 'job',\n label: 'Run Job',\n icon: 'bi-gear-wide-connected',\n description: 'Run an async job (Python module path)',\n fields: [\n { name: 'func', type: 'text', label: 'Module Path', placeholder: 'myapp.security.handlers.on_incident' }\n ],\n build: (opts) => `job://${opts.func || ''}`,\n preview: (opts) => `Run job: ${opts.func || '(no function specified)'}`\n },\n {\n value: 'llm',\n label: 'LLM Triage',\n icon: 'bi-stars',\n description: 'Use LLM to analyze and triage the incident',\n fields: [],\n build: () => 'llm://',\n preview: () => 'LLM-powered incident triage and analysis'\n }\n];\n\nexport default class HandlerBuilderView extends View {\n constructor(options = {}) {\n super({\n className: 'handler-builder-view',\n ...options\n });\n\n // Current handler string (from model or passed in)\n this.handlerString = options.value || '';\n this._parseExisting();\n\n this.template = `\n <style>\n .hb-container { border: 1px solid #dee2e6; border-radius: 8px; padding: 1rem; background: #f8f9fa; }\n .hb-type-select { margin-bottom: 0.75rem; }\n .hb-fields { margin-bottom: 0.75rem; }\n .hb-preview { padding: 0.5rem 0.75rem; background: #e9ecef; border-radius: 6px; font-size: 0.85rem; }\n .hb-preview code { font-size: 0.8rem; }\n .hb-preview .hb-desc { color: #495057; margin-bottom: 0.25rem; }\n .hb-preview .hb-raw { color: #6c757d; font-family: ui-monospace, monospace; }\n </style>\n <div class=\"hb-container\">\n <div class=\"hb-type-select\">\n <label class=\"form-label fw-semibold small\">Handler Type</label>\n <select class=\"form-select form-select-sm\" data-action=\"change-type\" id=\"hb-type-select\">\n <option value=\"\">— Select handler type —</option>\n ${HANDLER_TYPES.map(h => `<option value=\"${h.value}\">${h.label}</option>`).join('')}\n </select>\n </div>\n <div id=\"hb-fields\" class=\"hb-fields\"></div>\n <div id=\"hb-preview\" class=\"hb-preview\" style=\"display:none;\"></div>\n </div>\n `;\n }\n\n _parseExisting() {\n if (!this.handlerString) {\n this.selectedType = null;\n this.fieldValues = {};\n return;\n }\n\n // Parse handler URI like \"block://?ttl=86400\" or \"email://perm@manage_security\"\n const match = this.handlerString.match(/^(\\w+):\\/\\/(.*)$/);\n if (!match) {\n this.selectedType = null;\n this.fieldValues = {};\n return;\n }\n\n const [, type, rest] = match;\n this.selectedType = type;\n this.fieldValues = {};\n\n const handlerDef = HANDLER_TYPES.find(h => h.value === type);\n if (!handlerDef) return;\n\n if (rest.startsWith('?')) {\n // Query params: block://?ttl=86400\n const params = new URLSearchParams(rest);\n for (const field of handlerDef.fields) {\n const val = params.get(field.name);\n if (val !== null) this.fieldValues[field.name] = val;\n }\n } else if (handlerDef.fields.length === 1) {\n // Path value: email://perm@manage_security\n this.fieldValues[handlerDef.fields[0].name] = rest;\n }\n }\n\n onAfterRender() {\n // Set the select to current type\n const select = this.element?.querySelector('#hb-type-select');\n if (select && this.selectedType) {\n select.value = this.selectedType;\n this._renderFields();\n this._updatePreview();\n }\n }\n\n onActionChangeType(event) {\n const select = event.target;\n this.selectedType = select.value || null;\n this.fieldValues = {};\n\n // Set defaults\n const handlerDef = HANDLER_TYPES.find(h => h.value === this.selectedType);\n if (handlerDef) {\n for (const field of handlerDef.fields) {\n if (field.default !== undefined) {\n this.fieldValues[field.name] = field.default;\n }\n }\n }\n\n this._renderFields();\n this._updatePreview();\n this._emitChange();\n return true;\n }\n\n _renderFields() {\n const container = this.element?.querySelector('#hb-fields');\n if (!container) return;\n\n const handlerDef = HANDLER_TYPES.find(h => h.value === this.selectedType);\n if (!handlerDef || !handlerDef.fields.length) {\n container.innerHTML = '';\n return;\n }\n\n container.innerHTML = handlerDef.fields.map(field => {\n const value = this.fieldValues[field.name] || field.default || '';\n if (field.type === 'select') {\n const opts = field.options.map(o =>\n `<option value=\"${o.value}\" ${o.value === String(value) ? 'selected' : ''}>${o.label}</option>`\n ).join('');\n return `\n <div class=\"mb-2\">\n <label class=\"form-label small\">${field.label}</label>\n <select class=\"form-select form-select-sm\" data-field=\"${field.name}\" data-action=\"field-change\">${opts}</select>\n </div>`;\n }\n return `\n <div class=\"mb-2\">\n <label class=\"form-label small\">${field.label}</label>\n <input type=\"text\" class=\"form-control form-control-sm\" data-field=\"${field.name}\" data-action=\"field-change\"\n value=\"${value}\" placeholder=\"${field.placeholder || ''}\">\n </div>`;\n }).join('');\n }\n\n onActionFieldChange(event) {\n const el = event.target;\n const fieldName = el.dataset.field;\n if (fieldName) {\n this.fieldValues[fieldName] = el.value;\n this._updatePreview();\n this._emitChange();\n }\n return true;\n }\n\n _updatePreview() {\n const container = this.element?.querySelector('#hb-preview');\n if (!container) return;\n\n const handlerDef = HANDLER_TYPES.find(h => h.value === this.selectedType);\n if (!handlerDef) {\n container.style.display = 'none';\n return;\n }\n\n const handlerString = handlerDef.build(this.fieldValues);\n const description = handlerDef.preview(this.fieldValues);\n\n container.style.display = 'block';\n container.innerHTML = `\n <div class=\"hb-desc\"><i class=\"bi ${handlerDef.icon} me-1\"></i>${description}</div>\n <div class=\"hb-raw\"><code>${handlerString}</code></div>\n `;\n }\n\n _emitChange() {\n const value = this.getValue();\n this.emit('change', value);\n }\n\n /**\n * Get the generated handler string\n */\n getValue() {\n const handlerDef = HANDLER_TYPES.find(h => h.value === this.selectedType);\n if (!handlerDef) return '';\n return handlerDef.build(this.fieldValues);\n }\n\n /**\n * Set handler string and parse it\n */\n setValue(handlerString) {\n this.handlerString = handlerString || '';\n this._parseExisting();\n if (this.isMounted()) {\n const select = this.element?.querySelector('#hb-type-select');\n if (select) select.value = this.selectedType || '';\n this._renderFields();\n this._updatePreview();\n }\n }\n}\n","import View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { RuleSet, RuleList, BundleByOptions, MatchByOptions } from '@ext/admin/models/Incident.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport HandlerBuilderView from '../security/HandlerBuilderView.js';\n\nclass RuleSetView extends View {\n constructor(options = {}) {\n super({\n className: 'ruleset-view',\n ...options\n });\n\n this.model = options.model || new RuleSet(options.data || {});\n\n this.template = `\n <div class=\"ruleset-view-container\">\n <!-- Header -->\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\">Scope: {{model.category}} | Priority: {{model.priority}}</div>\n </div>\n </div>\n <div data-container=\"ruleset-context-menu\"></div>\n </div>\n\n <!-- Tabs -->\n <div data-container=\"ruleset-tabs\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // Get labels for current values\n const matchByValue = this.model.get('match_by');\n const matchByOption = MatchByOptions.find(opt => opt.value === matchByValue);\n const matchByLabel = matchByOption ? matchByOption.label : String(matchByValue);\n\n const bundleByValue = this.model.get('bundle_by');\n const bundleByOption = BundleByOptions.find(opt => opt.value === bundleByValue);\n const bundleByLabel = bundleByOption ? bundleByOption.label : String(bundleByValue);\n\n // Config Tab\n const triggerCount = this.model.get('trigger_count');\n const triggerWindow = this.model.get('trigger_window');\n const retriggerEvery = this.model.get('retrigger_every');\n\n this.configView = new DataView({\n model: this.model,\n className: \"p-3\",\n columns: 2,\n showEmptyValues: true,\n emptyValueText: '—',\n fields: [\n { name: 'name', label: 'Name', cols: 6 },\n { name: 'id', label: 'RuleSet ID', cols: 3 },\n { name: 'is_active', label: 'Active', formatter: 'yesnoicon', cols: 3 },\n { name: 'category', label: 'Event Category', formatter: 'badge', cols: 4 },\n { name: 'priority', label: 'Evaluation Priority', cols: 4 },\n {\n name: 'match_by',\n label: 'Match Logic',\n template: matchByLabel,\n cols: 4\n },\n {\n name: 'bundle_by',\n label: 'Bundle By',\n template: bundleByLabel,\n cols: 4\n },\n { name: 'bundle_minutes', label: 'Bundle Window (min)', cols: 4 },\n { name: 'bundle_by_rule_set', label: 'Bundle by RuleSet', formatter: 'yesnoicon', cols: 4 },\n {\n name: 'trigger_count',\n label: 'Trigger Count',\n template: triggerCount != null ? String(triggerCount) + ' events' : 'Immediate (first event)',\n cols: 4\n },\n {\n name: 'trigger_window',\n label: 'Trigger Window',\n template: triggerWindow != null ? triggerWindow + ' minutes' : 'All events counted',\n cols: 4\n },\n {\n name: 'retrigger_every',\n label: 'Re-trigger Every',\n template: retriggerEvery != null ? retriggerEvery + ' events' : 'Fire once only',\n cols: 4\n },\n { name: 'handler', label: 'Handler Chain', cols: 12 },\n ]\n });\n\n // Rules Tab\n const rulesCollection = new RuleList({\n params: { parent: this.model.get('id') }\n });\n this.rulesView = new TableView({\n collection: rulesCollection,\n hideActivePillNames: ['parent'],\n columns: [\n { key: 'id', label: 'ID', width: '70px' },\n { key: 'name', label: 'Name' },\n { key: 'field_name', label: 'Field' },\n { key: 'comparator', label: 'Comparator', width: '120px' },\n { key: 'value', label: 'Value' },\n { key: 'value_type', label: 'Type', width: '100px' },\n ],\n showAdd: true,\n clickAction: 'edit',\n actions: ['edit', 'delete'],\n contextMenu: [\n { label: 'Edit Rule', action: 'edit', icon: 'bi-pencil' },\n { label: 'Duplicate Rule', action: 'duplicate', icon: 'bi-files' },\n { divider: true },\n { label: 'Delete Rule', action: 'delete', icon: 'bi-trash', danger: true }\n ],\n // Pass the parent ID so new rules get associated with this ruleset\n addFormDefaults: {\n parent: this.model.get('id')\n }\n });\n\n this.tabView = new TabView({\n containerId: 'ruleset-tabs',\n tabs: {\n 'Configuration': this.configView,\n 'Rules': this.rulesView\n },\n activeTab: 'Configuration'\n });\n this.addChild(this.tabView);\n\n const contextMenu = new ContextMenu({\n containerId: 'ruleset-context-menu',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit RuleSet', action: 'edit-ruleset', icon: 'bi-pencil' },\n { label: 'Edit Handler', action: 'edit-handler', icon: 'bi-tools' },\n { label: 'Disable', action: 'disable-ruleset', icon: 'bi-toggle-off' },\n { type: 'divider' },\n { label: 'Delete RuleSet', action: 'delete-ruleset', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(contextMenu);\n }\n\n /**\n * Action handler: Edit Handler via guided builder\n */\n async onActionEditHandler() {\n const builder = new HandlerBuilderView({\n value: this.model.get('handler') || ''\n });\n\n const result = await Modal.dialog({\n title: 'Configure Handler',\n body: builder,\n size: 'md',\n scrollable: true,\n buttons: [\n { text: 'Cancel', class: 'btn-secondary', dismiss: true },\n { text: 'Save', class: 'btn-primary', action: 'save' }\n ]\n });\n\n if (result === 'save') {\n const handlerString = builder.getValue();\n if (handlerString) {\n const resp = await this.model.save({ handler: handlerString });\n if (resp.status === 200) {\n this.getApp()?.toast?.success('Handler updated');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to update handler');\n }\n }\n }\n }\n\n /**\n * Action handler: Edit RuleSet\n */\n async onActionEditRuleset() {\n const resp = await Modal.modelForm({\n title: `Edit RuleSet - ${this.model.get('name')}`,\n model: this.model,\n formConfig: RuleSet.EDIT_FORM,\n });\n if (resp) {\n await this.render();\n }\n }\n\n /**\n * Action handler: Disable/Enable RuleSet\n */\n async onActionDisableRuleset() {\n const isActive = this.model.get('is_active');\n const newStatus = !isActive;\n\n try {\n this.model.set('is_active', newStatus);\n await this.model.save();\n await this.render();\n this.getApp()?.toast?.success(`RuleSet ${newStatus ? 'enabled' : 'disabled'} successfully`);\n } catch (error) {\n this.getApp()?.toast?.error(`Failed to update RuleSet: ${error.message}`);\n }\n }\n\n /**\n * Action handler: Delete RuleSet\n */\n async onActionDeleteRuleset() {\n const confirmed = await Modal.confirm({\n title: 'Delete RuleSet',\n message: `Are you sure you want to delete the ruleset \"${this.model.get('name')}\"? This action cannot be undone.`,\n confirmText: 'Delete',\n confirmClass: 'btn-danger'\n });\n\n if (confirmed) {\n try {\n await this.model.destroy();\n this.getApp()?.toast?.success('RuleSet deleted successfully');\n\n // Close the dialog\n const dialog = this.element?.closest('.modal');\n if (dialog) {\n const bsModal = window.bootstrap?.Modal?.getInstance(dialog);\n if (bsModal) {\n bsModal.hide();\n }\n }\n\n // Emit event to parent to refresh the table\n this.emit('ruleset:deleted', { model: this.model });\n } catch (error) {\n this.getApp()?.toast?.error(`Failed to delete RuleSet: ${error.message}`);\n }\n }\n }\n}\n\nRuleSetView.VIEW_CLASS = RuleSetView;\n\nexport default RuleSetView;\n","import { IncidentHistory, IncidentHistoryList } from '@ext/admin/models/Incident.js';\nimport rest from '@core/Rest.js';\n\nclass IncidentHistoryAdapter {\n constructor(incidentId) {\n this.incidentId = incidentId;\n this.collection = new IncidentHistoryList({ params: { parent: this.incidentId, sort: 'created', size: 100 } });\n }\n\n async fetch() {\n await this.collection.fetch();\n const messages = this.collection.models.map(item => this.transform(item));\n // Render markdown for system_event messages (e.g., LLM analysis notes)\n await Promise.all(messages.map(async (msg) => {\n if (msg.type === 'system_event' && msg.content) {\n msg.content = await this._renderMarkdown(msg.content);\n }\n }));\n return messages;\n }\n\n transform(item) {\n return {\n id: item.get('id'),\n type: item.get('kind') === 'comment' ? 'user_comment' : 'system_event',\n author: {\n name: item.get('user.display_name') || 'System',\n avatarUrl: item.get('user.avatar.url')\n },\n timestamp: item.get('created'),\n content: item.get('note'),\n attachments: item.get('media') ? [item.get('media')] : []\n };\n }\n\n async addNote(data) {\n const history = new IncidentHistory();\n const resp = await history.save({\n parent: this.incidentId,\n note: data.text,\n kind: 'comment',\n media: data.files && data.files.length > 0 ? data.files[0].id : null\n });\n if (resp.success) {\n await this.collection.fetch();\n }\n return resp;\n }\n\n async _renderMarkdown(markdown) {\n if (!markdown) return '';\n try {\n const resp = await rest.post('/api/docit/render', { markdown });\n const html = resp?.data?.data?.html || resp?.data?.html;\n if (html) return html;\n } catch (_e) { /* API unavailable */ }\n // Fallback: escape and preserve whitespace\n const div = document.createElement('div');\n div.textContent = markdown;\n return `<pre style=\"white-space: pre-wrap;\">${div.innerHTML}</pre>`;\n }\n}\n\nexport default IncidentHistoryAdapter;\n","import ChatMessageView from '@core/views/chat/ChatMessageView.js';\nimport Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/**\n * AssistantMessageView - Extended message view for assistant responses\n *\n * Renders structured data blocks inline within chat messages:\n * - table blocks → TableView (inline collection, no pagination)\n * - chart blocks → SeriesChart or PieChart via setData()\n * - stat blocks → Bootstrap card row\n */\nclass AssistantMessageView extends ChatMessageView {\n constructor(options = {}) {\n super(options);\n this._blockViews = [];\n this._needsMarkdown = this.message.role === 'assistant' && !!this.message.content;\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Render markdown → HTML for assistant messages\n if (this._needsMarkdown) {\n this._needsMarkdown = false;\n await this._renderMarkdown();\n }\n\n // Collapsible long messages — check after markdown renders\n this._setupCollapsibleMessage();\n\n if (!this.message.blocks || this.message.blocks.length === 0) return;\n\n const blocksContainer = this.element.querySelector(\n `[data-container=\"blocks-${this.message.id || this.id}\"]`\n );\n if (!blocksContainer) return;\n\n for (let i = 0; i < this.message.blocks.length; i++) {\n const block = this.message.blocks[i];\n const wrapper = document.createElement('div');\n wrapper.className = 'assistant-block mb-3';\n blocksContainer.appendChild(wrapper);\n\n try {\n if (block.type === 'table') {\n await this._renderTableBlock(block, wrapper);\n } else if (block.type === 'chart') {\n await this._renderChartBlock(block, wrapper);\n } else if (block.type === 'stat') {\n this._renderStatBlock(block, wrapper);\n } else if (block.type === 'action') {\n this._renderActionBlock(block, wrapper);\n } else if (block.type === 'list') {\n this._renderListBlock(block, wrapper);\n } else if (block.type === 'alert') {\n this._renderAlertBlock(block, wrapper);\n } else if (block.type === 'progress') {\n this._renderProgressBlock(block, wrapper);\n } else if (block.type === 'file') {\n this._renderFileBlock(block, wrapper);\n }\n } catch (err) {\n console.error('Failed to render block:', block.type, err);\n const errEl = document.createElement('div');\n errEl.className = 'alert alert-warning small';\n errEl.textContent = `Failed to render ${block.type} block`;\n wrapper.appendChild(errEl);\n }\n }\n }\n\n /**\n * Create a collapsible card wrapper for a block.\n * Returns { body, onShow } — append content into body.\n * onShow(callback) registers a one-time callback for when the collapse first opens,\n * useful for rendering charts that need visible dimensions.\n * @private\n */\n _createCollapsibleCard(container, { icon, title, subtitle }) {\n const collapseId = `block-${this.message.id || this.id}-${++AssistantMessageView._blockCounter}`;\n const esc = this._escapeHtml.bind(this);\n\n const card = document.createElement('div');\n card.className = 'assistant-collapsible-block';\n card.innerHTML = `\n <a class=\"assistant-block-toggle collapsed\" data-bs-toggle=\"collapse\"\n href=\"#${collapseId}\" role=\"button\" aria-expanded=\"false\">\n <span class=\"assistant-block-toggle-icon\">\n <i class=\"bi ${icon}\"></i>\n </span>\n <span class=\"assistant-block-toggle-text\">\n <span class=\"assistant-block-toggle-title\">${esc(title || 'Data')}</span>\n ${subtitle ? `<span class=\"assistant-block-toggle-subtitle\">${esc(subtitle)}</span>` : ''}\n </span>\n <i class=\"bi bi-chevron-down assistant-block-chevron\"></i>\n </a>\n <div class=\"collapse\" id=\"${collapseId}\">\n <div class=\"assistant-block-body\"></div>\n </div>\n `;\n container.appendChild(card);\n\n const collapseEl = card.querySelector('.collapse');\n let showCallbacks = [];\n\n // Fire callbacks once when collapse is fully shown (elements are visible + sized)\n collapseEl.addEventListener('shown.bs.collapse', () => {\n showCallbacks.forEach(cb => cb());\n showCallbacks = [];\n }, { once: true });\n\n return {\n body: card.querySelector('.assistant-block-body'),\n onShow: (cb) => showCallbacks.push(cb)\n };\n }\n\n /**\n * Render a table block using TableView inside a collapsible card\n * @private\n */\n async _renderTableBlock(block, container) {\n const { default: TableView } = await import('@core/views/table/TableView.js');\n\n const rowCount = (block.rows || []).length;\n const colCount = (block.columns || []).length;\n const { body } = this._createCollapsibleCard(container, {\n icon: 'bi-table',\n title: block.title || 'Table',\n subtitle: `${rowCount} rows · ${colCount} columns`\n });\n\n const columns = (block.columns || []).map(col => {\n if (typeof col === 'string') return { key: col, label: col };\n return col;\n });\n const columnKeys = columns.map(c => c.key);\n\n const models = (block.rows || []).map((row, idx) => {\n const data = { id: idx };\n columnKeys.forEach((key, ci) => {\n data[key] = row[ci] !== undefined ? row[ci] : '';\n });\n return new Model(data);\n });\n\n const collection = new Collection({ preloaded: true });\n collection.add(models);\n\n const tableView = new TableView({\n collection,\n columns,\n paginated: false,\n sortable: false,\n searchable: false,\n filterable: false,\n showRefresh: false,\n showAdd: false\n });\n\n this._blockViews.push(tableView);\n this.addChild(tableView);\n body.appendChild(tableView.element);\n tableView.render(false);\n }\n\n /**\n * Render a chart block using PieChart or SeriesChart inside a collapsible\n * card. Forwards the optional block-level options (snake_case → camelCase\n * via a strict allowlist) to the chart constructor; the django-mojo server\n * validator guarantees these fields are well-formed by the time we see them.\n * @private\n */\n async _renderChartBlock(block, container) {\n const chartType = block.chart_type || 'line';\n const chartIcons = { line: 'bi-graph-up', bar: 'bi-bar-chart-fill', pie: 'bi-pie-chart-fill', area: 'bi-graph-up' };\n const chartLabels = { line: 'Line Chart', bar: 'Bar Chart', pie: 'Pie Chart', area: 'Area Chart' };\n\n const seriesCount = (block.series || []).length;\n const pointCount = block.labels?.length || 0;\n const isPie = chartType === 'pie';\n\n const { body, onShow } = this._createCollapsibleCard(container, {\n icon: chartIcons[chartType] || 'bi-graph-up',\n title: block.title || chartLabels[chartType] || 'Chart',\n subtitle: isPie\n ? `${pointCount} segments`\n : `${seriesCount} series · ${pointCount} points`\n });\n\n const chartContainer = document.createElement('div');\n chartContainer.className = 'assistant-chart-body';\n body.appendChild(chartContainer);\n\n // Strict allowlist for snake_case → camelCase forwarding from block fields.\n // The django-mojo server validates these before they reach us; new\n // server fields will need a corresponding entry here.\n const FORWARD_SERIES = {\n stacked: 'stacked',\n grouped: 'grouped',\n crosshair_tracking: 'crosshairTracking',\n colors: 'colors',\n show_legend: 'showLegend',\n legend_position: 'legendPosition'\n };\n const FORWARD_PIE = {\n cutout: 'cutout',\n show_labels: 'showLabels',\n show_percentages: 'showPercentages',\n colors: 'colors',\n legend_position: 'legendPosition'\n };\n const forward = (map) => Object.entries(map).reduce((acc, [snake, camel]) => {\n if (block[snake] !== undefined) acc[camel] = block[snake];\n return acc;\n }, {});\n\n const chartData = {\n labels: block.labels || [],\n datasets: (block.series || []).map(s => {\n const d = { label: s.name, data: s.values };\n if (s.color !== undefined) d.color = s.color;\n if (s.fill !== undefined) d.fill = s.fill;\n if (s.smoothing !== undefined) d.smoothing = s.smoothing;\n return d;\n })\n };\n\n if (isPie) {\n // PieChart is native SVG — no canvas sizing issues, renders fine\n // even in hidden containers.\n const { default: PieChart } = await import('../../charts/PieChart.js');\n const chartView = new PieChart({\n width: 180,\n height: 180,\n legendPosition: 'right',\n ...forward(FORWARD_PIE),\n data: chartData\n });\n\n this._blockViews.push(chartView);\n this.addChild(chartView);\n chartContainer.appendChild(chartView.element);\n chartView.render(false);\n } else {\n // SeriesChart is native SVG — no canvas sizing issues.\n const { default: SeriesChart } = await import('../../charts/SeriesChart.js');\n const chartView = new SeriesChart({\n chartType: chartType === 'area' ? 'line' : chartType,\n fill: chartType === 'area',\n height: 200,\n legendPosition: 'top',\n ...forward(FORWARD_SERIES),\n data: chartData\n });\n\n this._blockViews.push(chartView);\n this.addChild(chartView);\n chartContainer.appendChild(chartView.element);\n chartView.render(false);\n }\n }\n\n /**\n * Render a stat block as Bootstrap cards\n * @private\n */\n _renderStatBlock(block, container) {\n const items = block.items || [];\n const row = document.createElement('div');\n row.className = 'd-flex flex-wrap gap-2';\n\n items.forEach(item => {\n const card = document.createElement('div');\n card.className = 'assistant-stat-card card';\n card.innerHTML = `\n <div class=\"card-body text-center py-2 px-3\">\n <div class=\"text-muted small\">${this._escapeHtml(item.label)}</div>\n <div class=\"fw-bold fs-5\">${this._escapeHtml(String(item.value))}</div>\n </div>\n `;\n row.appendChild(card);\n });\n\n container.appendChild(row);\n }\n\n /**\n * Render an action confirmation card with buttons.\n * @private\n */\n _renderActionBlock(block, container) {\n const esc = this._escapeHtml.bind(this);\n const card = document.createElement('div');\n card.className = 'assistant-action-card';\n\n card.innerHTML = `\n <div class=\"assistant-action-header\">${esc(block.title || 'Action Required')}</div>\n ${block.description ? `<div class=\"assistant-action-desc\">${esc(block.description)}</div>` : ''}\n <div class=\"assistant-action-buttons\"></div>\n `;\n\n const btnRow = card.querySelector('.assistant-action-buttons');\n const actions = block.actions || [];\n\n actions.forEach((action, index) => {\n const btn = document.createElement('button');\n btn.className = index === 0 ? 'btn btn-sm btn-primary' : 'btn btn-sm btn-outline-secondary';\n btn.textContent = action.label;\n btn.addEventListener('click', () => {\n // Replace buttons with confirmation text\n btnRow.innerHTML = `\n <div class=\"assistant-action-chosen-label\">\n <i class=\"bi bi-check-circle-fill me-1\"></i>\n You chose: <strong>${esc(action.label)}</strong>\n </div>\n `;\n\n // Send choice via WS\n const app = this.getApp();\n if (app?.ws?.isConnected) {\n app.ws.send({\n type: 'assistant_action',\n conversation_id: this.message._conversationId,\n action_id: block.action_id,\n value: action.value\n });\n }\n });\n btnRow.appendChild(btn);\n });\n\n container.appendChild(card);\n }\n\n /**\n * Render a key/value detail list card.\n * @private\n */\n _renderListBlock(block, container) {\n const esc = this._escapeHtml.bind(this);\n const card = document.createElement('div');\n card.className = 'assistant-list-card';\n\n let html = '';\n if (block.title) {\n html += `<div class=\"assistant-list-title\">${esc(block.title)}</div>`;\n }\n html += '<dl class=\"assistant-list-items\">';\n (block.items || []).forEach(item => {\n html += `\n <div class=\"assistant-list-row\">\n <dt>${esc(item.label)}</dt>\n <dd>${esc(String(item.value ?? ''))}</dd>\n </div>`;\n });\n html += '</dl>';\n card.innerHTML = html;\n container.appendChild(card);\n }\n\n /**\n * Render a severity-colored alert banner.\n * @private\n */\n _renderAlertBlock(block, container) {\n const esc = this._escapeHtml.bind(this);\n const level = block.level || 'info';\n const icons = { info: 'bi-info-circle-fill', success: 'bi-check-circle-fill', warning: 'bi-exclamation-triangle-fill', error: 'bi-x-circle-fill' };\n const bsClass = { info: 'alert-info', success: 'alert-success', warning: 'alert-warning', error: 'alert-danger' };\n\n const alert = document.createElement('div');\n alert.className = `assistant-alert alert ${bsClass[level] || 'alert-info'}`;\n alert.innerHTML = `\n <i class=\"bi ${icons[level] || icons.info} me-2\"></i>\n <div class=\"assistant-alert-content\">\n ${block.title ? `<strong>${esc(block.title)}</strong>` : ''}\n <div>${esc(block.message || '')}</div>\n </div>\n `;\n container.appendChild(alert);\n }\n\n /**\n * Render a multi-step progress tracker.\n * @private\n */\n _renderProgressBlock(block, container) {\n const esc = this._escapeHtml.bind(this);\n const steps = block.steps || [];\n const doneCount = steps.filter(s => s.status === 'done').length;\n const pct = steps.length > 0 ? Math.round((doneCount / steps.length) * 100) : 0;\n\n const card = document.createElement('div');\n card.className = 'assistant-progress-card';\n if (block.plan_id) card.dataset.planId = block.plan_id;\n\n let stepsHtml = '';\n const statusIcons = { pending: 'bi-circle', in_progress: 'bi-arrow-repeat', done: 'bi-check-circle-fill', skipped: 'bi-slash-circle' };\n\n steps.forEach(step => {\n stepsHtml += `\n <div class=\"assistant-progress-step step-${step.status}\" data-step-id=\"${step.id}\">\n <i class=\"bi ${statusIcons[step.status] || statusIcons.pending} step-icon\"></i>\n <div class=\"step-content\">\n <span class=\"step-description\">${esc(step.description)}</span>\n ${step.summary ? `<span class=\"step-summary\">${esc(step.summary)}</span>` : ''}\n </div>\n </div>`;\n });\n\n card.innerHTML = `\n <div class=\"assistant-progress-header\">\n <span class=\"assistant-progress-title\">${esc(block.title || 'Plan')}</span>\n <span class=\"assistant-progress-counter\">${doneCount} of ${steps.length}</span>\n </div>\n <div class=\"progress\" style=\"height: 4px; margin-bottom: 10px;\">\n <div class=\"progress-bar\" role=\"progressbar\" style=\"width: ${pct}%\"></div>\n </div>\n <div class=\"assistant-progress-steps\">${stepsHtml}</div>\n `;\n container.appendChild(card);\n }\n\n /**\n * Render a downloadable file attachment card.\n * @private\n */\n _renderFileBlock(block, container) {\n if (!block.filename || !block.url) {\n console.warn('File block missing required fields (filename, url). type:', block.type);\n return;\n }\n\n // URL scheme validation — prevent javascript:, data:, and protocol-relative XSS\n let url;\n try {\n const parsed = new URL(block.url, window.location.href);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n console.warn('File block URL rejected (invalid scheme).');\n return;\n }\n url = block.url;\n } catch {\n console.warn('File block URL rejected (unparseable).');\n return;\n }\n\n const esc = this._escapeHtml.bind(this);\n const formatIcons = {\n csv: 'bi-filetype-csv',\n xlsx: 'bi-file-earmark-spreadsheet',\n pdf: 'bi-filetype-pdf',\n json: 'bi-filetype-json'\n };\n const icon = formatIcons[block.format] || 'bi-file-earmark-arrow-down';\n\n const card = document.createElement('a');\n card.className = 'assistant-file-card';\n card.href = url;\n card.download = block.filename;\n card.target = '_blank';\n card.rel = 'noopener';\n\n // Split metadata: file stats (size, rows) vs expiry\n const stats = [];\n if (block.size != null) stats.push(this._formatBytes(block.size));\n if (block.row_count != null) stats.push(`${Number(block.row_count).toLocaleString()} rows`);\n\n card.innerHTML = `\n <span class=\"assistant-file-icon\">\n <i class=\"bi ${icon}\"></i>\n </span>\n <div class=\"assistant-file-info\">\n <span class=\"assistant-file-name\">${esc(block.filename)}</span>\n ${stats.length ? `<span class=\"assistant-file-stats\">${stats.join(' · ')}</span>` : ''}\n ${block.expires_in ? `<span class=\"assistant-file-expiry\"><i class=\"bi bi-clock me-1\"></i>${esc(block.expires_in)}</span>` : ''}\n </div>\n <span class=\"assistant-file-download\" title=\"Download\">\n <i class=\"bi bi-download\"></i>\n </span>\n `;\n\n container.appendChild(card);\n }\n\n /**\n * Format byte count to human-readable string.\n * @private\n */\n _formatBytes(bytes) {\n if (bytes < 1024) return bytes + ' B';\n if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';\n return (bytes / 1048576).toFixed(1) + ' MB';\n }\n\n /**\n * Update a single step inside an existing progress block (called from WS events).\n * @param {string} planId\n * @param {number} stepId\n * @param {string} status\n * @param {string|null} summary\n */\n updateProgressStep(planId, stepId, status, summary) {\n const card = this.element?.querySelector(`[data-plan-id=\"${planId}\"]`);\n if (!card) return;\n\n const statusIcons = { pending: 'bi-circle', in_progress: 'bi-arrow-repeat', done: 'bi-check-circle-fill', skipped: 'bi-slash-circle' };\n const row = card.querySelector(`[data-step-id=\"${stepId}\"]`);\n if (row) {\n row.className = `assistant-progress-step step-${status}`;\n const icon = row.querySelector('.step-icon');\n if (icon) icon.className = `bi ${statusIcons[status] || statusIcons.pending} step-icon`;\n\n let summaryEl = row.querySelector('.step-summary');\n if (summary) {\n if (!summaryEl) {\n summaryEl = document.createElement('span');\n summaryEl.className = 'step-summary';\n row.querySelector('.step-content').appendChild(summaryEl);\n }\n summaryEl.textContent = summary;\n }\n }\n\n // Update counter and progress bar\n const allSteps = card.querySelectorAll('.assistant-progress-step');\n const doneCount = card.querySelectorAll('.step-done').length;\n const counter = card.querySelector('.assistant-progress-counter');\n if (counter) counter.textContent = `${doneCount} of ${allSteps.length}`;\n const bar = card.querySelector('.progress-bar');\n if (bar) bar.style.width = `${allSteps.length > 0 ? Math.round((doneCount / allSteps.length) * 100) : 0}%`;\n }\n\n /**\n * Add expand/collapse toggle for long assistant messages.\n * @private\n */\n _setupCollapsibleMessage() {\n if (this.message.role !== 'assistant') return;\n\n const textEl = this.element?.querySelector('.message-text');\n if (!textEl || !textEl.textContent.trim()) return;\n\n // Use requestAnimationFrame to measure after layout\n requestAnimationFrame(() => {\n const MAX_HEIGHT = 300;\n if (textEl.scrollHeight <= MAX_HEIGHT) return;\n\n textEl.classList.add('message-collapsed');\n textEl.style.setProperty('--collapse-height', MAX_HEIGHT + 'px');\n\n const toggle = document.createElement('button');\n toggle.className = 'message-expand-toggle';\n toggle.innerHTML = '<i class=\"bi bi-chevron-down me-1\"></i>Show more';\n toggle.addEventListener('click', () => {\n const collapsed = textEl.classList.toggle('message-collapsed');\n toggle.innerHTML = collapsed\n ? '<i class=\"bi bi-chevron-down me-1\"></i>Show more'\n : '<i class=\"bi bi-chevron-up me-1\"></i>Show less';\n });\n\n textEl.parentNode.insertBefore(toggle, textEl.nextSibling);\n });\n }\n\n /**\n * Render markdown content via the docit API, with frontend fallback.\n * @private\n */\n async _renderMarkdown() {\n const textEl = this.element?.querySelector('.message-text');\n if (!textEl) return;\n\n const raw = this.message.content;\n try {\n const app = this.getApp();\n const resp = await app.rest.post('/api/docit/render', { markdown: raw });\n const html = resp?.data?.data?.html || resp?.data?.html;\n if (html) {\n textEl.innerHTML = html;\n return;\n }\n } catch (_e) {\n // API unavailable — use frontend fallback\n }\n textEl.innerHTML = AssistantMessageView.markdownToHtml(raw);\n }\n\n /**\n * Escape HTML to prevent XSS in stat blocks\n * @private\n */\n _escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n /**\n * Lightweight markdown → HTML for LLM assistant responses.\n * Handles the common patterns: headings, bold, italic, code,\n * horizontal rules, bullet lists, and paragraphs.\n * @static\n */\n static markdownToHtml(text) {\n // Escape HTML first to prevent XSS\n const div = document.createElement('div');\n div.textContent = text;\n let html = div.innerHTML;\n\n // Code blocks (``` ... ```)\n html = html.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,\n (_m, _lang, code) => `<pre class=\"assistant-code-block\"><code>${code.trim()}</code></pre>`);\n\n // Inline code\n html = html.replace(/`([^`]+)`/g, '<code class=\"assistant-inline-code\">$1</code>');\n\n // Headings (###, ##, #)\n html = html.replace(/^### (.+)$/gm, '<h6 class=\"assistant-heading mt-3 mb-1\">$1</h6>');\n html = html.replace(/^## (.+)$/gm, '<h5 class=\"assistant-heading mt-3 mb-1\">$1</h5>');\n html = html.replace(/^# (.+)$/gm, '<h4 class=\"assistant-heading mt-3 mb-2\">$1</h4>');\n\n // Horizontal rules\n html = html.replace(/^---+$/gm, '<hr class=\"my-2 opacity-25\">');\n\n // Bold + italic\n html = html.replace(/\\*\\*\\*(.+?)\\*\\*\\*/g, '<strong><em>$1</em></strong>');\n html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');\n\n // Bullet lists — collect consecutive lines starting with \"- \"\n html = html.replace(/((?:^- .+$\\n?)+)/gm, (match) => {\n const items = match.trim().split('\\n').map(line => {\n const content = line.replace(/^- /, '');\n return `<li>${content}</li>`;\n }).join('');\n return `<ul class=\"assistant-list mb-2\">${items}</ul>`;\n });\n\n // Paragraphs — convert double newlines to paragraph breaks\n html = html.replace(/\\n{2,}/g, '</p><p>');\n // Remaining single newlines to <br>\n html = html.replace(/\\n/g, '<br>');\n // Wrap in paragraph\n html = `<p>${html}</p>`;\n // Clean up empty paragraphs\n html = html.replace(/<p>\\s*<\\/p>/g, '');\n // Don't wrap block elements in <p>\n html = html.replace(/<p>\\s*(<h[456]|<hr|<ul|<pre|<\\/ul>|<\\/pre>)/g, '$1');\n html = html.replace(/(<\\/h[456]>|<hr[^>]*>|<\\/ul>|<\\/pre>)\\s*<\\/p>/g, '$1');\n\n return html;\n }\n}\n\nAssistantMessageView._blockCounter = 0;\n\nexport default AssistantMessageView;\n","import View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\n/**\n * AssistantConversationListView - Left panel showing past conversations\n *\n * Displays conversations grouped by date (Today / Yesterday / Earlier).\n * Includes search input (debounced) and \"Load more\" pagination.\n * Emits events for conversation selection, creation, and deletion.\n */\nclass AssistantConversationListView extends View {\n constructor(options = {}) {\n super({\n className: 'assistant-conversation-list',\n ...options\n });\n\n this.collection = options.collection;\n this.activeId = null;\n this._searchTimeout = null;\n }\n\n getTemplate() {\n return `\n <div class=\"conversation-list-header\">\n <div class=\"conversation-search-wrapper mb-2\">\n <input type=\"text\" class=\"form-control form-control-sm conversation-search-input\"\n placeholder=\"Search conversations...\" data-ref=\"search-input\">\n </div>\n <button class=\"btn btn-outline-secondary w-100\" data-action=\"new-conversation\">\n <i class=\"bi bi-plus-lg me-1\"></i> New conversation\n </button>\n </div>\n <div class=\"conversation-list-items\" data-container=\"items\"></div>\n `;\n }\n\n async onInit() {\n await this.collection.fetch();\n }\n\n async onAfterRender() {\n this._renderItems();\n\n // Wire search input\n const searchInput = this.element.querySelector('[data-ref=\"search-input\"]');\n if (searchInput) {\n searchInput.addEventListener('input', () => this._onSearchInput(searchInput));\n }\n }\n\n /**\n * Handle search input with debounce\n * @private\n */\n _onSearchInput(input) {\n if (this._searchTimeout) clearTimeout(this._searchTimeout);\n this._searchTimeout = setTimeout(async () => {\n const query = input.value.trim();\n if (query) {\n this.collection.params.search = query;\n } else {\n delete this.collection.params.search;\n }\n this.collection.params.start = 0;\n await this.collection.fetch();\n this._renderItems();\n }, 300);\n }\n\n /**\n * Render conversation items grouped by date\n * @private\n */\n _renderItems() {\n const container = this.element.querySelector('[data-container=\"items\"]');\n if (!container) return;\n\n container.innerHTML = '';\n\n const models = this.collection.models || [];\n if (models.length === 0) {\n container.innerHTML = `\n <div class=\"text-center text-muted small p-4\">\n No conversations yet.<br>Start by typing a message.\n </div>\n `;\n return;\n }\n\n const groups = this._groupByDate(models);\n\n for (const [label, items] of groups) {\n // Date group header\n const header = document.createElement('div');\n header.className = 'conversation-date-header px-3 py-1 text-muted small fw-semibold text-uppercase';\n header.textContent = label;\n container.appendChild(header);\n\n // Conversation items\n items.forEach(model => {\n const id = model.get('id');\n const title = model.get('title') || model.get('summary') || 'New conversation';\n const modified = model.get('modified') || model.get('created');\n const timeStr = this._relativeTime(modified);\n const isActive = id === this.activeId;\n const user = model.get('user');\n const userName = user?.display_name || '';\n const avatarUrl = user?.avatar?.thumbnail || user?.avatar?.url || '';\n\n const item = document.createElement('div');\n item.className = `conversation-item px-3 py-2${isActive ? ' active' : ''}`;\n item.dataset.id = id;\n item.innerHTML = `\n <div class=\"d-flex align-items-start\">\n ${avatarUrl\n ? `<img src=\"${this._escapeHtml(avatarUrl)}\" alt=\"\" class=\"conversation-avatar\">`\n : `<div class=\"conversation-avatar conversation-avatar-initials\">${this._escapeHtml(this._initials(userName))}</div>`\n }\n <div class=\"flex-grow-1 overflow-hidden\">\n <div class=\"text-truncate conversation-title\">${this._escapeHtml(title)}</div>\n <div class=\"conversation-meta text-muted\">\n ${userName ? `<span>${this._escapeHtml(userName)}</span>` : ''}\n ${timeStr ? `<span>${timeStr}</span>` : ''}\n </div>\n </div>\n <button class=\"btn btn-sm btn-link text-muted p-0 ms-2 conversation-delete\" data-action=\"delete-conversation\" data-id=\"${id}\" title=\"Delete\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </div>\n `;\n\n // Click on the item (not the delete button) to select\n item.addEventListener('click', (e) => {\n if (e.target.closest('[data-action=\"delete-conversation\"]')) return;\n this.setActive(id);\n this.emit('conversation:select', { id, model });\n });\n\n container.appendChild(item);\n });\n }\n\n // \"Load more\" button when there are more pages\n if (this.collection.hasMore) {\n const loadMore = document.createElement('div');\n loadMore.className = 'conversation-load-more text-center py-2';\n loadMore.innerHTML = '<button class=\"btn btn-sm btn-link text-muted\" data-action=\"load-more\">Load more</button>';\n container.appendChild(loadMore);\n }\n }\n\n /**\n * Load next page of conversations and append\n */\n async onActionLoadMore() {\n await this.collection.nextPage();\n this._renderItems();\n }\n\n /**\n * Group models by date: Today, Yesterday, Earlier\n * @private\n */\n _groupByDate(models) {\n const now = new Date();\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n const groups = new Map();\n groups.set('Today', []);\n groups.set('Yesterday', []);\n groups.set('Earlier', []);\n\n models.forEach(model => {\n const raw = model.get('created') || model.get('modified');\n const created = new Date(dataFormatter.normalizeEpoch(raw));\n const createdDate = new Date(created.getFullYear(), created.getMonth(), created.getDate());\n\n if (createdDate >= today) {\n groups.get('Today').push(model);\n } else if (createdDate >= yesterday) {\n groups.get('Yesterday').push(model);\n } else {\n groups.get('Earlier').push(model);\n }\n });\n\n // Remove empty groups\n const result = [];\n for (const [label, items] of groups) {\n if (items.length > 0) result.push([label, items]);\n }\n return result;\n }\n\n /**\n * Set the active conversation\n * @param {number|string} id - Conversation ID to highlight\n */\n setActive(id) {\n this.activeId = id;\n const items = this.element.querySelectorAll('.conversation-item');\n items.forEach(item => {\n item.classList.toggle('active', String(item.dataset.id) === String(id));\n });\n }\n\n /**\n * Handle new conversation action\n */\n onActionNewConversation() {\n this.setActive(null);\n this.emit('conversation:new');\n }\n\n /**\n * Handle delete conversation action\n */\n async onActionDeleteConversation(event, element) {\n const id = element.dataset.id;\n const confirmed = await Modal.confirm({\n title: 'Delete Conversation',\n message: 'Are you sure you want to delete this conversation? This cannot be undone.',\n confirmText: 'Delete',\n confirmClass: 'btn-danger'\n });\n\n if (!confirmed) return;\n\n const model = this.collection.models.find(m => String(m.get('id')) === String(id));\n if (model) {\n await model.destroy();\n await this.refresh();\n this.emit('conversation:deleted', { id });\n }\n }\n\n /**\n * Refresh the conversation list\n */\n async refresh() {\n await this.collection.fetch();\n this._renderItems();\n }\n\n /**\n * Format a date string as relative time (e.g. \"2m ago\", \"3h ago\", \"Jan 5\")\n * @private\n */\n _relativeTime(dateStr) {\n if (!dateStr) return '';\n const date = new Date(dataFormatter.normalizeEpoch(dateStr));\n if (isNaN(date)) return '';\n\n const now = Date.now();\n const diff = now - date.getTime();\n const seconds = Math.floor(diff / 1000);\n\n if (seconds < 60) return 'just now';\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n\n // Older than a week — show short date\n const month = date.toLocaleString('default', { month: 'short' });\n return `${month} ${date.getDate()}`;\n }\n\n /**\n * Get initials from a display name (e.g. \"Internal Use\" → \"IU\")\n * @private\n */\n _initials(name) {\n if (!name) return '?';\n return name.split(/\\s+/).map(w => w[0]).join('').toUpperCase().slice(0, 2);\n }\n\n /**\n * Escape HTML\n * @private\n */\n _escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n}\n\nexport default AssistantConversationListView;\n","import View from '@core/View.js';\nimport ChatView from '@core/views/chat/ChatView.js';\nimport { AssistantConversation, AssistantConversationList } from '@ext/admin/models/Assistant.js';\nimport AssistantMessageView from './AssistantMessageView.js';\nimport AssistantConversationListView from './AssistantConversationListView.js';\n\n/**\n * AssistantView - Polished admin assistant chat interface\n *\n * Two-panel layout shown inside a fullscreen modal:\n * - Left: conversation history sidebar\n * - Right: chat area with welcome state, messages, and prominent input\n *\n * WS events: assistant_thinking, assistant_text, assistant_tool_call, assistant_response, assistant_error\n */\nclass AssistantView extends View {\n constructor(options = {}) {\n super({\n className: 'assistant-view',\n ...options\n });\n\n this.app = options.app;\n this.ws = this.app?.ws;\n this.conversationId = null;\n this._wsHandlers = {};\n this._messageIdCounter = 0;\n this._hasMessages = false;\n this._activePlans = {};\n this._requestStartTime = null;\n }\n\n getTemplate() {\n const userName = this._escapeHtml(\n this.app?.activeUser?.get('first_name') || 'there'\n );\n\n return `\n <div class=\"assistant-layout\">\n <div class=\"assistant-sidebar\" data-container=\"conversation-list\"></div>\n <div class=\"assistant-main\">\n <div class=\"assistant-welcome\" data-ref=\"welcome\">\n <div class=\"assistant-welcome-content\">\n <div class=\"assistant-welcome-icon\">\n <i class=\"bi bi-stars\"></i>\n </div>\n <h3 class=\"assistant-welcome-title\">Hi ${userName}</h3>\n <p class=\"assistant-welcome-subtitle\">How can I help you today?</p>\n <div class=\"assistant-suggestions\">\n <button class=\"assistant-suggestion\" data-action=\"use-suggestion\" data-text=\"Show me a summary of recent activity\">\n <i class=\"bi bi-activity\"></i>\n <span>Recent activity summary</span>\n </button>\n <button class=\"assistant-suggestion\" data-action=\"use-suggestion\" data-text=\"How many active users are there?\">\n <i class=\"bi bi-people\"></i>\n <span>Active user count</span>\n </button>\n <button class=\"assistant-suggestion\" data-action=\"use-suggestion\" data-text=\"Show me system health metrics\">\n <i class=\"bi bi-heart-pulse\"></i>\n <span>System health check</span>\n </button>\n </div>\n </div>\n </div>\n <div class=\"assistant-chat-area\" data-container=\"chat-area\"></div>\n <div class=\"assistant-input-wrapper\">\n <div class=\"assistant-input-status d-none\" data-ref=\"input-status\"></div>\n <div class=\"assistant-input-box\">\n <textarea class=\"assistant-input\" placeholder=\"Message the assistant...\" rows=\"1\" data-ref=\"input\"></textarea>\n <button class=\"assistant-send-btn\" data-action=\"send\" type=\"button\" title=\"Send message\" data-ref=\"send-btn\">\n <i class=\"bi bi-arrow-up\"></i>\n </button>\n <button class=\"assistant-stop-btn d-none\" data-action=\"stop\" type=\"button\" title=\"Stop generating\" data-ref=\"stop-btn\">\n <i class=\"bi bi-stop-fill\"></i>\n </button>\n </div>\n <div class=\"assistant-input-footer\">\n <span class=\"assistant-connection-indicator\" data-ref=\"status\">\n <span class=\"status-dot connected\"></span>\n </span>\n <span class=\"text-muted\">Press Enter to send, Shift+Enter for new line</span>\n </div>\n </div>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n // Conversation list — scoped to current user\n this.conversations = new AssistantConversationList();\n this.conversations.params.user = this.app?.activeUser?.id;\n this.conversationListView = new AssistantConversationListView({\n containerId: 'conversation-list',\n collection: this.conversations\n });\n this.addChild(this.conversationListView);\n\n // Chat view — input hidden, we use our own\n this.chatView = new ChatView({\n containerId: 'chat-area',\n theme: 'compact',\n messageViewClass: AssistantMessageView,\n currentUserId: this.app?.activeUser?.id,\n showFileInput: false,\n showInput: false,\n adapter: this._createAdapter()\n });\n this.addChild(this.chatView);\n\n // Safety net: whenever an assistant message is added to the chat\n // from any source (WS, fetch, etc.), re-enable input.\n const origAddMessage = this.chatView.addMessage.bind(this.chatView);\n this.chatView.addMessage = (msg, scroll) => {\n origAddMessage(msg, scroll);\n if (msg.role === 'assistant' && (msg.content || msg.blocks?.length)) {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n }\n };\n\n // Wire conversation list events\n this.conversationListView.on('conversation:select', (data) => this._onConversationSelect(data));\n this.conversationListView.on('conversation:new', () => this._onNewConversation());\n this.conversationListView.on('conversation:deleted', (data) => this._onConversationDeleted(data));\n\n // Subscribe to WS events\n this._subscribeWS();\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Setup our custom input\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (textarea) {\n textarea.addEventListener('input', () => this._autoResize(textarea));\n textarea.addEventListener('keydown', (e) => this._handleKeydown(e));\n // Focus the input\n setTimeout(() => textarea.focus(), 100);\n }\n\n this._updateConnectionStatus();\n }\n\n // ── Custom Input Handling ─────────────────────────────\n\n /**\n * Auto-resize textarea to fit content\n * @private\n */\n _autoResize(textarea) {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';\n }\n\n /**\n * Handle keydown: Enter sends, Shift+Enter adds newline\n * @private\n */\n _handleKeydown(e) {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n this._sendMessage();\n }\n }\n\n /**\n * Handle suggestion button clicks\n */\n onActionUseSuggestion(_event, element) {\n const text = element.dataset.text || element.closest('[data-text]')?.dataset.text;\n if (!text) return;\n\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (textarea) {\n textarea.value = text;\n this._autoResize(textarea);\n }\n this._sendMessage();\n }\n\n /**\n * Handle send button click\n */\n onActionSend() {\n this._sendMessage();\n }\n\n /**\n * Send the current input text\n * @private\n */\n async _sendMessage() {\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (!textarea) return;\n\n const text = textarea.value.trim();\n if (!text) return;\n\n // Clear input immediately\n textarea.value = '';\n textarea.style.height = 'auto';\n\n // Show chat area, hide welcome\n this._showChatArea();\n\n // Send through adapter\n await this.chatView.adapter.addNote({ text, files: [] });\n }\n\n /**\n * Show chat area and hide welcome screen\n * @private\n */\n _showChatArea() {\n if (this._hasMessages) return;\n this._hasMessages = true;\n\n const welcome = this.element.querySelector('[data-ref=\"welcome\"]');\n const chatArea = this.element.querySelector('[data-container=\"chat-area\"]');\n if (welcome) welcome.classList.add('d-none');\n if (chatArea) chatArea.classList.remove('d-none');\n }\n\n /**\n * Show welcome screen and hide chat area\n * @private\n */\n _showWelcome() {\n this._hasMessages = false;\n\n const welcome = this.element.querySelector('[data-ref=\"welcome\"]');\n const chatArea = this.element.querySelector('[data-container=\"chat-area\"]');\n if (welcome) welcome.classList.remove('d-none');\n if (chatArea) chatArea.classList.add('d-none');\n }\n\n /**\n * Set input enabled/disabled state and toggle send/stop buttons\n * @private\n */\n _setInputEnabled(enabled, reason) {\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n const sendBtn = this.element?.querySelector('[data-ref=\"send-btn\"]');\n const stopBtn = this.element?.querySelector('[data-ref=\"stop-btn\"]');\n\n if (textarea) textarea.disabled = !enabled;\n\n // Toggle send/stop button visibility\n if (sendBtn) sendBtn.classList.toggle('d-none', !enabled);\n if (stopBtn) stopBtn.classList.toggle('d-none', enabled);\n\n // Show/hide status bar with reason\n this._setInputStatus(enabled ? null : reason);\n\n // Clear or set safety timeout\n if (this._responseTimeout) clearTimeout(this._responseTimeout);\n if (!enabled) {\n this._responseTimeout = setTimeout(() => this._onResponseTimeout(), 60000);\n } else {\n this._requestStartTime = null;\n }\n }\n\n /**\n * Show or hide the input status bar.\n * @param {string|null} message - Status text, or null to hide.\n * @private\n */\n _setInputStatus(message) {\n const el = this.element?.querySelector('[data-ref=\"input-status\"]');\n if (!el) return;\n if (message) {\n el.innerHTML = `${this._escapeHtml(message)} <span class=\"assistant-input-status-dismiss\">Click to dismiss</span>`;\n el.classList.remove('d-none');\n // Click to dismiss — escape hatch for stuck state\n if (!el._hasDismiss) {\n el._hasDismiss = true;\n el.addEventListener('click', () => {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n });\n }\n } else {\n el.classList.add('d-none');\n el.innerHTML = '';\n }\n }\n\n /**\n * Handle stop button click — abort waiting for response\n */\n onActionStop() {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._showSystemMessage('Response cancelled.');\n\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n }\n\n /**\n * Safety timeout — re-enable input if server never responds\n * @private\n */\n _onResponseTimeout() {\n this._responseTimeout = null;\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._showSystemMessage('Request timed out. Please try again.');\n }\n\n // ── Chat Adapter ─────────────────────────────────────\n\n _createAdapter() {\n return {\n fetch: async () => {\n if (!this.conversationId) return [];\n try {\n const conversation = new AssistantConversation({ id: this.conversationId });\n await conversation.fetch({ graph: 'detail' });\n const messages = conversation.get('messages') || [];\n const transformed = messages.map(msg => this._transformMessage(msg)).filter(Boolean);\n return AssistantView._collapseMessages(transformed);\n } catch (_err) {\n if (_err.status === 404) {\n this._onNewConversation();\n this._showSystemMessage('Conversation not found.');\n }\n return [];\n }\n },\n addNote: async (data) => {\n if (!data.text || !data.text.trim()) return { success: false };\n\n // Add user message to chat immediately\n const userMsg = {\n id: `local-${++this._messageIdCounter}`,\n role: 'user',\n author: {\n id: this.app?.activeUser?.id,\n name: this.app?.activeUser?.get('display_name') || 'You'\n },\n content: data.text,\n timestamp: new Date().toISOString()\n };\n this.chatView.addMessage(userMsg);\n\n // Show thinking immediately — don't wait for server event\n this.chatView.showThinking('Thinking...');\n this._requestStartTime = Date.now();\n this._setInputEnabled(false, 'Waiting for response…');\n\n // Send via WebSocket\n if (this.ws && this.ws.isConnected) {\n this.ws.send({\n type: 'assistant_message',\n message: data.text,\n conversation_id: this.conversationId\n });\n } else {\n // Fallback: REST POST\n try {\n const resp = await this.app.rest.post('/api/assistant', {\n message: data.text,\n conversation_id: this.conversationId\n });\n const respData = resp?.data?.data || resp?.data || resp;\n if (respData.conversation_id) {\n this.conversationId = respData.conversation_id;\n }\n if (respData.response) {\n this.chatView.addMessage(this._transformMessage(respData.response));\n }\n this._setInputEnabled(true);\n } catch (_err) {\n this._handleAPIError(_err);\n }\n }\n\n return { success: true };\n }\n };\n }\n\n // ── WebSocket Subscriptions ───────────────────────────\n\n _subscribeWS() {\n if (!this.ws) return;\n\n // Server sends events two ways:\n // 1. Direct WS response: {\"type\":\"assistant_thinking\", ...}\n // → WebSocketClient emits \"message:assistant_thinking\"\n // 2. Background thread via send_to_user: {\"type\":\"message\",\"data\":{\"type\":\"assistant_*\",...}}\n // → WebSocketClient emits \"message:message\"\n this._wsHandlers = {\n thinking: (data) => this._onThinking(data),\n text: (data) => this._onText(data),\n tool_call: (data) => this._onToolCall(data),\n response: (data) => this._onResponse(data),\n error: (data) => this._onError(data),\n plan: (data) => this._onPlan(data),\n plan_update: (data) => this._onPlanUpdate(data),\n message: (envelope) => this._dispatchWSMessage(envelope),\n connected: () => this._updateConnectionStatus(),\n disconnected: () => this._updateConnectionStatus(),\n reconnecting: () => this._updateConnectionStatus()\n };\n\n // Direct events (from WS handler return)\n this.ws.on('message:assistant_thinking', this._wsHandlers.thinking);\n this.ws.on('message:assistant_text', this._wsHandlers.text);\n this.ws.on('message:assistant_tool_call', this._wsHandlers.tool_call);\n this.ws.on('message:assistant_response', this._wsHandlers.response);\n this.ws.on('message:assistant_error', this._wsHandlers.error);\n this.ws.on('message:assistant_plan', this._wsHandlers.plan);\n this.ws.on('message:assistant_plan_update', this._wsHandlers.plan_update);\n // Wrapped events (from background thread via send_to_user)\n this.ws.on('message:message', this._wsHandlers.message);\n // Connection status\n this.ws.on('connected', this._wsHandlers.connected);\n this.ws.on('disconnected', this._wsHandlers.disconnected);\n this.ws.on('reconnecting', this._wsHandlers.reconnecting);\n }\n\n _unsubscribeWS() {\n if (!this.ws || !this._wsHandlers) return;\n\n this.ws.off('message:assistant_thinking', this._wsHandlers.thinking);\n this.ws.off('message:assistant_text', this._wsHandlers.text);\n this.ws.off('message:assistant_tool_call', this._wsHandlers.tool_call);\n this.ws.off('message:assistant_response', this._wsHandlers.response);\n this.ws.off('message:assistant_error', this._wsHandlers.error);\n this.ws.off('message:assistant_plan', this._wsHandlers.plan);\n this.ws.off('message:assistant_plan_update', this._wsHandlers.plan_update);\n this.ws.off('message:message', this._wsHandlers.message);\n this.ws.off('connected', this._wsHandlers.connected);\n this.ws.off('disconnected', this._wsHandlers.disconnected);\n this.ws.off('reconnecting', this._wsHandlers.reconnecting);\n\n this._wsHandlers = {};\n }\n\n // ── WS Event Handlers ────────────────────────────────\n\n /**\n * Dispatch an incoming WS message envelope to the correct handler.\n * Envelope shape: { type: \"message\", data: { type: \"assistant_*\", ... }, timestamp }\n * @private\n */\n _dispatchWSMessage(envelope) {\n const inner = envelope?.data;\n if (!inner?.type) return;\n\n switch (inner.type) {\n case 'assistant_thinking': this._onThinking(inner); break;\n case 'assistant_text': this._onText(inner); break;\n case 'assistant_tool_call': this._onToolCall(inner); break;\n case 'assistant_response': this._onResponse(inner); break;\n case 'assistant_error': this._onError(inner); break;\n case 'assistant_plan': this._onPlan(inner); break;\n case 'assistant_plan_update': this._onPlanUpdate(inner); break;\n }\n }\n\n /**\n * Check if a WS event belongs to this view's conversation.\n * Accept if: no conversation_id on event, or matches ours, or we have none yet (new conversation).\n * @private\n */\n _isMyConversation(data) {\n if (!data.conversation_id) return true;\n if (!this.conversationId) return true; // new conversation — accept server-assigned ID\n return String(data.conversation_id) === String(this.conversationId);\n }\n\n /**\n * Adopt a conversation ID from a WS event if we don't have one yet.\n * @private\n */\n _adoptConversationId(data) {\n if (data.conversation_id && !this.conversationId) {\n this.conversationId = data.conversation_id;\n this.conversationListView.refresh();\n }\n }\n\n _onThinking(data) {\n if (!this._isMyConversation(data)) return;\n this._adoptConversationId(data);\n this._showChatArea();\n this.chatView.showThinking('Thinking...');\n this._setInputEnabled(false, 'Assistant is thinking…');\n }\n\n /**\n * Intermediate prose emitted by the model alongside tool calls in the same\n * turn. Renders as an assistant bubble using the same path as a final\n * response, but does NOT clear the thinking indicator or re-enable input —\n * `assistant_response` remains the terminal signal for the turn.\n * @private\n */\n _onText(data) {\n if (!this._isMyConversation(data)) return;\n this._adoptConversationId(data);\n // Server is still working — refresh the safety timeout so a long\n // intermediate text bubble doesn't trip the 60s \"Request timed out\".\n this._resetResponseTimeout();\n\n const msg = this._transformMessage({\n id: data.message_id || `text-${++this._messageIdCounter}`,\n role: 'assistant',\n content: data.text || '',\n blocks: data.blocks || [],\n tool_calls: [],\n created: data.created || data.timestamp || new Date().toISOString()\n });\n\n if (msg && (msg.content || msg.blocks?.length)) {\n this.chatView.addMessage(msg);\n }\n }\n\n _onToolCall(data) {\n if (!this._isMyConversation(data)) return;\n this.chatView.showThinking(`Using ${data.tool || data.name || 'tool'}...`);\n // Reset the safety timeout — server is still working\n this._resetResponseTimeout();\n }\n\n /**\n * Reset the safety timeout without changing input enabled state.\n * Called on intermediate events (tool_call, plan_update) to prevent\n * false \"Request timed out\" while the server is still processing.\n * @private\n */\n _resetResponseTimeout() {\n if (this._responseTimeout) {\n // Hard cap: 5 minutes from the original request — prevents infinite deferral\n if (this._requestStartTime && (Date.now() - this._requestStartTime) >= 300000) {\n this._onResponseTimeout();\n return;\n }\n clearTimeout(this._responseTimeout);\n this._responseTimeout = setTimeout(() => this._onResponseTimeout(), 60000);\n }\n }\n\n _onResponse(data) {\n if (!this._isMyConversation(data)) return;\n\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._adoptConversationId(data);\n\n // Focus input after response\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n\n const msg = this._transformMessage({\n id: data.message_id || `resp-${++this._messageIdCounter}`,\n role: 'assistant',\n content: data.response || data.content || data.message || '',\n blocks: data.blocks || [],\n tool_calls: data.tool_calls_made || data.tool_calls || [],\n created: data.created || data.timestamp || new Date().toISOString()\n });\n\n // Skip empty messages (e.g. plan-only responses where all tool calls are internal)\n if (msg && (msg.content || msg.blocks?.length || msg.tool_calls?.length)) {\n this.chatView.addMessage(msg);\n }\n }\n\n _onError(data) {\n if (!this._isMyConversation(data)) return;\n\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this._adoptConversationId(data);\n\n const errorText = data.error || data.message || 'An error occurred';\n this._showSystemMessage(errorText);\n }\n\n _onPlan(data) {\n if (!this._isMyConversation(data)) return;\n this._adoptConversationId(data);\n this._showChatArea();\n\n const plan = data.plan;\n if (!plan) return;\n\n this._activePlans[plan.plan_id] = plan;\n\n // Add as a message with a progress block\n this.chatView.addMessage({\n id: `plan-${plan.plan_id}`,\n role: 'assistant',\n author: { name: 'Assistant' },\n content: '',\n timestamp: new Date().toISOString(),\n blocks: [{ type: 'progress', ...plan }],\n tool_calls: []\n });\n }\n\n _onPlanUpdate(data) {\n if (!this._isMyConversation(data)) return;\n\n const plan = this._activePlans[data.plan_id];\n if (plan) {\n const step = plan.steps.find(s => s.id === data.step_id);\n if (step) {\n step.status = data.status;\n step.summary = data.summary;\n }\n }\n\n // Update the rendered step in-place\n const msgView = this.chatView.messageViews.get(`plan-${data.plan_id}`);\n if (msgView?.updateProgressStep) {\n msgView.updateProgressStep(data.plan_id, data.step_id, data.status, data.summary);\n }\n this._resetResponseTimeout();\n }\n\n // ── Conversation Management ──────────────────────────\n\n async _onConversationSelect(data) {\n this.conversationId = data.id;\n this.conversationListView.setActive(data.id);\n this._showChatArea();\n await this.chatView.refresh();\n }\n\n _onNewConversation() {\n this.conversationId = null;\n this.conversationListView.setActive(null);\n this.chatView.clearMessages();\n this._setInputEnabled(true);\n this._showWelcome();\n\n // Focus input\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n }\n\n _onConversationDeleted(data) {\n if (String(data.id) === String(this.conversationId)) {\n this._onNewConversation();\n }\n }\n\n // ── Helpers ──────────────────────────────────────────\n\n _transformMessage(msg) {\n // Skip tool_result messages — they are internal API artifacts\n if (msg.role === 'tool_result') return null;\n\n let content = msg.content || msg.text || '';\n let blocks = msg.blocks || [];\n let toolCalls = msg.tool_calls || [];\n\n // Extract thinking text from tool_call entries and separate tool_use entries\n if (toolCalls.length > 0) {\n // Normalize WS `tool_calls_made` shape `{ tool, input }` → Anthropic `{ type, name, input }`\n toolCalls = toolCalls.map(tc =>\n (!tc.type && tc.tool)\n ? { type: 'tool_use', name: tc.tool, input: tc.input }\n : tc\n );\n const textParts = toolCalls\n .filter(tc => tc.type === 'text' && tc.text)\n .map(tc => tc.text);\n if (!content && textParts.length > 0) {\n content = textParts.join('\\n\\n');\n }\n toolCalls = toolCalls\n .filter(tc => tc.type === 'tool_use')\n .filter(tc => !AssistantView.INTERNAL_TOOLS.has(tc.name));\n }\n\n // Safety net: parse blocks from content if API didn't return them\n // (e.g. WS events or older API responses that still embed fences).\n if (blocks.length === 0 && content.includes('assistant_block')) {\n const parsed = AssistantView._parseBlocks(content);\n content = parsed.content;\n blocks = parsed.blocks;\n }\n\n const currentUserId = this.app?.activeUser?.id;\n\n return {\n id: msg.id,\n role: msg.role || 'user',\n author: msg.role === 'assistant'\n ? { name: 'Assistant' }\n : msg.author || {\n name: msg.user?.display_name || this.app?.activeUser?.get('display_name') || 'You',\n id: msg.user?.id || currentUserId\n },\n content,\n timestamp: msg.created || msg.timestamp,\n blocks,\n tool_calls: toolCalls,\n _conversationId: this.conversationId\n };\n }\n\n /**\n * Post-process transformed messages to reduce noise:\n * 1. Strip internal orchestration tool calls (create_plan, update_plan, load_tools)\n * 2. Drop messages that become empty after stripping\n * 3. Merge consecutive tool-call-only assistant messages into one\n * @static\n */\n static _collapseMessages(messages) {\n const result = [];\n\n for (const msg of messages) {\n // Only process assistant messages with tool_calls\n if (msg.role === 'assistant' && msg.tool_calls?.length > 0) {\n msg.tool_calls = msg.tool_calls.filter(tc => !AssistantView.INTERNAL_TOOLS.has(tc.name));\n }\n\n const hasContent = !!msg.content;\n const hasTools = msg.tool_calls?.length > 0;\n const hasBlocks = msg.blocks?.length > 0;\n\n // Skip empty assistant messages (no content, no tools, no blocks)\n if (msg.role === 'assistant' && !hasContent && !hasTools && !hasBlocks) {\n continue;\n }\n\n // Merge consecutive tool-call-only assistant messages\n if (msg.role === 'assistant' && !hasContent && hasTools && !hasBlocks) {\n const prev = result[result.length - 1];\n if (prev?.role === 'assistant' && !prev.content && prev.tool_calls?.length > 0 && !prev.blocks?.length) {\n prev.tool_calls = [...prev.tool_calls, ...msg.tool_calls];\n continue;\n }\n }\n\n result.push(msg);\n }\n\n return result;\n }\n\n /**\n * Parse assistant_block fences from message content.\n * Mirrors the server-side _parse_blocks() logic.\n * @static\n */\n static _parseBlocks(text) {\n const BLOCK_RE = /```assistant_block\\s*\\n([\\s\\S]*?)```/g;\n const VALID_TYPES = new Set(['table', 'chart', 'stat', 'action', 'list', 'alert', 'progress', 'file']);\n const blocks = [];\n let match;\n\n while ((match = BLOCK_RE.exec(text)) !== null) {\n try {\n const block = JSON.parse(match[1].trim());\n if (block && VALID_TYPES.has(block.type)) {\n blocks.push(block);\n }\n } catch (_e) {\n // Skip malformed blocks\n }\n }\n\n const content = text.replace(BLOCK_RE, '').replace(/\\n{3,}/g, '\\n\\n').trim();\n return { content, blocks };\n }\n\n _showSystemMessage(text) {\n this._showChatArea();\n this.chatView.addMessage({\n id: `sys-${++this._messageIdCounter}`,\n type: 'system_event',\n content: text,\n timestamp: new Date().toISOString()\n });\n }\n\n _handleAPIError(_err) {\n if (_err.status === 404) {\n this._showSystemMessage('Assistant is not enabled on this server.');\n } else if (_err.status === 503) {\n this._showSystemMessage('LLM API key not configured. Contact your administrator.');\n } else {\n this._showSystemMessage('Failed to send message. Please try again.');\n }\n this._setInputEnabled(true);\n }\n\n /**\n * Update the connection status indicator\n * @private\n */\n _updateConnectionStatus() {\n const dot = this.element?.querySelector('.status-dot');\n if (!dot) return;\n\n if (this.ws?.isConnected) {\n dot.className = 'status-dot connected';\n dot.title = 'Connected';\n // Re-enable input on reconnect if no pending request\n if (!this._responseTimeout) {\n this._setInputEnabled(true);\n } else {\n // Still waiting — fully restore the \"waiting\" UI so buttons are correct\n this._setInputEnabled(false, 'Waiting for response…');\n }\n } else if (this.ws?.isReconnecting) {\n dot.className = 'status-dot reconnecting';\n dot.title = 'Reconnecting...';\n // Pause input through the standard path; clear the response timeout\n // since it only governs active requests over a live connection.\n this._setInputEnabled(false, 'Reconnecting…');\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = null;\n }\n } else {\n dot.className = 'status-dot disconnected';\n dot.title = 'Disconnected';\n // Disable input through the standard path; clear the response timeout.\n this._setInputEnabled(false, 'Disconnected — reconnecting…');\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = null;\n }\n }\n }\n\n _escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n async onBeforeDestroy() {\n this._unsubscribeWS();\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = null;\n }\n }\n}\n\nAssistantView.INTERNAL_TOOLS = new Set(['create_plan', 'update_plan', 'load_tools']);\n\nexport default AssistantView;\n","/**\n * AssistantContextChat - Scoped AI assistant chat for any model view\n *\n * Opens a single-conversation assistant chat in a Dialog, scoped to a specific\n * model instance via POST /api/assistant/context. The conversation_id is\n * persisted in the model's metadata for session resumption.\n *\n * Usage from any view:\n * import { openAssistantChat } from '@ext/admin/assistant/AssistantContextChat.js';\n * await openAssistantChat(this, 'incident.Ticket');\n */\n\nimport View from '@core/View.js';\nimport ChatView from '@core/views/chat/ChatView.js';\nimport { AssistantConversation } from '@ext/admin/models/Assistant.js';\nimport AssistantMessageView from './AssistantMessageView.js';\nimport AssistantView from './AssistantView.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\n\n// ── Adapter ────────────────────────────────────────────────\n\n/**\n * ChatView adapter for context-scoped assistant conversations.\n * Implements the { fetch(), addNote() } interface expected by ChatView.\n */\nclass AssistantContextAdapter {\n constructor({ app, modelName, pk, conversationId }) {\n this.app = app;\n this.modelName = modelName;\n this.pk = pk;\n this.conversationId = conversationId;\n this._messageIdCounter = 0;\n this._onConversationCreated = null;\n }\n\n async fetch() {\n // Create or retrieve conversation on first fetch\n if (!this.conversationId) {\n try {\n const resp = await this.app.rest.post('/api/assistant/context', {\n model: this.modelName,\n pk: this.pk\n });\n const data = resp?.data?.data || resp?.data || resp;\n this.conversationId = data.conversation_id;\n if (this._onConversationCreated) {\n this._onConversationCreated(this.conversationId);\n }\n } catch (_err) {\n return [];\n }\n }\n\n // Fetch conversation messages\n try {\n const conversation = new AssistantConversation({ id: this.conversationId });\n await conversation.fetch({ graph: 'detail' });\n const messages = conversation.get('messages') || [];\n const transformed = messages.map(msg => this._transformMessage(msg)).filter(Boolean);\n return AssistantView._collapseMessages(transformed);\n } catch (_err) {\n // Stale conversation — clear and retry once\n if (_err.status === 404 && this.conversationId && !this._fetchRetried) {\n this._fetchRetried = true;\n this.conversationId = null;\n return this.fetch();\n }\n return [];\n }\n }\n\n async addNote(data) {\n if (!data.text || !data.text.trim()) return { success: false };\n return { success: true };\n }\n\n _transformMessage(msg) {\n // Skip tool_result messages — they are internal API artifacts\n if (msg.role === 'tool_result') return null;\n\n let content = msg.content || msg.text || '';\n let blocks = msg.blocks || [];\n let toolCalls = msg.tool_calls || [];\n\n // Extract thinking text from tool_call entries and separate tool_use entries\n if (toolCalls.length > 0) {\n // Normalize WS `tool_calls_made` shape `{ tool, input }` → Anthropic `{ type, name, input }`\n toolCalls = toolCalls.map(tc =>\n (!tc.type && tc.tool)\n ? { type: 'tool_use', name: tc.tool, input: tc.input }\n : tc\n );\n const textParts = toolCalls\n .filter(tc => tc.type === 'text' && tc.text)\n .map(tc => tc.text);\n if (!content && textParts.length > 0) {\n content = textParts.join('\\n\\n');\n }\n toolCalls = toolCalls\n .filter(tc => tc.type === 'tool_use')\n .filter(tc => !AssistantView.INTERNAL_TOOLS.has(tc.name));\n }\n\n // Parse embedded blocks from content if not provided\n if (blocks.length === 0 && content.includes('assistant_block')) {\n const BLOCK_RE = /```assistant_block\\s*\\n([\\s\\S]*?)```/g;\n const VALID_TYPES = new Set(['table', 'chart', 'stat', 'action', 'list', 'alert', 'progress', 'file']);\n let match;\n while ((match = BLOCK_RE.exec(content)) !== null) {\n try {\n const block = JSON.parse(match[1].trim());\n if (block && VALID_TYPES.has(block.type)) blocks.push(block);\n } catch (_e) { /* skip */ }\n }\n content = content.replace(BLOCK_RE, '').replace(/\\n{3,}/g, '\\n\\n').trim();\n }\n\n return {\n id: msg.id,\n role: msg.role || 'user',\n author: msg.role === 'assistant'\n ? { name: 'Assistant' }\n : msg.author || {\n name: msg.user?.display_name || this.app?.activeUser?.get('display_name') || 'You',\n id: msg.user?.id || this.app?.activeUser?.id\n },\n content,\n timestamp: msg.created || msg.timestamp,\n blocks,\n tool_calls: toolCalls,\n _conversationId: this.conversationId\n };\n }\n}\n\n\n// ── Context Chat View ──────────────────────────────────────\n\n/**\n * Self-contained assistant chat view with custom input and WebSocket streaming.\n * No conversation sidebar — always a single conversation.\n */\nclass AssistantContextChat extends View {\n constructor(options = {}) {\n super({\n className: 'assistant-context-chat',\n ...options\n });\n\n this.app = options.app;\n this.ws = this.app?.ws;\n this.adapter = options.adapter;\n this._wsHandlers = {};\n this._messageIdCounter = 0;\n this._activePlans = {};\n\n this.template = `\n <div class=\"d-flex flex-column h-100\">\n <div class=\"flex-grow-1\" style=\"min-height: 0;\" data-container=\"chat-area\"></div>\n <div class=\"assistant-input-wrapper border-top p-2\">\n <div class=\"assistant-input-status d-none\" data-ref=\"input-status\"></div>\n <div class=\"d-flex gap-2 align-items-end\">\n <textarea class=\"form-control\" placeholder=\"Ask the AI assistant...\" rows=\"1\" data-ref=\"input\" style=\"resize: none; max-height: 150px;\"></textarea>\n <button class=\"btn btn-primary\" data-action=\"send\" type=\"button\" data-ref=\"send-btn\">\n <i class=\"bi bi-arrow-up\"></i>\n </button>\n <button class=\"btn btn-outline-danger d-none\" data-action=\"stop\" type=\"button\" data-ref=\"stop-btn\">\n <i class=\"bi bi-stop-fill\"></i>\n </button>\n </div>\n <div class=\"d-flex align-items-center gap-2 mt-1\">\n <span class=\"assistant-connection-indicator\" data-ref=\"status\">\n <span class=\"status-dot connected\" style=\"display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #198754;\"></span>\n </span>\n <span class=\"text-muted small\">Enter to send, Shift+Enter for new line</span>\n </div>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n this.chatView = new ChatView({\n containerId: 'chat-area',\n theme: 'compact',\n messageViewClass: AssistantMessageView,\n currentUserId: this.app?.activeUser?.id,\n showFileInput: false,\n showInput: false,\n adapter: this.adapter\n });\n this.addChild(this.chatView);\n\n // Safety net: whenever an assistant message with content arrives,\n // re-enable input regardless of which path delivered it.\n const origAddMessage = this.chatView.addMessage.bind(this.chatView);\n this.chatView.addMessage = (msg, scroll) => {\n origAddMessage(msg, scroll);\n if (msg.role === 'assistant' && (msg.content || msg.blocks?.length)) {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n }\n };\n\n this._subscribeWS();\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (textarea) {\n textarea.addEventListener('input', () => this._autoResize(textarea));\n textarea.addEventListener('keydown', (e) => this._handleKeydown(e));\n setTimeout(() => textarea.focus(), 100);\n }\n\n this._updateConnectionStatus();\n }\n\n // ── Input ────────────────────────────────────────────────\n\n _autoResize(textarea) {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';\n }\n\n _handleKeydown(e) {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n this._sendMessage();\n }\n }\n\n onActionSend() {\n this._sendMessage();\n }\n\n async _sendMessage() {\n const textarea = this.element.querySelector('[data-ref=\"input\"]');\n if (!textarea) return;\n\n const text = textarea.value.trim();\n if (!text) return;\n\n // Clear input\n textarea.value = '';\n textarea.style.height = 'auto';\n\n // Ensure conversation exists\n if (!this.adapter.conversationId) {\n await this.adapter.fetch();\n if (!this.adapter.conversationId) {\n this.app?.toast?.error('Failed to create conversation');\n return;\n }\n }\n\n // Add user message to chat\n const userMsg = {\n id: `local-${++this._messageIdCounter}`,\n role: 'user',\n author: {\n id: this.app?.activeUser?.id,\n name: this.app?.activeUser?.get('display_name') || 'You'\n },\n content: text,\n timestamp: new Date().toISOString()\n };\n this.chatView.addMessage(userMsg);\n\n // Show thinking immediately — don't wait for server event\n this.chatView.showThinking('Thinking...');\n this._setInputEnabled(false, 'Waiting for response…');\n\n // Send via WebSocket or REST fallback\n if (this.ws && this.ws.isConnected) {\n this.ws.send({\n type: 'assistant_message',\n message: text,\n conversation_id: this.adapter.conversationId\n });\n } else {\n try {\n const resp = await this.app.rest.post('/api/assistant', {\n message: text,\n conversation_id: this.adapter.conversationId\n });\n const respData = resp?.data?.data || resp?.data || resp;\n if (respData.response) {\n this.chatView.addMessage(this.adapter._transformMessage({\n id: respData.message_id || `resp-${++this._messageIdCounter}`,\n role: 'assistant',\n content: respData.response,\n blocks: respData.blocks || [],\n created: new Date().toISOString()\n }));\n }\n this._setInputEnabled(true);\n } catch (_err) {\n this.app?.toast?.error('Failed to send message');\n this._setInputEnabled(true);\n }\n }\n }\n\n onActionStop() {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n }\n\n _setInputEnabled(enabled, reason) {\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n const sendBtn = this.element?.querySelector('[data-ref=\"send-btn\"]');\n const stopBtn = this.element?.querySelector('[data-ref=\"stop-btn\"]');\n\n if (textarea) textarea.disabled = !enabled;\n if (sendBtn) sendBtn.classList.toggle('d-none', !enabled);\n if (stopBtn) stopBtn.classList.toggle('d-none', enabled);\n\n // Show/hide status bar with reason\n const statusEl = this.element?.querySelector('[data-ref=\"input-status\"]');\n if (statusEl) {\n if (!enabled && reason) {\n const esc = (t) => { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; };\n statusEl.innerHTML = `${esc(reason)} <span class=\"assistant-input-status-dismiss\">Click to dismiss</span>`;\n statusEl.classList.remove('d-none');\n if (!statusEl._hasDismiss) {\n statusEl._hasDismiss = true;\n statusEl.addEventListener('click', () => {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n const ta = this.element?.querySelector('[data-ref=\"input\"]');\n if (ta) ta.focus();\n });\n }\n } else {\n statusEl.classList.add('d-none');\n statusEl.innerHTML = '';\n }\n }\n\n if (this._responseTimeout) clearTimeout(this._responseTimeout);\n if (!enabled) {\n this._responseTimeout = setTimeout(() => {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this.app?.toast?.warning('Request timed out');\n }, 60000);\n }\n }\n\n // ── WebSocket ────────────────────────────────────────────\n\n _subscribeWS() {\n if (!this.ws) return;\n\n this._wsHandlers = {\n thinking: (data) => this._onThinking(data),\n text: (data) => this._onText(data),\n tool_call: (data) => this._onToolCall(data),\n response: (data) => this._onResponse(data),\n error: (data) => this._onError(data),\n plan: (data) => this._onPlan(data),\n plan_update: (data) => this._onPlanUpdate(data),\n message: (envelope) => this._dispatchWSMessage(envelope),\n connected: () => this._updateConnectionStatus(),\n disconnected: () => this._updateConnectionStatus()\n };\n\n this.ws.on('message:assistant_thinking', this._wsHandlers.thinking);\n this.ws.on('message:assistant_text', this._wsHandlers.text);\n this.ws.on('message:assistant_tool_call', this._wsHandlers.tool_call);\n this.ws.on('message:assistant_response', this._wsHandlers.response);\n this.ws.on('message:assistant_error', this._wsHandlers.error);\n this.ws.on('message:assistant_plan', this._wsHandlers.plan);\n this.ws.on('message:assistant_plan_update', this._wsHandlers.plan_update);\n this.ws.on('message:message', this._wsHandlers.message);\n this.ws.on('connected', this._wsHandlers.connected);\n this.ws.on('disconnected', this._wsHandlers.disconnected);\n }\n\n _unsubscribeWS() {\n if (!this.ws || !this._wsHandlers) return;\n\n this.ws.off('message:assistant_thinking', this._wsHandlers.thinking);\n this.ws.off('message:assistant_text', this._wsHandlers.text);\n this.ws.off('message:assistant_tool_call', this._wsHandlers.tool_call);\n this.ws.off('message:assistant_response', this._wsHandlers.response);\n this.ws.off('message:assistant_error', this._wsHandlers.error);\n this.ws.off('message:assistant_plan', this._wsHandlers.plan);\n this.ws.off('message:assistant_plan_update', this._wsHandlers.plan_update);\n this.ws.off('message:message', this._wsHandlers.message);\n this.ws.off('connected', this._wsHandlers.connected);\n this.ws.off('disconnected', this._wsHandlers.disconnected);\n this._wsHandlers = {};\n }\n\n _dispatchWSMessage(envelope) {\n const inner = envelope?.data;\n if (!inner?.type) return;\n\n switch (inner.type) {\n case 'assistant_thinking': this._onThinking(inner); break;\n case 'assistant_text': this._onText(inner); break;\n case 'assistant_tool_call': this._onToolCall(inner); break;\n case 'assistant_response': this._onResponse(inner); break;\n case 'assistant_error': this._onError(inner); break;\n case 'assistant_plan': this._onPlan(inner); break;\n case 'assistant_plan_update': this._onPlanUpdate(inner); break;\n }\n }\n\n _isMyConversation(data) {\n if (!data.conversation_id) return true;\n if (!this.adapter.conversationId) return true;\n return String(data.conversation_id) === String(this.adapter.conversationId);\n }\n\n /**\n * Adopt a conversation ID from a WS event if the adapter doesn't have one\n * yet. Mirrors the helper present in AssistantView / AssistantPanelView so\n * all three views handle new-conversation events uniformly. The adapter\n * remains the canonical source of truth for `conversationId`.\n * @private\n */\n _adoptConversationId(data) {\n if (data.conversation_id && this.adapter && !this.adapter.conversationId) {\n this.adapter.conversationId = data.conversation_id;\n }\n }\n\n _onThinking(data) {\n if (!this._isMyConversation(data)) return;\n this.chatView.showThinking('Thinking...');\n this._setInputEnabled(false, 'Assistant is thinking…');\n }\n\n /**\n * Intermediate prose alongside tool calls in the same turn. Renders as an\n * assistant bubble, but does NOT clear the thinking indicator or re-enable\n * input — `assistant_response` remains the terminal signal for the turn.\n * @private\n */\n _onText(data) {\n if (!this._isMyConversation(data)) return;\n this._adoptConversationId(data);\n this._resetResponseTimeout();\n\n const msg = this.adapter._transformMessage({\n id: data.message_id || `text-${++this._messageIdCounter}`,\n role: 'assistant',\n content: data.text || '',\n blocks: data.blocks || [],\n tool_calls: [],\n created: data.created || data.timestamp || new Date().toISOString()\n });\n\n if (msg && (msg.content || msg.blocks?.length)) {\n this.chatView.addMessage(msg);\n }\n }\n\n _onToolCall(data) {\n if (!this._isMyConversation(data)) return;\n this.chatView.showThinking(`Using ${data.tool || data.name || 'tool'}...`);\n this._resetResponseTimeout();\n }\n\n /**\n * Reset the safety timeout without changing input enabled state.\n * @private\n */\n _resetResponseTimeout() {\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = setTimeout(() => {\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n this.app?.toast?.warning('Request timed out');\n }, 60000);\n }\n }\n\n _onResponse(data) {\n if (!this._isMyConversation(data)) return;\n\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n if (textarea) textarea.focus();\n\n const msg = this.adapter._transformMessage({\n id: data.message_id || `resp-${++this._messageIdCounter}`,\n role: 'assistant',\n content: data.response || data.content || data.message || '',\n blocks: data.blocks || [],\n tool_calls: data.tool_calls_made || data.tool_calls || [],\n created: data.created || data.timestamp || new Date().toISOString()\n });\n\n // Skip empty messages (e.g. plan-only responses where all tool calls are internal)\n if (msg && (msg.content || msg.blocks?.length || msg.tool_calls?.length)) {\n this.chatView.addMessage(msg);\n }\n }\n\n _onError(data) {\n if (!this._isMyConversation(data)) return;\n\n this.chatView.hideThinking();\n this._setInputEnabled(true);\n\n console.error('[AssistantContextChat] WS error:', data.error || data.message);\n this.app?.toast?.error('The assistant encountered an error. Please try again.');\n }\n\n _onPlan(data) {\n if (!this._isMyConversation(data)) return;\n\n const plan = data.plan;\n if (!plan) return;\n\n this._activePlans[plan.plan_id] = plan;\n\n this.chatView.addMessage({\n id: `plan-${plan.plan_id}`,\n role: 'assistant',\n author: { name: 'Assistant' },\n content: '',\n timestamp: new Date().toISOString(),\n blocks: [{ type: 'progress', ...plan }],\n tool_calls: [],\n _conversationId: this.adapter.conversationId\n });\n }\n\n _onPlanUpdate(data) {\n if (!this._isMyConversation(data)) return;\n\n const plan = this._activePlans[data.plan_id];\n if (plan) {\n const step = plan.steps.find(s => s.id === data.step_id);\n if (step) {\n step.status = data.status;\n step.summary = data.summary;\n }\n }\n\n const msgView = this.chatView.messageViews.get(`plan-${data.plan_id}`);\n if (msgView?.updateProgressStep) {\n msgView.updateProgressStep(data.plan_id, data.step_id, data.status, data.summary);\n }\n this._resetResponseTimeout();\n }\n\n _updateConnectionStatus() {\n const dot = this.element?.querySelector('.status-dot');\n if (!dot) return;\n\n if (this.ws?.isConnected) {\n dot.style.background = '#198754';\n dot.title = 'Connected';\n // Re-enable input on reconnect if no pending request\n if (!this._responseTimeout) {\n this._setInputEnabled(true);\n } else {\n this._setInputEnabled(false, 'Waiting for response…');\n }\n } else {\n dot.style.background = '#dc3545';\n dot.title = 'Disconnected';\n // Disable input visually but don't start response timeout\n const textarea = this.element?.querySelector('[data-ref=\"input\"]');\n const sendBtn = this.element?.querySelector('[data-ref=\"send-btn\"]');\n if (textarea) textarea.disabled = true;\n if (sendBtn) sendBtn.classList.add('d-none');\n const statusEl = this.element?.querySelector('[data-ref=\"input-status\"]');\n if (statusEl) {\n statusEl.textContent = 'Disconnected — reconnecting…';\n statusEl.classList.remove('d-none');\n }\n }\n }\n\n async onBeforeDestroy() {\n this._unsubscribeWS();\n if (this._responseTimeout) {\n clearTimeout(this._responseTimeout);\n this._responseTimeout = null;\n }\n }\n}\n\n\n// ── Helper ─────────────────────────────────────────────────\n\n/**\n * Open a context-scoped assistant chat for any model view.\n *\n * @param {View} view - The calling view (must have this.model with metadata)\n * @param {string} modelName - Backend model name, e.g. 'incident.Ticket'\n */\nasync function openAssistantChat(view, modelName) {\n const app = view.getApp();\n if (!app) return;\n\n const model = view.model;\n const pk = model.get('id');\n const metadata = model.get('metadata') || {};\n const conversationId = metadata.assistant_conversation_id || null;\n\n const adapter = new AssistantContextAdapter({\n app,\n modelName,\n pk,\n conversationId\n });\n\n // Persist conversation_id on first creation\n adapter._onConversationCreated = async (newConversationId) => {\n try {\n await model.save({ metadata: { assistant_conversation_id: newConversationId } });\n } catch (_err) {\n // Non-fatal — chat still works, just won't resume next time\n }\n };\n\n const chatView = new AssistantContextChat({\n app,\n adapter\n });\n\n await Modal.show(chatView, {\n title: 'AI Assistant',\n size: 'xl'\n });\n}\n\n\nexport { AssistantContextAdapter, AssistantContextChat, openAssistantChat };\nexport default AssistantContextChat;\n","/**\n * IncidentView - Rich security operations view for an Incident record\n *\n * SideNavView layout for security investigation workflow:\n * Overview (summary + GeoIP + quick actions), Events, Rule Engine,\n * Tickets, History, Forensics (Stack Trace, Metadata)\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport StackTraceView from '@core/views/data/StackTraceView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Incident, IncidentForms, IncidentList, IncidentEventList, RuleSet, RuleSetForms, Rule, RuleList, BundleByOptions, MatchByOptions } from '@ext/admin/models/Incident.js';\nimport { GeoLocatedIP } from '@core/models/System.js';\nimport { Ticket, TicketList, TicketForms } from '@ext/admin/models/Tickets.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport GeoIPView from '../account/devices/GeoIPView.js';\nimport RuleSetView from './RuleSetView.js';\nimport IncidentHistoryAdapter from './adapters/IncidentHistoryAdapter.js';\nimport ChatView from '@core/views/chat/ChatView.js';\nimport { openAssistantChat } from '../assistant/AssistantContextChat.js';\n\n\n// ── Module Helpers ──────────────────────────────────────────\n\n/** Infer Rule value_type from a JS value */\nfunction _inferValueType(val) {\n if (typeof val === 'boolean') return 'bool';\n if (typeof val === 'number') return Number.isInteger(val) ? 'int' : 'float';\n const num = Number(val);\n if (val !== '' && !isNaN(num)) return Number.isInteger(num) ? 'int' : 'float';\n return 'str';\n}\n\nfunction _escapeHtml(str) {\n return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, '"');\n}\n\nfunction _truncate(str, max) {\n return str.length > max ? str.slice(0, max) + '…' : str;\n}\n\n/** Render markdown to HTML via the docit API, with plain-text fallback */\nasync function _renderMarkdown(view, markdown) {\n if (!markdown) return '';\n try {\n const app = view.getApp();\n const resp = await app.rest.post('/api/docit/render', { markdown });\n const html = resp?.data?.data?.html || resp?.data?.html;\n if (html) return html;\n } catch (_e) { /* API unavailable — fall through */ }\n // Fallback: escape and preserve whitespace\n return `<pre style=\"white-space: pre-wrap;\">${_escapeHtml(markdown)}</pre>`;\n}\n\n// ── Status & Priority Helpers ──────────────────────────────\n\nconst STATUS_CONFIG = {\n new: { badge: 'bg-info', icon: 'bi-bell-fill', label: 'New', help: 'Unhandled — needs triage by human or LLM agent' },\n open: { badge: 'bg-primary', icon: 'bi-folder2-open', label: 'Open', help: 'Claimed by an operator for investigation' },\n investigating: { badge: 'bg-warning text-dark', icon: 'bi-search', label: 'Investigating', help: 'Actively being investigated (human or LLM)' },\n paused: { badge: 'bg-secondary', icon: 'bi-pause-circle-fill', label: 'Paused', help: 'On hold — waiting for external input' },\n resolved: { badge: 'bg-success', icon: 'bi-check-circle-fill', label: 'Resolved', help: 'Root cause addressed, no further action needed' },\n closed: { badge: 'bg-dark', icon: 'bi-x-circle-fill', label: 'Closed', help: 'Closed — archived for record keeping' },\n ignored: { badge: 'bg-secondary', icon: 'bi-eye-slash-fill', label: 'Ignored', help: 'Noise — review periodically to tune rules' },\n pending: { badge: 'bg-light text-dark border', icon: 'bi-hourglass-split', label: 'Pending', help: 'Below trigger threshold — accumulating events' },\n};\n\nfunction getStatusConfig(status) {\n return STATUS_CONFIG[(status || '').toLowerCase()] || STATUS_CONFIG.new;\n}\n\nfunction getPriorityConfig(priority) {\n const p = parseInt(priority) || 5;\n if (p >= 9) return { color: 'text-white', bg: 'bg-danger', label: 'Critical' };\n if (p >= 7) return { color: 'text-white', bg: 'bg-danger', label: 'High' };\n if (p >= 5) return { color: 'text-dark', bg: 'bg-warning', label: 'Medium' };\n if (p >= 3) return { color: 'text-white', bg: 'bg-info', label: 'Low' };\n return { color: 'text-white', bg: 'bg-secondary', label: 'Info' };\n}\n\nfunction getHeaderIcon(status) {\n const s = (status || '').toLowerCase();\n if (s === 'resolved' || s === 'closed') return { icon: 'bi-check-circle-fill', color: 'text-success' };\n if (s === 'open' || s === 'investigating') return { icon: 'bi-exclamation-triangle-fill', color: 'text-danger' };\n if (s === 'new') return { icon: 'bi-bell-fill', color: 'text-info' };\n if (s === 'paused' || s === 'ignored') return { icon: 'bi-pause-circle-fill', color: 'text-warning' };\n return { icon: 'bi-shield-exclamation', color: 'text-secondary' };\n}\n\n\n// ── GeoIP Summary Card (Enhanced) ──────────────────────────\n\nclass GeoIPSummaryCard extends View {\n constructor(options = {}) {\n super({\n className: 'geoip-summary-card',\n ...options\n });\n\n this.sourceIP = options.sourceIP;\n this.ipInfo = options.ipInfo || null;\n this.geoData = null;\n this.threatBadgeClass = 'bg-secondary';\n this.threatLevel = 'Unknown';\n this.isBlocked = false;\n this.isWhitelisted = false;\n this.blockedReason = '';\n this.geoModel = null;\n\n this.template = `\n {{#geoData}}\n <div class=\"card shadow-sm\">\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between align-items-start\">\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"text-primary\">\n <i class=\"bi bi-globe-americas fs-3\"></i>\n </div>\n <div>\n <div class=\"mb-1\">\n <a role=\"button\" class=\"fw-semibold font-monospace text-decoration-none\" data-action=\"view-geoip\">{{sourceIP}}</a>\n {{#isBlocked|bool}}\n <span class=\"badge bg-danger ms-2\" data-bs-toggle=\"tooltip\" title=\"{{blockedReason}}\"><i class=\"bi bi-slash-circle me-1\"></i>Blocked</span>\n {{/isBlocked|bool}}\n {{#isWhitelisted|bool}}\n <span class=\"badge bg-success ms-2\"><i class=\"bi bi-check-circle me-1\"></i>Whitelisted</span>\n {{/isWhitelisted|bool}}\n </div>\n <div class=\"text-muted small\">\n {{geoData.city|default('Unknown')}}, {{geoData.country_name|default('Unknown')}}\n {{#geoData.country_code}}\n <span class=\"text-muted\">({{geoData.country_code}})</span>\n {{/geoData.country_code}}\n </div>\n <div class=\"text-muted small\">\n {{geoData.isp|default('Unknown ISP')}}\n {{#geoData.asn}} · {{geoData.asn}}{{/geoData.asn}}\n {{#geoData.connection_type}} · {{geoData.connection_type}}{{/geoData.connection_type}}\n </div>\n </div>\n </div>\n <div class=\"text-end\">\n <span class=\"badge {{threatBadgeClass}}\">{{threatLevel}}</span>\n {{#geoData.risk_score}}\n <div class=\"text-muted small mt-1\">Risk Score: {{geoData.risk_score}}</div>\n {{/geoData.risk_score}}\n <div class=\"d-flex gap-1 mt-1 justify-content-end\">\n {{#geoData.is_tor|bool}}<span class=\"badge bg-danger-subtle text-danger\" title=\"TOR Exit Node\">TOR</span>{{/geoData.is_tor|bool}}\n {{#geoData.is_vpn|bool}}<span class=\"badge bg-warning-subtle text-warning\" title=\"VPN Detected\">VPN</span>{{/geoData.is_vpn|bool}}\n {{#geoData.is_proxy|bool}}<span class=\"badge bg-info-subtle text-info\" title=\"Proxy\">Proxy</span>{{/geoData.is_proxy|bool}}\n {{#geoData.is_datacenter|bool}}<span class=\"badge bg-secondary-subtle text-secondary\" title=\"Datacenter IP\">DC</span>{{/geoData.is_datacenter|bool}}\n {{#geoData.is_known_attacker|bool}}<span class=\"badge bg-danger\" title=\"Known Attacker\">Attacker</span>{{/geoData.is_known_attacker|bool}}\n {{#geoData.is_known_abuser|bool}}<span class=\"badge bg-danger-subtle text-danger\" title=\"Known Abuser\">Abuser</span>{{/geoData.is_known_abuser|bool}}\n </div>\n </div>\n </div>\n <!-- Inline actions -->\n <div class=\"mt-3 pt-2 border-top d-flex gap-2\">\n {{^isBlocked|bool}}\n <button class=\"btn btn-outline-danger btn-sm\" data-action=\"block-ip\">\n <i class=\"bi bi-slash-circle me-1\"></i>Block IP\n </button>\n {{/isBlocked|bool}}\n {{#isBlocked|bool}}\n <button class=\"btn btn-outline-success btn-sm\" data-action=\"unblock-ip\">\n <i class=\"bi bi-unlock me-1\"></i>Unblock IP\n </button>\n {{/isBlocked|bool}}\n {{^isWhitelisted|bool}}\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"whitelist-ip\">\n <i class=\"bi bi-check-circle me-1\"></i>Whitelist\n </button>\n {{/isWhitelisted|bool}}\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"view-geoip\">\n <i class=\"bi bi-box-arrow-up-right me-1\"></i>Full GeoIP Record\n </button>\n </div>\n </div>\n </div>\n {{/geoData}}\n {{^geoData}}\n <div class=\"card shadow-sm\">\n <div class=\"card-body text-muted text-center py-3\">\n <i class=\"bi bi-globe me-2\"></i>No GeoIP data available for {{sourceIP}}\n </div>\n </div>\n {{/geoData}}\n `;\n }\n\n async onInit() {\n if (!this.sourceIP) return;\n\n // Use pre-fetched ip_info if available, otherwise fall back to API lookup\n if (this.ipInfo) {\n this.geoData = this.ipInfo;\n this.geoModel = new GeoLocatedIP(this.ipInfo);\n } else {\n try {\n this.geoModel = await GeoLocatedIP.lookup(this.sourceIP);\n if (this.geoModel) {\n this.geoData = this.geoModel.attributes;\n }\n } catch (e) {\n // GeoIP lookup failed — card shows \"no data\" state\n return;\n }\n }\n\n if (this.geoData) {\n this.threatLevel = (this.geoData.threat_level || 'unknown').toUpperCase();\n this.threatBadgeClass = this._getThreatBadgeClass(this.geoData.threat_level);\n this.isBlocked = !!this.geoData.is_blocked;\n this.isWhitelisted = !!this.geoData.is_whitelisted;\n this.blockedReason = this.geoData.blocked_reason || 'Blocked';\n }\n }\n\n _getThreatBadgeClass(level) {\n const l = (level || '').toLowerCase();\n if (l === 'high' || l === 'critical') return 'bg-danger';\n if (l === 'medium') return 'bg-warning text-dark';\n if (l === 'low') return 'bg-success';\n return 'bg-secondary';\n }\n\n async onActionViewGeoip() {\n await GeoIPView.show(this.sourceIP);\n }\n\n async onActionBlockIp() {\n const data = await Modal.form({\n title: `Block IP — ${this.sourceIP}`,\n icon: 'bi-slash-circle',\n size: 'sm',\n fields: [\n { name: 'reason', type: 'text', label: 'Reason', required: true, placeholder: 'e.g., Suspicious activity from incident' },\n {\n name: 'ttl', type: 'select', label: 'Duration',\n options: [\n { value: 3600, label: '1 hour' },\n { value: 21600, label: '6 hours' },\n { value: 86400, label: '24 hours' },\n { value: 604800, label: '7 days' },\n { value: 2592000, label: '30 days' },\n { value: 0, label: 'Permanent' }\n ],\n value: 86400\n }\n ]\n });\n if (!data) return true;\n\n if (!this.geoModel) return true;\n const resp = await this.geoModel.save({\n block: { reason: data.reason, ttl: parseInt(data.ttl) }\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success(`IP ${this.sourceIP} blocked`);\n await this.geoModel.fetch();\n this.geoData = this.geoModel.attributes;\n this.isBlocked = true;\n this.blockedReason = data.reason;\n await this.render();\n } else {\n this.getApp().toast.error('Failed to block IP');\n }\n return true;\n }\n\n async onActionUnblockIp() {\n const data = await Modal.form({\n title: `Unblock IP — ${this.sourceIP}`,\n icon: 'bi-unlock',\n size: 'sm',\n fields: [\n { name: 'reason', type: 'text', label: 'Reason', placeholder: 'e.g., False positive' }\n ]\n });\n if (!data) return true;\n\n if (!this.geoModel) return true;\n const resp = await this.geoModel.save({\n unblock: data.reason || 'Unblocked from incident view'\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success(`IP ${this.sourceIP} unblocked`);\n await this.geoModel.fetch();\n this.geoData = this.geoModel.attributes;\n this.isBlocked = false;\n await this.render();\n } else {\n this.getApp().toast.error('Failed to unblock IP');\n }\n return true;\n }\n\n async onActionWhitelistIp() {\n const data = await Modal.form({\n title: `Whitelist IP — ${this.sourceIP}`,\n icon: 'bi-check-circle',\n size: 'sm',\n fields: [\n { name: 'reason', type: 'text', label: 'Reason', required: true, placeholder: 'e.g., Known office IP' }\n ]\n });\n if (!data) return true;\n\n if (!this.geoModel) return true;\n const resp = await this.geoModel.save({\n whitelist: data.reason\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success(`IP ${this.sourceIP} whitelisted`);\n await this.geoModel.fetch();\n this.geoData = this.geoModel.attributes;\n this.isWhitelisted = true;\n this.isBlocked = false;\n await this.render();\n } else {\n this.getApp().toast.error('Failed to whitelist IP');\n }\n return true;\n }\n}\n\n\n// ── Quick Actions Bar ───────────────────────────────────────\n\nclass QuickActionsBar extends View {\n constructor(options = {}) {\n super({\n className: 'quick-actions-bar mb-3',\n ...options\n });\n\n this.incident = options.incident;\n const status = (this.incident.get('status') || '').toLowerCase();\n this.isActive = ['new', 'open', 'investigating'].includes(status);\n this.isResolved = ['resolved', 'closed'].includes(status);\n this.isProtected = !!(this.incident.get('metadata')?.do_not_delete);\n\n this.template = `\n <div class=\"d-flex align-items-center justify-content-between flex-wrap gap-2\">\n <div class=\"d-flex align-items-center gap-2\">\n {{#isActive|bool}}\n <button class=\"btn btn-success btn-sm\" data-action=\"quick-resolve\" data-bs-toggle=\"tooltip\" title=\"Mark this incident as resolved\">\n <i class=\"bi bi-check-circle me-1\"></i>Resolve\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"quick-pause\" data-bs-toggle=\"tooltip\" title=\"Put on hold\">\n <i class=\"bi bi-pause-circle\"></i>\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"quick-ignore\" data-bs-toggle=\"tooltip\" title=\"Mark as noise\">\n <i class=\"bi bi-eye-slash\"></i>\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"quick-escalate\" data-bs-toggle=\"tooltip\" title=\"Escalate priority\">\n <i class=\"bi bi-arrow-up-circle\"></i>\n </button>\n {{/isActive|bool}}\n {{#isResolved|bool}}\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"quick-reopen\" data-bs-toggle=\"tooltip\" title=\"Re-open for further investigation\">\n <i class=\"bi bi-arrow-counterclockwise me-1\"></i>Re-open\n </button>\n {{/isResolved|bool}}\n </div>\n <div class=\"d-flex align-items-center gap-2\">\n {{#isProtected|bool}}\n <button class=\"btn btn-warning btn-sm\" data-action=\"quick-unprotect\" data-bs-toggle=\"tooltip\" title=\"Remove deletion protection\">\n <i class=\"bi bi-shield-fill-check me-1\"></i>Protected\n </button>\n {{/isProtected|bool}}\n {{^isProtected|bool}}\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"quick-protect\" data-bs-toggle=\"tooltip\" title=\"Protect from auto-deletion\">\n <i class=\"bi bi-shield me-1\"></i>Protect\n </button>\n {{/isProtected|bool}}\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"quick-create-ticket\" data-bs-toggle=\"tooltip\" title=\"Create a review ticket\">\n <i class=\"bi bi-ticket-perforated me-1\"></i>Ticket\n </button>\n <button class=\"btn btn-outline-dark btn-sm\" data-action=\"quick-analyze-llm\" data-bs-toggle=\"tooltip\" title=\"LLM agent reviews events, merges related incidents, and proposes rules\">\n <i class=\"bi bi-robot me-1\"></i>LLM Analyze\n </button>\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"quick-ask-ai\" data-bs-toggle=\"tooltip\" title=\"Chat with AI about this incident\">\n <i class=\"bi bi-chat-dots me-1\"></i>Ask AI\n </button>\n </div>\n </div>\n `;\n }\n\n async onActionQuickResolve() {\n await this.incident.save({ status: 'resolved' });\n this.getApp()?.toast?.success('Incident resolved');\n this.emit('incident:updated');\n }\n\n async onActionQuickPause() {\n await this.incident.save({ status: 'paused' });\n this.getApp()?.toast?.success('Incident paused');\n this.emit('incident:updated');\n }\n\n async onActionQuickIgnore() {\n await this.incident.save({ status: 'ignored' });\n this.getApp()?.toast?.success('Incident ignored');\n this.emit('incident:updated');\n }\n\n async onActionQuickReopen() {\n await this.incident.save({ status: 'open' });\n this.getApp()?.toast?.success('Incident re-opened');\n this.emit('incident:updated');\n }\n\n async onActionQuickCreateTicket() {\n this.emit('create-ticket');\n }\n\n async onActionQuickEscalate() {\n const current = parseInt(this.incident.get('priority')) || 5;\n const newPriority = Math.min(current + 2, 10);\n await this.incident.save({ priority: newPriority });\n this.getApp()?.toast?.success(`Priority escalated to ${newPriority}`);\n this.emit('incident:updated');\n }\n\n async onActionQuickAnalyzeLlm() {\n this.emit('analyze-llm');\n }\n\n async onActionQuickAskAi() {\n this.emit('ask-ai');\n }\n\n async onActionQuickProtect() {\n await this.incident.save({ metadata: { do_not_delete: true } });\n this.getApp()?.toast?.success('Incident protected from deletion');\n this.emit('incident:updated');\n }\n\n async onActionQuickUnprotect() {\n await this.incident.save({ metadata: { do_not_delete: false } });\n this.getApp()?.toast?.success('Deletion protection removed');\n this.emit('incident:updated');\n }\n}\n\n\n// ── LLM Analysis Results ────────────────────────────────\n\nclass LLMAnalysisResultsView extends View {\n constructor(options = {}) {\n super({\n className: 'llm-analysis-results mb-3',\n ...options\n });\n\n this.analysis = options.analysis || {};\n this.incident = options.incident;\n this.summary = this.analysis.summary || '';\n this.summaryHtml = '';\n this.hasProposedRule = !!(this.analysis.proposed_ruleset_id);\n this.proposedRulesetId = this.analysis.proposed_ruleset_id;\n this.mergedCount = (this.analysis.merged_incidents || []).length;\n this.mergedIds = (this.analysis.merged_incidents || []).join(', ');\n\n this.template = `\n <div class=\"card border-info shadow-sm\">\n <div class=\"card-header bg-info bg-opacity-10 d-flex align-items-center justify-content-between\">\n <div>\n <i class=\"bi bi-robot me-2 text-info\"></i>\n <strong>LLM Analysis</strong>\n <span class=\"text-muted small ms-2\">AI-generated triage</span>\n </div>\n <button class=\"btn btn-outline-info btn-sm\" data-action=\"re-analyze\" data-bs-toggle=\"tooltip\" title=\"Run a fresh LLM analysis\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Re-analyze\n </button>\n </div>\n <div class=\"card-body\">\n {{#summary}}\n <div class=\"mb-3\">\n <h6 class=\"text-muted mb-2\"><i class=\"bi bi-chat-left-text me-1\"></i>Summary</h6>\n <div class=\"bg-light rounded p-3 small llm-summary-content\">{{{summaryHtml}}}</div>\n </div>\n {{/summary}}\n {{#mergedCount}}\n <div class=\"mb-3\">\n <span class=\"badge bg-primary\"><i class=\"bi bi-union me-1\"></i>{{mergedCount}} incident(s) merged</span>\n <span class=\"text-muted small ms-2\">IDs: {{mergedIds}}</span>\n </div>\n {{/mergedCount}}\n {{#hasProposedRule|bool}}\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"badge bg-success\"><i class=\"bi bi-gear me-1\"></i>Proposed Rule</span>\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"view-proposed-rule\">\n <i class=\"bi bi-box-arrow-up-right me-1\"></i>View Proposed RuleSet #{{proposedRulesetId}}\n </button>\n <span class=\"text-muted small\">(created disabled — review before enabling)</span>\n </div>\n {{/hasProposedRule|bool}}\n </div>\n </div>\n `;\n }\n\n async onBeforeRender() {\n if (this.summary && !this.summaryHtml) {\n this.summaryHtml = await _renderMarkdown(this, this.summary);\n }\n }\n\n async onActionReAnalyze() {\n this.emit('analyze-llm');\n }\n\n async onActionViewProposedRule() {\n if (!this.proposedRulesetId) return;\n try {\n const ruleset = new RuleSet({ id: this.proposedRulesetId });\n await ruleset.fetch();\n const view = new RuleSetView({ model: ruleset });\n await Modal.show(view, { size: 'xl', header: false });\n } catch (e) {\n this.getApp()?.toast?.error('Could not load proposed RuleSet');\n }\n }\n}\n\n\n// ── Overview Section ─────────────────────────────────────\n\nclass IncidentOverviewSection extends View {\n constructor(options = {}) {\n super({\n className: 'incident-overview-section p-3',\n template: `\n <div data-container=\"quick-actions\" class=\"mb-3\"></div>\n <div data-container=\"llm-analysis-results\"></div>\n <div data-container=\"overview-data\" class=\"mb-3\"></div>\n <div data-container=\"geoip-summary\"></div>\n {{#serverInfo}}\n <div class=\"text-muted small mt-2\"><i class=\"bi bi-hdd-rack me-1\"></i>{{serverInfo}}</div>\n {{/serverInfo}}\n `,\n ...options\n });\n }\n\n async onInit() {\n // Quick actions bar\n this.quickActions = new QuickActionsBar({\n containerId: 'quick-actions',\n incident: this.model\n });\n this.quickActions.on('incident:updated', () => this.emit('incident:updated'));\n this.quickActions.on('create-ticket', () => this.emit('create-ticket'));\n this.quickActions.on('analyze-llm', () => this.emit('analyze-llm'));\n this.quickActions.on('ask-ai', () => this.emit('ask-ai'));\n this.addChild(this.quickActions);\n\n // LLM Analysis results (if available)\n this._showLlmAnalysisIfAvailable();\n\n // Core incident fields\n const statusCfg = getStatusConfig(this.model.get('status'));\n const priorityCfg = getPriorityConfig(this.model.get('priority'));\n this.statusHelp = statusCfg.help;\n this.priorityHelp = priorityCfg.label;\n\n // Show details separately only if different from title\n const title = this.model.get('title') || '';\n const details = this.model.get('details') || '';\n const showDetails = details && details !== title;\n\n const fields = [\n { name: 'status', label: 'Status', formatter: 'badge', cols: 4 },\n { name: 'priority', label: 'Priority', cols: 4 },\n { name: 'category', label: 'Category', formatter: 'badge', cols: 4 },\n { name: 'event_count', label: 'Events', cols: 4 },\n { name: 'scope', label: 'Scope', cols: 4 },\n { name: 'hostname', label: 'Hostname', cols: 4 },\n { name: 'created', label: 'Created', formatter: 'epoch|datetime', cols: 4 },\n { name: 'modified', label: 'Last Updated', formatter: 'epoch|datetime', cols: 4 },\n { name: 'title', label: 'Title', cols: 12 },\n ];\n if (showDetails) {\n fields.push({ name: 'details', label: 'Details', cols: 12 });\n }\n // Show model reference only if present\n if (this.model.get('model_name')) {\n fields.push({ name: 'model_name', label: 'Related Model', cols: 6 });\n fields.push({ name: 'model_id', label: 'Model ID', cols: 6 });\n }\n\n this.dataView = new DataView({\n containerId: 'overview-data',\n model: this.model,\n columns: 2,\n showEmptyValues: false,\n fields\n });\n this.addChild(this.dataView);\n\n // Server/environment info from metadata\n const metadata = this.model.get('metadata') || {};\n const serverParts = [];\n if (metadata.server) serverParts.push(metadata.server);\n if (metadata.timezone) serverParts.push(metadata.timezone);\n this.serverInfo = serverParts.length ? serverParts.join(' · ') : null;\n\n // GeoIP summary — pass ip_info to avoid redundant API call\n const sourceIP = await this._resolveSourceIP();\n if (sourceIP) {\n this.geoipCard = new GeoIPSummaryCard({\n containerId: 'geoip-summary',\n sourceIP,\n ipInfo: this.model.get('ip_info')\n });\n this.addChild(this.geoipCard);\n }\n }\n\n _showLlmAnalysisIfAvailable() {\n const metadata = this.model.get('metadata') || {};\n const llmAnalysis = metadata.llm_analysis;\n if (llmAnalysis && !this.llmResultsView) {\n this.llmResultsView = new LLMAnalysisResultsView({\n containerId: 'llm-analysis-results',\n analysis: llmAnalysis,\n incident: this.model\n });\n this.llmResultsView.on('analyze-llm', () => this.emit('analyze-llm'));\n this.addChild(this.llmResultsView);\n }\n }\n\n /** Called after polling completes to show newly available LLM results */\n refreshAnalysis() {\n if (this.llmResultsView) {\n this.removeChild(this.llmResultsView);\n this.llmResultsView = null;\n }\n this._showLlmAnalysisIfAvailable();\n this.render();\n }\n\n async _resolveSourceIP() {\n const metadata = this.model.get('metadata') || {};\n if (metadata.source_ip) return metadata.source_ip;\n if (metadata.ip) return metadata.ip;\n if (metadata.ip_address) return metadata.ip_address;\n\n try {\n const events = new IncidentEventList({\n params: { incident: this.model.get('id'), size: 1, sort: '-created' }\n });\n await events.fetch();\n const firstEvent = events.first();\n if (firstEvent) {\n return firstEvent.get('source_ip') || firstEvent.get('ip_address') || null;\n }\n } catch (e) {\n // No IP available\n }\n return null;\n }\n}\n\n\n// ── HTTP Request Section ────────────────────────────────\n\nclass HttpRequestSection extends View {\n constructor(options = {}) {\n super({\n className: 'http-request-section p-3',\n template: `\n <h6 class=\"mb-3\"><i class=\"bi bi-globe2 me-2\"></i>HTTP Request Details</h6>\n <div data-container=\"http-data\"></div>\n `,\n ...options\n });\n this.metadata = options.metadata || {};\n }\n\n async onInit() {\n const m = this.metadata;\n const metaModel = { get: (key) => m[key], attributes: m };\n this.dataView = new DataView({\n containerId: 'http-data',\n model: metaModel,\n columns: 2,\n showEmptyValues: false,\n fields: [\n { name: 'http_method', label: 'Method', formatter: 'badge', cols: 3 },\n { name: 'http_status', label: 'Status Code', cols: 3 },\n { name: 'http_host', label: 'Host', cols: 6 },\n { name: 'http_path', label: 'Path', cols: 12 },\n { name: 'http_url', label: 'URL', cols: 12 },\n { name: 'http_protocol', label: 'Protocol', cols: 6 },\n { name: 'http_query_string', label: 'Query String', cols: 6 },\n { name: 'http_user_agent', label: 'User Agent', cols: 12 },\n ]\n });\n this.addChild(this.dataView);\n }\n}\n\n\n// ── IP Intelligence Section ─────────────────────────────\n\nclass IPIntelligenceSection extends View {\n constructor(options = {}) {\n super({\n className: 'ip-intelligence-section p-3',\n template: `\n <h6 class=\"mb-3\"><i class=\"bi bi-shield-lock me-2\"></i>Network</h6>\n <div data-container=\"ip-network\" class=\"mb-4\"></div>\n <h6 class=\"mb-3\"><i class=\"bi bi-exclamation-triangle me-2\"></i>Threat Assessment</h6>\n <div data-container=\"ip-threat\" class=\"mb-4\"></div>\n <h6 class=\"mb-3\"><i class=\"bi bi-flag me-2\"></i>Threat Flags</h6>\n <div data-container=\"ip-flags\" class=\"mb-4\"></div>\n <h6 class=\"mb-3\"><i class=\"bi bi-slash-circle me-2\"></i>Block Status</h6>\n <div data-container=\"ip-block\"></div>\n `,\n ...options\n });\n this.ipInfo = options.ipInfo || {};\n }\n\n async onInit() {\n const ip = this.ipInfo;\n const ipModel = { get: (key) => ip[key], attributes: ip };\n\n this.networkView = new DataView({\n containerId: 'ip-network',\n model: ipModel,\n columns: 2,\n showEmptyValues: false,\n fields: [\n { name: 'ip_address', label: 'IP Address', cols: 6 },\n { name: 'subnet', label: 'Subnet', cols: 6 },\n { name: 'isp', label: 'ISP', cols: 6 },\n { name: 'asn', label: 'ASN', cols: 3 },\n { name: 'asn_org', label: 'ASN Org', cols: 3 },\n { name: 'mobile_carrier', label: 'Mobile Carrier', cols: 6 },\n { name: 'connection_type', label: 'Connection Type', cols: 6 },\n ]\n });\n this.addChild(this.networkView);\n\n this.threatView = new DataView({\n containerId: 'ip-threat',\n model: ipModel,\n columns: 2,\n showEmptyValues: false,\n fields: [\n { name: 'threat_level', label: 'Threat Level', formatter: 'badge', cols: 4 },\n { name: 'risk_score', label: 'Risk Score', cols: 4 },\n { name: 'is_threat', label: 'Threat', formatter: 'yesnoicon', cols: 2 },\n { name: 'is_suspicious', label: 'Suspicious', formatter: 'yesnoicon', cols: 2 },\n ]\n });\n this.addChild(this.threatView);\n\n this.flagsView = new DataView({\n containerId: 'ip-flags',\n model: ipModel,\n columns: 2,\n showEmptyValues: false,\n fields: [\n { name: 'is_tor', label: 'TOR', formatter: 'yesnoicon', cols: 3 },\n { name: 'is_vpn', label: 'VPN', formatter: 'yesnoicon', cols: 3 },\n { name: 'is_proxy', label: 'Proxy', formatter: 'yesnoicon', cols: 3 },\n { name: 'is_datacenter', label: 'Datacenter', formatter: 'yesnoicon', cols: 3 },\n { name: 'is_mobile', label: 'Mobile', formatter: 'yesnoicon', cols: 3 },\n { name: 'is_cloud', label: 'Cloud', formatter: 'yesnoicon', cols: 3 },\n { name: 'is_known_attacker', label: 'Known Attacker', formatter: 'yesnoicon', cols: 3 },\n { name: 'is_known_abuser', label: 'Known Abuser', formatter: 'yesnoicon', cols: 3 },\n ]\n });\n this.addChild(this.flagsView);\n\n this.blockView = new DataView({\n containerId: 'ip-block',\n model: ipModel,\n columns: 2,\n showEmptyValues: false,\n fields: [\n { name: 'is_blocked', label: 'Blocked', formatter: 'yesnoicon', cols: 3 },\n { name: 'block_count', label: 'Block Count', cols: 3 },\n { name: 'is_whitelisted', label: 'Whitelisted', formatter: 'yesnoicon', cols: 3 },\n { name: 'blocked_reason', label: 'Block Reason', cols: 3 },\n { name: 'blocked_at', label: 'Blocked At', formatter: 'epoch|datetime', cols: 6 },\n { name: 'blocked_until', label: 'Blocked Until', formatter: 'epoch|datetime', cols: 6 },\n { name: 'whitelisted_reason', label: 'Whitelist Reason', cols: 12 },\n ]\n });\n this.addChild(this.blockView);\n }\n}\n\n\n// ── Rule Engine Section ──────────────────────────────────\n\nclass RuleEngineSection extends View {\n constructor(options = {}) {\n super({\n className: 'rule-engine-section p-3',\n ...options\n });\n\n this.incident = options.incident;\n const ruleSet = this.incident.get('rule_set');\n this.rulesetId = ruleSet && typeof ruleSet === 'object' ? ruleSet.id : ruleSet;\n this.rulesetModel = null;\n this.hasRuleset = false;\n this.autoDeleteEnabled = false;\n this.incidentProtected = !!(this.incident.get('metadata')?.do_not_delete);\n\n this.template = `\n {{#hasRuleset|bool}}\n {{#autoDeleteEnabled|bool}}\n {{^incidentProtected|bool}}\n <div class=\"alert alert-warning d-flex align-items-center mb-3\" role=\"alert\">\n <i class=\"bi bi-exclamation-triangle-fill me-2\"></i>\n <div>This incident will be <strong>permanently deleted</strong> when resolved or closed. Events and history will also be removed.</div>\n </div>\n {{/incidentProtected|bool}}\n {{#incidentProtected|bool}}\n <div class=\"alert alert-info d-flex align-items-center mb-3\" role=\"alert\">\n <i class=\"bi bi-shield-fill-check me-2\"></i>\n <div>Auto-delete is enabled on this rule, but this incident is <strong>protected</strong> from deletion.</div>\n </div>\n {{/incidentProtected|bool}}\n {{/autoDeleteEnabled|bool}}\n <div class=\"mb-3\">\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"mb-0\"><i class=\"bi bi-gear-wide-connected me-2\"></i>Linked RuleSet</h6>\n <div class=\"d-flex gap-2\">\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"edit-linked-ruleset\">\n <i class=\"bi bi-pencil me-1\"></i>Edit RuleSet\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"view-linked-ruleset\">\n <i class=\"bi bi-box-arrow-up-right me-1\"></i>View Full Details\n </button>\n <button class=\"btn btn-outline-success btn-sm\" data-action=\"create-rule-from-incident\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create New Rule\n </button>\n </div>\n </div>\n <div data-container=\"ruleset-data\"></div>\n <div class=\"mt-3\">\n <h6 class=\"mb-2\"><i class=\"bi bi-funnel me-2\"></i>Rule Conditions</h6>\n <div data-container=\"ruleset-rules\"></div>\n </div>\n </div>\n {{/hasRuleset|bool}}\n {{^hasRuleset|bool}}\n <div class=\"text-center py-5\">\n <div class=\"text-muted mb-3\">\n <i class=\"bi bi-gear fs-1\"></i>\n </div>\n <h6 class=\"text-muted\">No RuleSet Linked</h6>\n <p class=\"text-muted small mb-3\">\n This incident was not created by a rule engine match.<br>\n You can create a new rule based on this incident's event pattern to catch similar events automatically.\n </p>\n <button class=\"btn btn-primary\" data-action=\"create-rule-from-incident\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Rule from Incident\n </button>\n </div>\n {{/hasRuleset|bool}}\n `;\n }\n\n async onInit() {\n if (!this.rulesetId) return;\n\n try {\n this.rulesetModel = new RuleSet({ id: this.rulesetId });\n await this.rulesetModel.fetch();\n this.hasRuleset = true;\n this.autoDeleteEnabled = !!(this.rulesetModel.get('metadata')?.delete_on_resolution);\n } catch (e) {\n return;\n }\n\n // RuleSet summary\n const matchByValue = this.rulesetModel.get('match_by');\n const matchByOption = MatchByOptions.find(opt => opt.value === matchByValue);\n const bundleByValue = this.rulesetModel.get('bundle_by');\n const bundleByOption = BundleByOptions.find(opt => opt.value === bundleByValue);\n\n this.rulesetDataView = new DataView({\n containerId: 'ruleset-data',\n model: this.rulesetModel,\n className: 'border rounded p-3 bg-light',\n columns: 2,\n fields: [\n { name: 'name', label: 'Name', cols: 6 },\n { name: 'category', label: 'Scope', formatter: 'badge', cols: 3 },\n { name: 'is_active', label: 'Active', formatter: 'yesnoicon', cols: 3 },\n { name: 'priority', label: 'Priority', cols: 3 },\n { name: 'match_by', label: 'Match Logic', template: matchByOption ? matchByOption.label : String(matchByValue), cols: 3 },\n { name: 'bundle_by', label: 'Bundle By', template: bundleByOption ? bundleByOption.label : String(bundleByValue), cols: 3 },\n { name: 'bundle_minutes', label: 'Bundle Window', cols: 3 },\n { name: 'trigger_count', label: 'Trigger Count', cols: 3 },\n { name: 'trigger_window', label: 'Trigger Window (min)', cols: 3 },\n { name: 'retrigger_every', label: 'Re-trigger Every', cols: 3 },\n { name: 'handler', label: 'Handler', cols: 12 },\n ]\n });\n this.addChild(this.rulesetDataView);\n\n // Rules table\n const rulesCollection = new RuleList({\n params: { parent: this.rulesetId }\n });\n this.rulesTable = new TableView({\n containerId: 'ruleset-rules',\n collection: rulesCollection,\n hideActivePillNames: ['parent'],\n columns: [\n { key: 'name', label: 'Name' },\n { key: 'field_name', label: 'Field' },\n { key: 'comparator', label: 'Comparator', width: '110px' },\n { key: 'value', label: 'Value' },\n { key: 'value_type', label: 'Type', width: '80px' },\n ],\n showAdd: false,\n size: 10,\n paginated: false\n });\n this.addChild(this.rulesTable);\n }\n\n async onActionEditLinkedRuleset() {\n if (!this.rulesetModel) return;\n const resp = await Modal.modelForm({\n title: `Edit RuleSet — ${this.rulesetModel.get('name')}`,\n model: this.rulesetModel,\n formConfig: RuleSetForms.edit,\n });\n if (resp) {\n await this.render();\n this.getApp()?.toast?.success('RuleSet updated');\n }\n }\n\n async onActionViewLinkedRuleset() {\n if (!this.rulesetModel) return;\n const view = new RuleSetView({ model: this.rulesetModel });\n await Modal.show(view, { size: 'xl', header: false });\n }\n\n async onActionCreateRuleFromIncident() {\n // Pre-fill ruleset creation with incident context\n const incident = this.incident;\n const category = incident.get('category') || '';\n const scope = incident.get('scope') || '';\n const metadata = incident.get('metadata') || {};\n\n // Step 1: Create the RuleSet using the standard form\n const resp = await Modal.form({\n title: 'Create RuleSet from Incident',\n icon: 'bi-gear-wide-connected',\n formConfig: RuleSetForms.create,\n size: 'lg',\n data: {\n name: `Rule: ${category || 'custom'} (from incident #${incident.get('id')})`,\n category: scope || category,\n priority: 10,\n is_active: false,\n bundle_by: metadata.source_ip ? 4 : 0,\n bundle_minutes: 30,\n match_by: 0\n }\n });\n\n if (!resp) return;\n\n const ruleset = new RuleSet();\n const saveResp = await ruleset.save({\n ...resp,\n bundle_by: parseInt(resp.bundle_by),\n bundle_minutes: parseInt(resp.bundle_minutes) || 30,\n match_by: parseInt(resp.match_by) || 0\n });\n\n if (!saveResp.success && saveResp.status !== 200) {\n this.getApp()?.toast?.error('Failed to create RuleSet');\n return;\n }\n\n // Link the new ruleset to this incident\n await this.incident.save({ rule_set: ruleset.id });\n this.rulesetId = ruleset.id;\n this.rulesetModel = ruleset;\n this.hasRuleset = true;\n\n // Step 2: Show metadata field picker for rule conditions\n const rulesCreated = await this._showMetadataRulePicker(ruleset, metadata);\n\n this.getApp()?.toast?.success(\n rulesCreated\n ? `RuleSet created with ${rulesCreated} rule condition(s)`\n : 'RuleSet created — add rule conditions to activate'\n );\n\n // Open the full RuleSet view so user can review\n const view = new RuleSetView({ model: ruleset });\n await Modal.show(view, { size: 'xl', header: false });\n }\n\n /**\n * Show a picker dialog letting the user select which metadata fields\n * should become Rule conditions on the new RuleSet.\n * @returns {number} Number of rules created, or 0 if skipped\n */\n async _showMetadataRulePicker(ruleset, metadata) {\n // Fields to exclude — duplicates of top-level incident fields or not useful for matching\n const EXCLUDE_FIELDS = new Set([\n 'title', 'details', 'scope', 'category', 'source_ip', 'hostname',\n 'model_name', 'model_id', 'country_code', 'country_name',\n 'latitude', 'longitude', 'stack_trace', 'traceback',\n 'do_not_delete', 'delete_on_resolution'\n ]);\n\n // Build field entries from metadata\n const entries = Object.entries(metadata)\n .filter(([key, val]) => !EXCLUDE_FIELDS.has(key) && val !== null && val !== '' && typeof val !== 'object')\n .map(([key, val]) => ({ key, value: val, type: _inferValueType(val) }));\n\n if (!entries.length) return 0;\n\n // Build switch fields — each metadata key becomes a toggle with its value shown\n const fields = [\n {\n type: 'html',\n columns: 12,\n html: `<div class=\"text-muted small mb-3\">\n Select metadata fields to create as rule conditions.\n Each selected field becomes an <code>==</code> match rule.\n </div>`\n },\n ...entries.map(e => ({\n name: `rule__${e.key}`,\n type: 'switch',\n label: `${e.key} = ${_truncate(String(e.value), 30)}`,\n tooltip: `${e.type}: ${String(e.value)}`,\n value: false,\n columns: 6\n }))\n ];\n\n const pickerResp = await Modal.form({\n title: 'Create Rules from Metadata',\n icon: 'bi-list-check',\n size: 'lg',\n fields,\n submitText: 'Create Rules',\n cancelText: 'Skip'\n });\n\n if (!pickerResp) return 0;\n\n // Collect selected fields\n const selected = entries.filter(e => pickerResp[`rule__${e.key}`]);\n if (!selected.length) return 0;\n\n // Create Rule conditions in parallel\n const app = this.getApp();\n try {\n await Promise.all(selected.map((e, i) => {\n const rule = new Rule();\n return rule.save({\n parent: ruleset.id,\n name: `Match ${e.key}`,\n field_name: e.key,\n comparator: '==',\n value: String(e.value),\n value_type: e.type,\n index: i\n });\n }));\n } catch (err) {\n app?.toast?.warning('Some rule conditions failed to save');\n }\n\n return selected.length;\n }\n}\n\n\n// ── Tickets Section ──────────────────────────────────────\n\nclass IncidentTicketsSection extends View {\n constructor(options = {}) {\n super({\n className: 'incident-tickets-section p-3',\n template: `\n <div class=\"d-flex align-items-center justify-content-between mb-3\">\n <h6 class=\"mb-0\"><i class=\"bi bi-ticket-perforated me-2\"></i>Related Tickets</h6>\n <button class=\"btn btn-primary btn-sm\" data-action=\"create-ticket\">\n <i class=\"bi bi-plus-circle me-1\"></i>Create Ticket\n </button>\n </div>\n <div data-container=\"tickets-table\"></div>\n `,\n ...options\n });\n\n this.incident = options.incident;\n }\n\n async onInit() {\n const ticketsCollection = new TicketList({\n params: { incident: this.incident.get('id'), sort: '-created' }\n });\n\n this.ticketsTable = new TableView({\n containerId: 'tickets-table',\n collection: ticketsCollection,\n hideActivePillNames: ['incident'],\n columns: [\n { key: 'id', label: 'ID', width: '60px', sortable: true },\n { key: 'created', label: 'Created', formatter: 'epoch|datetime', sortable: true, width: '160px' },\n { key: 'status', label: 'Status', formatter: 'badge', width: '100px' },\n { key: 'category', label: 'Category', formatter: 'badge', width: '120px' },\n { key: 'priority', label: 'Priority', width: '80px', sortable: true },\n { key: 'title', label: 'Title' },\n ],\n actions: ['view'],\n showAdd: false,\n paginated: true,\n size: 10,\n emptyMessage: 'No tickets linked to this incident.'\n });\n this.addChild(this.ticketsTable);\n }\n\n async onActionCreateTicket() {\n const incident = this.incident;\n const title = `Incident #${incident.get('id')}: ${incident.get('category') || incident.get('title') || 'Investigation'}`;\n\n const formConfig = {\n ...TicketForms.create,\n fields: TicketForms.create.fields.map(f => {\n if (f.name === 'title') return { ...f, value: title };\n if (f.name === 'category') return { ...f, value: 'incident' };\n if (f.name === 'priority') return { ...f, value: incident.get('priority') || 5 };\n if (f.name === 'incident') return { ...f, value: incident.get('id'), type: 'hidden' };\n return f;\n })\n };\n\n const data = await Modal.form(formConfig);\n if (!data) return;\n\n const ticket = new Ticket();\n const resp = await ticket.save({\n ...data,\n incident: incident.get('id')\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('Ticket created');\n this.ticketsTable?.collection?.fetch();\n } else {\n this.getApp()?.toast?.error('Failed to create ticket');\n }\n }\n}\n\n\n// ── Related Incidents Section ────────────────────────────\n\nclass RelatedIncidentsSection extends View {\n constructor(options = {}) {\n super({\n className: 'related-incidents-section p-3',\n template: `\n <div class=\"mb-3\">\n <h6 class=\"mb-1\"><i class=\"bi bi-diagram-2 me-2\"></i>Related Incidents</h6>\n <p class=\"text-muted small mb-0\">Incidents sharing the same source IP or category</p>\n </div>\n <div data-container=\"related-table\"></div>\n `,\n ...options\n });\n\n this.incident = options.incident;\n this.sourceIP = options.sourceIP;\n }\n\n async onInit() {\n // Build filter params — same category or same source IP, excluding this incident\n const params = {\n id__not: this.incident.get('id'),\n sort: '-created',\n size: 10\n };\n\n // Prefer source IP for network incidents, fall back to category\n if (this.sourceIP) {\n params.source_ip = this.sourceIP;\n } else {\n const category = this.incident.get('category');\n if (category) {\n params.category = category;\n }\n }\n\n const relatedCollection = new IncidentList({ params });\n\n this.relatedTable = new TableView({\n containerId: 'related-table',\n collection: relatedCollection,\n hideActivePillNames: ['id__not', 'source_ip', 'category'],\n columns: [\n { key: 'id', label: 'ID', width: '60px', sortable: true },\n { key: 'created', label: 'Created', formatter: 'epoch|datetime', sortable: true, width: '160px' },\n { key: 'status', label: 'Status', formatter: 'badge', width: '100px' },\n { key: 'category', label: 'Category', formatter: 'badge' },\n { key: 'priority', label: 'Priority', width: '80px', sortable: true },\n { key: 'title', label: 'Title', formatter: \"truncate(60)|default('—')\" },\n ],\n actions: ['view'],\n showAdd: false,\n paginated: true,\n size: 10,\n emptyMessage: 'No related incidents found.'\n });\n this.addChild(this.relatedTable);\n }\n}\n\n\n// ── IncidentView ─────────────────────────────────────────\n\nclass IncidentView extends View {\n constructor(options = {}) {\n super({\n className: 'incident-view',\n ...options\n });\n\n this.model = options.model || new Incident(options.data || {});\n this.incidentIcon = getHeaderIcon(this.model.get('status'));\n this.statusCfg = getStatusConfig(this.model.get('status'));\n this.priorityCfg = getPriorityConfig(this.model.get('priority'));\n this.isProtected = !!(this.model.get('metadata')?.do_not_delete);\n\n this.template = `\n <div class=\"incident-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start 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 <h4 class=\"mb-1\">Incident #{{model.id}}</h4>\n {{#model.title}}\n <div class=\"text-muted mb-2\">{{model.title|truncate(80)}}</div>\n {{/model.title}}\n <div class=\"d-flex align-items-center gap-2 flex-wrap\">\n <span class=\"badge {{statusCfg.badge}}\" data-bs-toggle=\"tooltip\" title=\"{{statusCfg.help}}\">\n <i class=\"bi {{statusCfg.icon}} me-1\"></i>{{statusCfg.label}}\n </span>\n <span class=\"badge {{priorityCfg.bg}} {{priorityCfg.color}}\" data-bs-toggle=\"tooltip\" title=\"Priority {{model.priority}} — {{priorityCfg.label}} severity\">\n P{{model.priority}} · {{priorityCfg.label}}\n </span>\n {{#model.category}}\n <span class=\"badge bg-light text-dark border\">{{model.category}}</span>\n {{/model.category}}\n {{#isProtected|bool}}\n <span class=\"badge bg-warning text-dark\"><i class=\"bi bi-shield-fill-check me-1\"></i>Protected</span>\n {{/isProtected|bool}}\n </div>\n <div class=\"text-muted small mt-1\">\n {{model.created|datetime}}\n {{#model.scope}} · {{model.scope}}{{/model.scope}}\n {{#model.hostname}} · {{model.hostname}}{{/model.hostname}}\n {{#model.event_count}} · {{model.event_count}} events{{/model.event_count}}\n </div>\n </div>\n </div>\n <div data-container=\"incident-context-menu\"></div>\n </div>\n\n <!-- SideNav -->\n <div data-container=\"incident-sidenav\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // Resolve source IP early for reuse\n this._sourceIP = await this._resolveSourceIP();\n this.getApp().showLoading();\n await this.model.fetch({params: {graph:\"detailed\"}});\n this.getApp().hideLoading();\n\n // ── Overview section ──\n this.overviewSection = new IncidentOverviewSection({ model: this.model });\n const overviewSection = this.overviewSection;\n overviewSection.on('incident:updated', () => this._handleIncidentUpdated());\n overviewSection.on('create-ticket', () => this._handleCreateTicket());\n overviewSection.on('analyze-llm', () => this._handleAnalyzeLlm());\n overviewSection.on('ask-ai', () => this._handleAskAi());\n\n // ── Events section ──\n const eventsCollection = new IncidentEventList({\n params: { incident: this.model.get('id') }\n });\n const eventsSection = new TableView({\n collection: eventsCollection,\n hideActivePillNames: ['incident'],\n columns: [\n { key: 'id', label: 'ID', width: '50px', sortable: true },\n {\n key: 'created', label: 'Date / Category', sortable: true, width: '160px',\n template: `<div>{{{model.created|epoch|datetime}}}</div><div class=\"text-muted small\">{{{model.category|badge}}}</div>`\n },\n {\n key: 'source_ip', label: 'Source', sortable: true, width: '130px',\n template: `<div>{{model.hostname}}</div><div class=\"text-muted small\">{{model.source_ip}}</div>`,\n filter: { type: 'text' }\n },\n { key: 'title', label: 'Title', sortable: true, formatter: \"truncate(80)|default('—')\" },\n {\n key: 'level', label: 'Level', sortable: true, width: '60px',\n filter: { type: 'text' }\n },\n ],\n showAdd: false,\n actions: ['view'],\n paginated: true,\n size: 10\n });\n\n // ── Rule Engine section ──\n const ruleEngineSection = new RuleEngineSection({ incident: this.model });\n\n // ── Tickets section ──\n const ticketsSection = new IncidentTicketsSection({ incident: this.model });\n\n // ── History section ──\n const adapter = new IncidentHistoryAdapter(this.model.get('id'));\n const historySection = new ChatView({ adapter });\n\n // ── Related Incidents section ──\n const relatedSection = new RelatedIncidentsSection({\n incident: this.model,\n sourceIP: this._sourceIP\n });\n\n // Build sections\n const sections = [\n { key: 'Overview', label: 'Overview', icon: 'bi-shield-exclamation', view: overviewSection },\n { key: 'Events', label: 'Events', icon: 'bi-list-ul', view: eventsSection },\n { key: 'Rule Engine', label: 'Rule Engine', icon: 'bi-gear-wide-connected', view: ruleEngineSection },\n { key: 'Tickets', label: 'Tickets', icon: 'bi-ticket-perforated', view: ticketsSection },\n { key: 'History', label: 'History', icon: 'bi-chat-left-text', view: historySection },\n { type: 'divider', label: 'Investigation' },\n { key: 'Related Incidents', label: 'Related', icon: 'bi-diagram-2', view: relatedSection },\n ];\n\n // Conditional investigation sections\n const metadata = this.model.get('metadata') || {};\n const ipInfo = this.model.get('ip_info');\n\n if (metadata.http_method || metadata.http_path) {\n const httpSection = new HttpRequestSection({ metadata });\n sections.push({ key: 'HTTP Request', label: 'HTTP Request', icon: 'bi-globe2', view: httpSection });\n }\n\n if (ipInfo) {\n const ipIntelSection = new IPIntelligenceSection({ ipInfo });\n sections.push({ key: 'IP Intelligence', label: 'IP Intel', icon: 'bi-shield-lock', view: ipIntelSection });\n }\n\n // Conditional forensics sections\n const stackTrace = metadata.stack_trace || metadata.traceback;\n const hasStackTrace = !!stackTrace;\n const hasMetadata = Object.keys(metadata).length > 0;\n\n if (hasStackTrace || hasMetadata) {\n sections.push({ type: 'divider', label: 'Forensics' });\n }\n\n if (hasStackTrace) {\n const stackTraceSection = new StackTraceView({\n stackTrace\n });\n sections.push({ key: 'Stack Trace', label: 'Stack Trace', icon: 'bi-code-square', view: stackTraceSection });\n }\n\n if (hasMetadata) {\n const metadataSection = this._buildMetadataSection(metadata);\n sections.push({ key: 'Metadata', label: 'Metadata', icon: 'bi-braces', view: metadataSection });\n }\n\n // SideNavView\n this.sideNav = new SideNavView({\n containerId: 'incident-sidenav',\n sections,\n activeSection: 'Overview',\n navWidth: 180,\n contentPadding: '1.25rem 2rem',\n enableResponsive: true,\n minWidth: 500\n });\n this.addChild(this.sideNav);\n\n // ContextMenu\n this._buildContextMenu();\n }\n\n _buildMetadataSection(metadata) {\n // Structured display of known metadata fields + raw JSON\n const knownFields = [];\n const knownKeys = ['source_ip', 'hostname', 'user_agent', 'http_url', 'http_method',\n 'http_status', 'country_code', 'region', 'city', 'request_path', 'user',\n 'component', 'component_id', 'error_class', 'error_message', 'rule_id',\n 'risk_score', 'action', 'trigger'];\n\n for (const key of knownKeys) {\n if (metadata[key] !== undefined && metadata[key] !== null) {\n knownFields.push({ name: key, label: key.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()), cols: 6 });\n }\n }\n\n if (knownFields.length > 0) {\n // Use a wrapper view with structured fields + raw JSON\n return new View({\n model: this.model,\n metadata: metadata,\n hasStructuredFields: knownFields.length > 0,\n template: `\n <div class=\"p-3\">\n {{#hasStructuredFields|bool}}\n <h6 class=\"mb-3\"><i class=\"bi bi-list-check me-2\"></i>Key Fields</h6>\n <div data-container=\"structured-metadata\" class=\"mb-4\"></div>\n {{/hasStructuredFields|bool}}\n <h6 class=\"mb-2\"><i class=\"bi bi-braces me-2\"></i>Raw Metadata</h6>\n <pre class=\"bg-light p-3 border rounded small\"><code>{{{model.metadata|json}}}</code></pre>\n </div>\n `,\n onInit() {\n if (knownFields.length > 0) {\n // Build a simple model from metadata for DataView\n const metaModel = { get: (key) => metadata[key], attributes: metadata };\n this.structuredView = new DataView({\n containerId: 'structured-metadata',\n model: metaModel,\n columns: 2,\n showEmptyValues: false,\n fields: knownFields\n });\n this.addChild(this.structuredView);\n }\n }\n });\n }\n\n // Fallback: just raw JSON\n return new View({\n model: this.model,\n template: `<pre class=\"bg-light p-3 border rounded small\"><code>{{{model.metadata|json}}}</code></pre>`\n });\n }\n\n _buildContextMenu() {\n const status = (this.model.get('status') || '').toLowerCase();\n const items = [];\n\n // Status transitions\n items.push({ label: 'Change Status', icon: 'bi-arrow-repeat', header: true });\n\n if (status !== 'open')\n items.push({ label: 'Open', action: 'set-status-open', icon: 'bi-folder2-open' });\n if (status !== 'investigating')\n items.push({ label: 'Investigate', action: 'set-status-investigating', icon: 'bi-search' });\n if (status !== 'paused')\n items.push({ label: 'Pause', action: 'set-status-paused', icon: 'bi-pause-circle' });\n if (status !== 'resolved')\n items.push({ label: 'Resolve', action: 'set-status-resolved', icon: 'bi-check-circle' });\n if (status !== 'ignored')\n items.push({ label: 'Ignore', action: 'set-status-ignored', icon: 'bi-eye-slash' });\n\n items.push({ type: 'divider' });\n\n // Edit & priority\n items.push({ label: 'Edit Incident', action: 'edit-incident', icon: 'bi-pencil' });\n items.push({ label: 'Change Priority', action: 'change-priority', icon: 'bi-arrow-up-circle' });\n\n // Protection\n if (this.model.get('metadata')?.do_not_delete) {\n items.push({ label: 'Remove Protection', action: 'remove-protection', icon: 'bi-shield' });\n } else {\n items.push({ label: 'Protect from Deletion', action: 'protect-incident', icon: 'bi-shield-fill-check' });\n }\n\n items.push({ type: 'divider' });\n\n // IP actions\n if (this._sourceIP) {\n items.push({ label: `Block IP (${this._sourceIP})`, action: 'block-source-ip', icon: 'bi-slash-circle', class: 'text-danger' });\n items.push({ label: `View GeoIP (${this._sourceIP})`, action: 'view-source-geoip', icon: 'bi-globe' });\n }\n\n // Ticket & merge\n items.push({ label: 'Create Ticket', action: 'create-ticket', icon: 'bi-ticket-perforated' });\n items.push({ label: 'Merge Incidents', action: 'merge-incidents', icon: 'bi-union' });\n\n items.push({ type: 'divider' });\n\n // AI\n items.push({ label: 'Ask AI', action: 'ask-ai', icon: 'bi-chat-dots' });\n items.push({ label: 'LLM Analyze', action: 'analyze-llm', icon: 'bi-robot' });\n\n items.push({ type: 'divider' });\n items.push({ label: 'Delete Incident', action: 'delete-incident', icon: 'bi-trash', danger: true });\n\n const incidentMenu = new ContextMenu({\n containerId: 'incident-context-menu',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items\n }\n });\n this.addChild(incidentMenu);\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n if (window.bootstrap && window.bootstrap.Tooltip && this.element) {\n const tooltipTriggerList = this.element.querySelectorAll('[data-bs-toggle=\"tooltip\"]');\n tooltipTriggerList.forEach(el => {\n const existing = window.bootstrap.Tooltip.getInstance(el);\n if (existing && typeof existing.dispose === 'function') existing.dispose();\n new window.bootstrap.Tooltip(el);\n });\n }\n }\n\n // ── Source IP resolution ──\n\n async _resolveSourceIP() {\n const metadata = this.model.get('metadata') || {};\n if (metadata.source_ip) return metadata.source_ip;\n if (metadata.ip) return metadata.ip;\n if (metadata.ip_address) return metadata.ip_address;\n\n try {\n const events = new IncidentEventList({\n params: { incident: this.model.get('id'), size: 1, sort: '-created' }\n });\n await events.fetch();\n const firstEvent = events.first();\n if (firstEvent) {\n return firstEvent.get('source_ip') || firstEvent.get('ip_address') || null;\n }\n } catch (e) {\n // No IP available\n }\n return null;\n }\n\n // ── Status actions ──\n\n async _setStatus(status) {\n await this.model.save({ status });\n this.getApp()?.toast?.success(`Status changed to ${status}`);\n this._handleIncidentUpdated();\n }\n\n async onActionSetStatusOpen() { await this._setStatus('open'); }\n async onActionSetStatusInvestigating() { await this._setStatus('investigating'); }\n async onActionSetStatusPaused() { await this._setStatus('paused'); }\n async onActionSetStatusResolved() { await this._setStatus('resolved'); }\n async onActionSetStatusIgnored() { await this._setStatus('ignored'); }\n\n // ── Protection ──\n\n async onActionProtectIncident() {\n await this.model.save({ metadata: { do_not_delete: true } });\n this.getApp()?.toast?.success('Incident protected from deletion');\n this._handleIncidentUpdated();\n }\n\n async onActionRemoveProtection() {\n await this.model.save({ metadata: { do_not_delete: false } });\n this.getApp()?.toast?.success('Deletion protection removed');\n this._handleIncidentUpdated();\n }\n\n // ── Priority ──\n\n async onActionChangePriority() {\n const data = await Modal.form({\n title: 'Change Priority',\n icon: 'bi-arrow-up-circle',\n size: 'sm',\n fields: [\n {\n name: 'priority', type: 'select', label: 'Priority',\n value: this.model.get('priority') || 5,\n options: [\n { value: 10, label: '10 — Critical' },\n { value: 9, label: '9 — Critical' },\n { value: 8, label: '8 — High' },\n { value: 7, label: '7 — High' },\n { value: 6, label: '6 — Medium' },\n { value: 5, label: '5 — Medium' },\n { value: 4, label: '4 — Low' },\n { value: 3, label: '3 — Low' },\n { value: 2, label: '2 — Info' },\n { value: 1, label: '1 — Info' },\n ]\n }\n ]\n });\n if (!data) return;\n\n await this.model.save({ priority: parseInt(data.priority) });\n this.getApp()?.toast?.success(`Priority changed to ${data.priority}`);\n this._handleIncidentUpdated();\n }\n\n // ── Edit ──\n\n async onActionEditIncident() {\n const resp = await Modal.modelForm({\n title: `Edit Incident #${this.model.id}`,\n model: this.model,\n formConfig: IncidentForms.edit,\n });\n if (resp) {\n this._handleIncidentUpdated();\n }\n }\n\n // ── IP actions ──\n\n async onActionBlockSourceIp() {\n if (!this._sourceIP) return;\n\n const geoModel = await GeoLocatedIP.lookup(this._sourceIP);\n if (!geoModel) {\n this.getApp()?.toast?.error('Could not find GeoIP record for this IP');\n return;\n }\n\n const data = await Modal.form({\n title: `Block IP — ${this._sourceIP}`,\n icon: 'bi-slash-circle',\n size: 'sm',\n fields: [\n { name: 'reason', type: 'text', label: 'Reason', required: true, value: `Blocked from incident #${this.model.id}` },\n {\n name: 'ttl', type: 'select', label: 'Duration',\n options: [\n { value: 3600, label: '1 hour' },\n { value: 21600, label: '6 hours' },\n { value: 86400, label: '24 hours' },\n { value: 604800, label: '7 days' },\n { value: 2592000, label: '30 days' },\n { value: 0, label: 'Permanent' }\n ],\n value: 86400\n }\n ]\n });\n if (!data) return;\n\n const resp = await geoModel.save({\n block: { reason: data.reason, ttl: parseInt(data.ttl) }\n });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success(`IP ${this._sourceIP} blocked fleet-wide`);\n } else {\n this.getApp()?.toast?.error('Failed to block IP');\n }\n }\n\n async onActionViewSourceGeoip() {\n if (this._sourceIP) {\n await GeoIPView.show(this._sourceIP);\n }\n }\n\n // ── Ticket ──\n\n async onActionCreateTicket() {\n this._handleCreateTicket();\n }\n\n async _handleCreateTicket() {\n const title = `Incident #${this.model.get('id')}: ${this.model.get('category') || this.model.get('title') || 'Investigation'}`;\n\n const data = await Modal.form({\n ...TicketForms.create,\n fields: TicketForms.create.fields.map(f => {\n if (f.name === 'title') return { ...f, value: title };\n if (f.name === 'category') return { ...f, value: 'incident' };\n if (f.name === 'priority') return { ...f, value: this.model.get('priority') || 5 };\n if (f.name === 'incident') return { ...f, value: this.model.get('id'), type: 'hidden' };\n return f;\n })\n });\n if (!data) return;\n\n const ticket = new Ticket();\n const resp = await ticket.save({ ...data, incident: this.model.get('id') });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('Ticket created');\n } else {\n this.getApp()?.toast?.error('Failed to create ticket');\n }\n }\n\n // ── Merge ──\n\n async onActionMergeIncidents() {\n const data = await Modal.form({\n title: 'Merge Incidents',\n icon: 'bi-union',\n size: 'sm',\n fields: [\n {\n name: 'merge_ids', type: 'text', label: 'Incident IDs to merge into this one',\n required: true,\n placeholder: 'e.g., 102, 105, 108',\n help: 'Comma-separated IDs. Events from those incidents will be merged here.'\n }\n ]\n });\n if (!data) return;\n\n const mergeIds = data.merge_ids.split(',').map(s => parseInt(s.trim())).filter(n => n && n !== this.model.id);\n if (mergeIds.length === 0) return;\n\n const resp = await this.model.save({ merge: mergeIds });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success(`Merged ${mergeIds.length} incident(s)`);\n this._handleIncidentUpdated();\n } else {\n this.getApp()?.toast?.error('Merge failed');\n }\n }\n\n // ── LLM Analysis ──\n\n async onActionAskAi() {\n await this._handleAskAi();\n }\n\n async _handleAskAi() {\n await openAssistantChat(this, 'incident.Incident');\n }\n\n async onActionAnalyzeLlm() {\n await this._handleAnalyzeLlm();\n }\n\n async _handleAnalyzeLlm() {\n const confirmed = await Modal.confirm(\n 'Run LLM analysis on this incident? The AI agent will review all events, ' +\n 'attempt to merge related incidents, and propose a new rule to catch similar patterns. ' +\n 'The incident status will be set to \"investigating\".',\n 'LLM Analysis',\n { confirmText: 'Analyze', confirmClass: 'btn-info' }\n );\n if (!confirmed) return;\n\n const app = this.getApp();\n app?.showLoading('Starting LLM analysis...');\n\n try {\n const resp = await this.model.save({ analyze: 1 });\n\n if (!resp.success && resp.status !== 200) {\n const errorMsg = resp.data?.error || resp.error || 'Failed to start analysis';\n app?.toast?.error(errorMsg);\n return;\n }\n\n app?.toast?.success('LLM analysis started — polling for results in the background...');\n\n // Poll in the background — don't block the UI\n this._pollAnalysisProgress();\n\n } catch (error) {\n app?.toast?.error(`Analysis failed: ${error.message}`);\n } finally {\n app?.hideLoading();\n }\n }\n\n _pollAnalysisProgress() {\n const maxAttempts = 60; // 5 minutes at 5s intervals\n const pollInterval = 5000;\n let attempt = 0;\n\n const poll = () => {\n attempt++;\n if (attempt > maxAttempts) {\n this.getApp()?.toast?.error('Analysis is taking longer than expected. Check back later.');\n return;\n }\n\n setTimeout(() => {\n this.model.fetch().then(() => {\n const metadata = this.model.get('metadata') || {};\n\n if (!metadata.analysis_in_progress) {\n if (metadata.llm_analysis) {\n this.getApp()?.toast?.success('LLM analysis complete');\n this.overviewSection?.refreshAnalysis();\n } else {\n this.getApp()?.toast?.success('Analysis finished');\n }\n this._handleIncidentUpdated();\n return;\n }\n poll();\n }).catch(() => {\n // Fetch failed — keep polling\n poll();\n });\n }, pollInterval);\n };\n\n poll();\n }\n\n // ── Delete ──\n\n async onActionDeleteIncident() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete incident #${this.model.id}? This action cannot be undone.`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n if (confirmed) {\n const resp = await this.model.destroy();\n if (resp.success) {\n this.emit('incident:deleted', { model: this.model });\n }\n }\n }\n\n // ── Update handler ──\n\n _handleIncidentUpdated() {\n this.incidentIcon = getHeaderIcon(this.model.get('status'));\n this.statusCfg = getStatusConfig(this.model.get('status'));\n this.priorityCfg = getPriorityConfig(this.model.get('priority'));\n this.render();\n this.emit('incident:updated', { model: this.model });\n }\n}\n\nIncident.VIEW_CLASS = IncidentView;\n\nexport default IncidentView;\n","/**\n * PriorityQueueView — top critical/high incidents that need attention.\n *\n * List view (not table) of up to 8 incidents matching:\n * priority >= 8, status in (new, open, investigating), sorted by recency.\n *\n * Each row: priority pill, title, age + source IP + event count, hover-\n * revealed inline actions (resolve / pause / block IP). Click row opens\n * the existing IncidentView in a Modal — same pattern as IncidentTablePage.\n *\n * Inline actions are gated on `allowActions` (true when user has\n * manage_security). When false the icons are hidden.\n */\n\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { IncidentList, Incident } from '@ext/admin/models/Incident.js';\nimport IncidentView from '../IncidentView.js';\n\n// Track NEW incidents only — these are untriaged. `open` means someone\n// has already claimed it (work-in-progress), `investigating` is actively\n// being worked. The \"needs attention\" queue is for the unhandled tier.\nconst STATUS_FILTER = 'new';\n\nclass PriorityQueueView extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-priority-queue ${options.className || ''}`.trim()\n });\n this.allowActions = options.allowActions !== false;\n this.size = options.size || 8;\n this.collection = new IncidentList({\n params: {\n priority__gte: 8,\n status__in: STATUS_FILTER,\n sort: '-created',\n size: this.size\n }\n });\n // State read by the Mustache template — must be on `this` (not on\n // `this.data`) for Mustache to resolve {{items}} / {{isEmpty}} / etc.\n this.items = [];\n this.isLoading = true;\n this.hasError = false;\n this.error = '';\n this.isEmpty = false;\n this.showActions = this.allowActions;\n }\n\n async getTemplate() {\n return `\n <div class=\"card sd-card\">\n <div class=\"card-header bg-transparent border-0 d-flex justify-content-between align-items-start\">\n <div>\n <h3 class=\"card-title sd-card-title mb-0\">Needs Attention</h3>\n <span class=\"card-subtitle text-muted small\">Top critical & high-priority incidents</span>\n </div>\n <a class=\"text-muted small\" href=\"?page=system/incidents\">All incidents <i class=\"bi bi-arrow-right-short\"></i></a>\n </div>\n <div class=\"card-body p-0\" data-region=\"list\">\n {{#isLoading|bool}}<div class=\"p-4 text-center text-muted small\"><i class=\"bi bi-hourglass-split me-1\"></i>Loading…</div>{{/isLoading|bool}}\n {{#hasError|bool}}<div class=\"p-3\"><div class=\"alert alert-warning small mb-0\">{{error}}</div></div>{{/hasError|bool}}\n {{#isEmpty|bool}}<div class=\"p-4 text-center text-success small\"><i class=\"bi bi-check-circle me-1\"></i>Nothing critical right now.</div>{{/isEmpty|bool}}\n <ol class=\"sd-priority-list list-unstyled mb-0\">\n {{#items}}\n <li class=\"sd-pri-row\" data-action=\"open-incident\" data-id=\"{{id}}\">\n <span class=\"sd-pri sd-pri-{{severityClass}}\">{{severityLabel}} {{priority}}</span>\n <div class=\"sd-pri-body\">\n <span class=\"sd-pri-title\">{{title}}</span>\n <span class=\"sd-pri-meta\">{{ageLabel}} · <span class=\"text-body sd-mono\">{{sourceIp}}</span> · {{eventCount}} events</span>\n </div>\n {{#showActions|bool}}\n <span class=\"sd-pri-actions\">\n <button type=\"button\" class=\"btn btn-sm btn-link text-success p-1\" data-action=\"resolve-incident\" data-id=\"{{id}}\" title=\"Resolve\"><i class=\"bi bi-check2\"></i></button>\n <button type=\"button\" class=\"btn btn-sm btn-link text-secondary p-1\" data-action=\"pause-incident\" data-id=\"{{id}}\" title=\"Pause\"><i class=\"bi bi-pause\"></i></button>\n </span>\n {{/showActions|bool}}\n </li>\n {{/items}}\n </ol>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n await this._fetchSafely();\n }\n\n async _fetchSafely() {\n try {\n await this.collection.fetch();\n const models = this.collection.models || [];\n this.items = models.map(m => this._rowFor(m));\n this.isLoading = false;\n this.hasError = false;\n this.error = '';\n this.isEmpty = this.items.length === 0;\n } catch (err) {\n console.warn('[PriorityQueueView] fetch failed:', err);\n this.items = [];\n this.isLoading = false;\n this.hasError = true;\n this.error = 'Could not load incidents.';\n this.isEmpty = false;\n }\n }\n\n _rowFor(model) {\n const priority = parseInt(model.get('priority'), 10) || 0;\n const sevClass = priority >= 12 ? 'critical'\n : (priority >= 8 ? 'high' : (priority >= 4 ? 'warn' : 'info'));\n const sevLabel = sevClass === 'critical' ? 'CRIT' : sevClass.toUpperCase();\n return {\n id: model.id,\n title: model.get('title') || model.get('details') || `Incident #${model.id}`,\n priority,\n severityClass: sevClass,\n severityLabel: sevLabel,\n ageLabel: this._relativeTime(model.get('created')),\n sourceIp: model.get('source_ip') || model.get('metadata')?.source_ip || '—',\n eventCount: model.get('event_count') ?? model.get('metadata')?.event_count ?? '—'\n };\n }\n\n _relativeTime(timestamp) {\n if (!timestamp) return '—';\n const t = typeof timestamp === 'number' ? timestamp * 1000 : new Date(timestamp).getTime();\n if (!t) return '—';\n const diff = Math.floor((Date.now() - t) / 1000);\n if (diff < 60) return `${diff}s ago`;\n if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;\n if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;\n return `${Math.floor(diff / 86400)}d ago`;\n }\n\n async refresh() {\n await this._fetchSafely();\n if (this.isMounted()) await this.render();\n }\n\n // ── action handlers ────────────────────────────────────────────────\n\n async onActionOpenIncident(event, element) {\n // Don't open the incident view if a nested action button was clicked.\n if (event.target.closest('[data-action=\"resolve-incident\"], [data-action=\"pause-incident\"]')) {\n return;\n }\n const id = element.dataset.id;\n if (!id) return;\n const model = new Incident({ id });\n await model.fetch();\n if (!model.id) return;\n const view = new IncidentView({ model });\n await Modal.show(view, { size: 'xl', header: false });\n }\n\n async onActionResolveIncident(event, element) {\n event.stopPropagation();\n const id = element.dataset.id;\n if (!id) return;\n const ok = await Modal.confirm('Mark this incident as resolved?');\n if (!ok) return;\n const model = new Incident({ id });\n await model.save({ status: 'resolved' });\n await this.refresh();\n }\n\n async onActionPauseIncident(event, element) {\n event.stopPropagation();\n const id = element.dataset.id;\n if (!id) return;\n const model = new Incident({ id });\n await model.save({ status: 'paused' });\n await this.refresh();\n }\n}\n\nexport default PriorityQueueView;\n","import { TicketNote, TicketNoteList } from '@ext/admin/models/Tickets.js';\nimport rest from '@core/Rest.js';\n\nclass TicketNoteAdapter {\n constructor(ticketId) {\n this.ticketId = ticketId;\n this.collection = new TicketNoteList({ params: { parent: this.ticketId, sort: 'created', size: 100} });\n }\n\n async fetch() {\n await this.collection.fetch();\n const messages = this.collection.models.map(note => this.transform(note));\n // Render markdown for system/LLM-generated notes\n await Promise.all(messages.map(async (msg) => {\n if (msg.content) {\n msg.content = await this._renderMarkdown(msg.content);\n }\n }));\n return messages;\n }\n\n transform(note) {\n return {\n id: note.get('id'),\n type: note.get('user') ? 'user_comment' : 'system_event',\n author: {\n id: note.get('user.id'),\n name: note.get('user.display_name') || 'System',\n avatarUrl: note.get('user.avatar.url')\n },\n timestamp: note.get('created'),\n content: note.get('note'),\n attachments: note.get('media') ? [note.get('media')] : []\n };\n }\n\n async addNote(data) {\n const note = new TicketNote();\n const resp = await note.save({\n parent: this.ticketId,\n note: data.text,\n media: data.files && data.files.length > 0 ? data.files[0].id : null\n });\n if (resp.success) {\n await this.collection.fetch();\n }\n return resp;\n }\n\n async _renderMarkdown(markdown) {\n if (!markdown) return '';\n try {\n const resp = await rest.post('/api/docit/render', { markdown });\n const html = resp?.data?.data?.html || resp?.data?.html;\n if (html) return html;\n } catch (_e) { /* API unavailable */ }\n const div = document.createElement('div');\n div.textContent = markdown;\n return `<pre style=\"white-space: pre-wrap;\">${div.innerHTML}</pre>`;\n }\n}\n\nexport default TicketNoteAdapter;\n","import View from '@core/View.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Ticket, TicketForms } from '@ext/admin/models/Tickets.js';\nimport { Incident } from '@ext/admin/models/Incident.js';\nimport { UserList } from '@core/models/User.js';\nimport ChatView from '@core/views/chat/ChatView.js';\nimport TicketNoteAdapter from './adapters/TicketNoteAdapter.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport IncidentView from './IncidentView.js';\nimport rest from '@core/Rest.js';\nimport { openAssistantChat } from '../assistant/AssistantContextChat.js';\n\n\n// ── Helpers ─────────────────────────────────────────────\n\nconst STATUS_CONFIG = {\n new: { badge: 'bg-info', icon: 'bi-bell-fill' },\n open: { badge: 'bg-primary', icon: 'bi-folder2-open' },\n in_progress: { badge: 'bg-warning text-dark', icon: 'bi-gear-fill' },\n pending: { badge: 'bg-secondary', icon: 'bi-pause-circle-fill' },\n resolved: { badge: 'bg-success', icon: 'bi-check-circle-fill' },\n qa: { badge: 'bg-purple', icon: 'bi-clipboard-check' },\n closed: { badge: 'bg-dark', icon: 'bi-x-circle-fill' },\n ignored: { badge: 'bg-secondary', icon: 'bi-eye-slash-fill' }\n};\n\nfunction getStatusBadge(status) {\n const cfg = STATUS_CONFIG[status] || { badge: 'bg-secondary', icon: 'bi-circle' };\n return `<span class=\"badge ${cfg.badge}\"><i class=\"${cfg.icon} me-1\"></i>${(status || '').replace('_', ' ')}</span>`;\n}\n\nasync function renderMarkdown(markdown) {\n if (!markdown) return '';\n try {\n const resp = await rest.post('/api/docit/render', { markdown });\n const html = resp?.data?.data?.html || resp?.data?.html;\n if (html) return html;\n } catch (_e) { /* fallback */ }\n const div = document.createElement('div');\n div.textContent = markdown;\n return `<pre style=\"white-space: pre-wrap;\">${div.innerHTML}</pre>`;\n}\n\n\n// ── Ticket Description Section ──────────────────────────\n\nclass TicketDescriptionSection extends View {\n constructor(options = {}) {\n super({\n className: 'ticket-description-section',\n ...options\n });\n\n this.descriptionHtml = '';\n this.template = `\n <div class=\"card mb-3\">\n <div class=\"card-body\">\n <div class=\"ticket-description-content\">{{{descriptionHtml}}}</div>\n </div>\n </div>\n `;\n }\n\n async onBeforeRender() {\n const description = this.model.get('description') || '';\n this.descriptionHtml = await renderMarkdown(description);\n }\n}\n\n\n// ── Linked Incident Card ────────────────────────────────\n\nclass LinkedIncidentCard extends View {\n constructor(options = {}) {\n super({\n className: 'linked-incident-card',\n ...options\n });\n\n this.incident = options.incident || {};\n this.incidentTitle = this.incident.title || 'Untitled';\n this.incidentId = this.incident.id;\n this.incidentStatus = this.incident.status || 'unknown';\n this.incidentPriority = this.incident.priority;\n this.incidentCategory = this.incident.category || '';\n this.incidentScope = this.incident.scope || '';\n\n this.template = `\n <div class=\"card border-start border-3 border-warning mb-3\">\n <div class=\"card-body py-2 px-3\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center gap-2 flex-grow-1\" style=\"min-width: 0;\">\n <i class=\"bi bi-exclamation-triangle-fill text-warning flex-shrink-0\"></i>\n <div style=\"min-width: 0;\">\n <div class=\"fw-semibold text-truncate\">Incident #{{incidentId}}: {{incidentTitle}}</div>\n <div class=\"text-muted small\">\n {{{statusBadge}}}\n <span class=\"ms-2\">Priority: {{incidentPriority}}</span>\n {{#incidentCategory}}\n <span class=\"ms-2\">{{{incidentCategory|badge}}}</span>\n {{/incidentCategory}}\n </div>\n </div>\n </div>\n <button class=\"btn btn-outline-primary btn-sm flex-shrink-0 ms-2\" data-action=\"open-incident\">\n <i class=\"bi bi-box-arrow-up-right me-1\"></i>Open\n </button>\n </div>\n </div>\n </div>\n `;\n }\n\n async onBeforeRender() {\n this.statusBadge = getStatusBadge(this.incidentStatus);\n }\n\n async onActionOpenIncident() {\n if (!this.incidentId) return;\n try {\n const incident = new Incident({ id: this.incidentId });\n await incident.fetch({ params: { graph: 'detailed' } });\n const view = new IncidentView({ model: incident });\n await Modal.show(view, { size: 'xl', header: false });\n } catch (e) {\n this.getApp()?.toast?.error('Failed to load incident');\n }\n }\n}\n\n\n// ── TicketView ──────────────────────────────────────────\n\nclass TicketView extends View {\n constructor(options = {}) {\n super({\n className: 'ticket-view',\n ...options\n });\n\n this.model = options.model || new Ticket(options.data || {});\n\n this.template = `\n <div class=\"ticket-view-container d-flex flex-column h-100\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start mb-3 flex-shrink-0\">\n <div class=\"flex-grow-1\" style=\"min-width: 0;\">\n <div class=\"d-flex align-items-center gap-2 mb-1\">\n {{{statusBadge}}}\n {{#model.category}}\n <span class=\"badge bg-secondary\">{{model.category}}</span>\n {{/model.category}}\n <span class=\"text-muted small\">Ticket #{{model.id}}</span>\n </div>\n <h4 class=\"mb-1\">{{model.title}}</h4>\n <div class=\"text-muted small d-flex align-items-center gap-3 flex-wrap\">\n <span><i class=\"bi bi-flag-fill me-1\"></i>Priority {{model.priority}}</span>\n {{#assigneeName}}\n <span><i class=\"bi bi-person-fill me-1\"></i>{{assigneeName}}</span>\n {{/assigneeName}}\n {{#model.created}}\n <span><i class=\"bi bi-clock me-1\"></i>{{model.created|relative}}</span>\n {{/model.created}}\n {{#model.modified}}\n <span><i class=\"bi bi-pencil me-1\"></i>{{model.modified|relative}}</span>\n {{/model.modified}}\n </div>\n </div>\n <div class=\"d-flex align-items-center gap-2 flex-shrink-0\">\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"ask-ai\" data-bs-toggle=\"tooltip\" title=\"Chat with AI about this ticket\">\n <i class=\"bi bi-robot me-1\"></i>Ask AI\n </button>\n <div data-container=\"ticket-context-menu\"></div>\n </div>\n </div>\n\n <!-- Linked Incident -->\n <div data-container=\"linked-incident\"></div>\n\n <!-- Description (collapsible) -->\n {{#hasDescription|bool}}\n <div class=\"mb-3\">\n <a class=\"text-muted small d-inline-flex align-items-center gap-1\" data-bs-toggle=\"collapse\" href=\"#ticket-desc-{{model.id}}\" role=\"button\" aria-expanded=\"false\">\n <i class=\"bi bi-chevron-right\"></i>\n <i class=\"bi bi-file-text me-1\"></i>Description\n </a>\n <div class=\"collapse\" id=\"ticket-desc-{{model.id}}\">\n <div data-container=\"ticket-description\" class=\"mt-2\"></div>\n </div>\n </div>\n {{/hasDescription|bool}}\n\n <!-- Chat / Notes -->\n <div class=\"d-flex align-items-center justify-content-between mb-2\">\n <h6 class=\"mb-0 text-muted\"><i class=\"bi bi-chat-left-text me-1\"></i>Notes</h6>\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"refresh-notes\">\n <i class=\"bi bi-arrow-clockwise\"></i>\n </button>\n </div>\n <div class=\"flex-grow-1\" style=\"min-height: 0;\" data-container=\"chat-view\"></div>\n </div>\n `;\n }\n\n async onBeforeRender() {\n this.statusBadge = getStatusBadge(this.model.get('status'));\n const assignee = this.model.get('assignee');\n this.assigneeName = assignee?.display_name || (typeof assignee === 'string' ? assignee : null);\n this.hasDescription = !!this.model.get('description');\n }\n\n async onInit() {\n // Linked Incident\n const incident = this.model.get('incident');\n if (incident && typeof incident === 'object' && incident.id) {\n this.linkedIncident = new LinkedIncidentCard({\n containerId: 'linked-incident',\n incident\n });\n this.addChild(this.linkedIncident);\n }\n\n // Description\n if (this.model.get('description')) {\n this.descriptionView = new TicketDescriptionSection({\n containerId: 'ticket-description',\n model: this.model\n });\n this.addChild(this.descriptionView);\n }\n\n // Chat View\n this.adapter = new TicketNoteAdapter(this.model.get('id'));\n this.chatView = new ChatView({\n containerId: 'chat-view',\n adapter: this.adapter,\n theme: 'compact',\n currentUserId: this.getCurrentUserId(),\n inputPlaceholder: 'Add a note...',\n inputButtonText: 'Add Note'\n });\n this.addChild(this.chatView);\n\n // Context Menu\n const ticketMenu = new ContextMenu({\n containerId: 'ticket-context-menu',\n className: \"context-menu-view header-menu-absolute\",\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit Ticket', action: 'edit-ticket', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'Change Status', action: 'change-status', icon: 'bi-tag' },\n { label: 'Set Priority', action: 'set-priority', icon: 'bi-flag' },\n { label: 'Assign User', action: 'assign-user', icon: 'bi-person' },\n { type: 'divider' },\n { label: 'Close Ticket', action: 'close-ticket', icon: 'bi-x-circle', class: 'text-danger' },\n ]\n }\n });\n this.addChild(ticketMenu);\n }\n\n getCurrentUserId() {\n const currentUser = window.app?.state?.user;\n return currentUser?.id || null;\n }\n\n async onActionRefreshNotes() {\n await this.chatView.refresh();\n this.getApp()?.toast?.success('Notes refreshed');\n }\n\n async onActionAskAi() {\n await openAssistantChat(this, 'incident.Ticket');\n }\n\n // ── Context Menu Actions ──\n\n async onActionEditTicket() {\n const resp = await Modal.modelForm({\n title: `Edit Ticket #${this.model.get('id')}`,\n model: this.model,\n size: 'lg',\n fields: TicketForms.edit.fields\n });\n if (resp) {\n this.render();\n }\n }\n\n async onActionChangeStatus() {\n const statuses = ['new', 'open', 'in_progress', 'pending', 'resolved', 'qa', 'closed', 'ignored'];\n const result = await Modal.form({\n title: 'Change Status',\n icon: 'bi-tag',\n size: 'sm',\n fields: [{\n name: 'status', label: 'Status', type: 'select',\n options: statuses.map(s => ({ value: s, label: s.replace(/_/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()) })),\n value: this.model.get('status'),\n required: true\n }]\n });\n if (!result) return;\n await this.model.save({ status: result.status });\n this.render();\n }\n\n async onActionSetPriority() {\n const result = await Modal.form({\n title: 'Set Priority',\n icon: 'bi-flag',\n size: 'sm',\n fields: [{\n name: 'priority', label: 'Priority', type: 'select',\n value: this.model.get('priority') || 5,\n options: [\n { value: 10, label: '10 — Critical' },\n { value: 9, label: '9 — Severe' },\n { value: 8, label: '8 — High' },\n { value: 7, label: '7 — Elevated' },\n { value: 5, label: '5 — Normal' },\n { value: 3, label: '3 — Low' },\n { value: 1, label: '1 — Info' }\n ],\n required: true\n }]\n });\n if (!result) return;\n await this.model.save({ priority: parseInt(result.priority) });\n this.render();\n }\n\n async onActionAssignUser() {\n const data = await Modal.form({\n title: 'Assign User',\n icon: 'bi-person-plus',\n size: 'sm',\n fields: [{\n name: 'assignee', type: 'collection', label: 'User',\n Collection: UserList, labelField: 'display_name', valueField: 'id',\n required: true, cols: 12,\n value: this.model.get('assignee')\n }]\n });\n if (!data) return;\n await this.model.save({ assignee: data.assignee });\n this.getApp()?.toast?.success('Ticket assigned');\n this.render();\n }\n\n async onActionCloseTicket() {\n const confirmed = await Modal.confirm(\n `Close ticket #${this.model.get('id')}?`,\n 'Close Ticket',\n { confirmText: 'Close', confirmClass: 'btn-warning' }\n );\n if (!confirmed) return;\n await this.model.save({ status: 'closed' });\n this.getApp()?.toast?.success('Ticket closed');\n this.render();\n }\n}\n\nTicket.VIEW_CLASS = TicketView;\n\nexport default TicketView;\n","/**\n * TicketsQueueView — top open tickets that need a human owner.\n *\n * Sibling to PriorityQueueView (which shows incidents). Tickets are\n * the human-actionable layer above incidents — an incident is \"the\n * system noticed something\"; a ticket is \"a person needs to do\n * something about it\".\n *\n * Lists up to 8 tickets matching status=new, sorted by priority desc\n * then recency. Each row: priority pill, title, age, assignee, status.\n * Click row opens TicketView in a modal. Inline actions for users with\n * manage_security: resolve / pause.\n */\n\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { Ticket, TicketList } from '@ext/admin/models/Tickets.js';\nimport TicketView from '../TicketView.js';\n\nclass TicketsQueueView extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-tickets-queue ${options.className || ''}`.trim()\n });\n this.allowActions = options.allowActions !== false;\n this.size = options.size || 8;\n // Show both untriaged (new) AND in-flight (open) tickets so the\n // queue reflects \"anything a human still needs to do\". Resolved /\n // paused / ignored are excluded.\n this.collection = new TicketList({\n params: {\n status__in: 'new,open',\n sort: '-priority,-created',\n size: this.size\n }\n });\n // Mustache-resolved state on `this`.\n this.items = [];\n this.isLoading = true;\n this.hasError = false;\n this.error = '';\n this.isEmpty = false;\n this.showActions = this.allowActions;\n }\n\n async getTemplate() {\n return `\n <div class=\"card sd-card\">\n <div class=\"card-header bg-transparent border-0 d-flex justify-content-between align-items-start\">\n <div>\n <h3 class=\"card-title sd-card-title mb-0\">Open Tickets</h3>\n <span class=\"card-subtitle text-muted small\">Human-actionable items</span>\n </div>\n <a class=\"text-muted small\" href=\"?page=system/tickets\">All tickets <i class=\"bi bi-arrow-right-short\"></i></a>\n </div>\n <div class=\"card-body p-0\" data-region=\"list\">\n {{#isLoading|bool}}<div class=\"p-4 text-center text-muted small\"><i class=\"bi bi-hourglass-split me-1\"></i>Loading…</div>{{/isLoading|bool}}\n {{#hasError|bool}}<div class=\"p-3\"><div class=\"alert alert-warning small mb-0\">{{error}}</div></div>{{/hasError|bool}}\n {{#isEmpty|bool}}<div class=\"p-4 text-center text-success small\"><i class=\"bi bi-check-circle me-1\"></i>No open tickets.</div>{{/isEmpty|bool}}\n <ol class=\"sd-priority-list list-unstyled mb-0\">\n {{#items}}\n <li class=\"sd-pri-row\" data-action=\"open-ticket\" data-id=\"{{id}}\">\n <span class=\"sd-pri sd-pri-{{severityClass}}\">{{severityLabel}} {{priority}}</span>\n <div class=\"sd-pri-body\">\n <span class=\"sd-pri-title\">{{title}}</span>\n <span class=\"sd-pri-meta\">{{ageLabel}} · {{assigneeLabel}}</span>\n </div>\n {{#showActions|bool}}\n <span class=\"sd-pri-actions\">\n <button type=\"button\" class=\"btn btn-sm btn-link text-success p-1\" data-action=\"resolve-ticket\" data-id=\"{{id}}\" title=\"Resolve\"><i class=\"bi bi-check2\"></i></button>\n <button type=\"button\" class=\"btn btn-sm btn-link text-secondary p-1\" data-action=\"pause-ticket\" data-id=\"{{id}}\" title=\"Pause\"><i class=\"bi bi-pause\"></i></button>\n </span>\n {{/showActions|bool}}\n </li>\n {{/items}}\n </ol>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n await this._fetchSafely();\n }\n\n async _fetchSafely() {\n try {\n await this.collection.fetch();\n const models = this.collection.models || [];\n this.items = models.map(m => this._rowFor(m));\n this.isLoading = false;\n this.hasError = false;\n this.error = '';\n this.isEmpty = this.items.length === 0;\n } catch (err) {\n console.warn('[TicketsQueueView] fetch failed:', err);\n this.items = [];\n this.isLoading = false;\n this.hasError = true;\n this.error = 'Could not load tickets.';\n this.isEmpty = false;\n }\n }\n\n _rowFor(model) {\n const priority = parseInt(model.get('priority'), 10) || 0;\n const sevClass = priority >= 12 ? 'critical'\n : (priority >= 8 ? 'high' : (priority >= 4 ? 'warn' : 'info'));\n const sevLabel = sevClass === 'critical' ? 'CRIT' : sevClass.toUpperCase();\n const assignee = model.get('assignee');\n const assigneeName = assignee?.display_name || assignee?.username;\n return {\n id: model.id,\n title: model.get('title') || `Ticket #${model.id}`,\n priority,\n severityClass: sevClass,\n severityLabel: sevLabel,\n ageLabel: this._relativeTime(model.get('created')),\n assigneeLabel: assigneeName ? `assigned to ${assigneeName}` : 'unassigned'\n };\n }\n\n _relativeTime(timestamp) {\n if (!timestamp) return '—';\n const t = typeof timestamp === 'number' ? timestamp * 1000 : new Date(timestamp).getTime();\n if (!t) return '—';\n const diff = Math.floor((Date.now() - t) / 1000);\n if (diff < 60) return `${diff}s ago`;\n if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;\n if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;\n return `${Math.floor(diff / 86400)}d ago`;\n }\n\n async refresh() {\n await this._fetchSafely();\n if (this.isMounted()) await this.render();\n }\n\n // ── action handlers ────────────────────────────────────────────────\n\n async onActionOpenTicket(event, element) {\n if (event.target.closest('[data-action=\"resolve-ticket\"], [data-action=\"pause-ticket\"]')) {\n return;\n }\n const id = element.dataset.id;\n if (!id) return;\n const model = new Ticket({ id });\n await model.fetch();\n if (!model.id) return;\n const view = new TicketView({ model });\n await Modal.show(view, { size: 'xl', header: false });\n }\n\n async onActionResolveTicket(event, element) {\n event.stopPropagation();\n const id = element.dataset.id;\n if (!id) return;\n const ok = await Modal.confirm('Mark this ticket as resolved?');\n if (!ok) return;\n const model = new Ticket({ id });\n await model.save({ status: 'resolved' });\n await this.refresh();\n }\n\n async onActionPauseTicket(event, element) {\n event.stopPropagation();\n const id = element.dataset.id;\n if (!id) return;\n const model = new Ticket({ id });\n await model.save({ status: 'paused' });\n await this.refresh();\n }\n}\n\nexport default TicketsQueueView;\n","/**\n * ThreatCompositionChart — single stacked bar chart that condenses\n * incident_events / firewall:blocks / bouncer:blocks / auth:failures\n * into one daily-bucketed view.\n *\n * Replaces what would otherwise be 4 separate side-by-side mini charts.\n * Range toggle: 7D / 30D / 90D.\n *\n * Click any bar emits `composition:bar-click` with the bucket date —\n * the parent dashboard wires this to a Day Drill-Down drawer.\n */\n\nimport View from '@core/View.js';\nimport MetricsChart from '@ext/charts/MetricsChart.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nconst SLUGS = ['incident_events', 'firewall:blocks', 'bouncer:blocks', 'auth:failures'];\n\nclass ThreatCompositionChart extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-composition ${options.className || ''}`.trim()\n });\n this.range = options.range || '30d';\n this._reflectRange();\n }\n\n _reflectRange() {\n this.is7d = this.range === '7d';\n this.is30d = this.range === '30d';\n this.is90d = this.range === '90d';\n }\n\n async getTemplate() {\n return `\n <div class=\"card sd-card\">\n <div class=\"card-header bg-transparent border-0 d-flex justify-content-between align-items-start\">\n <div>\n <h3 class=\"card-title sd-card-title mb-0\">Threat Composition</h3>\n <span class=\"card-subtitle text-muted small\">Daily, stacked · click any day to drill in</span>\n </div>\n <div class=\"btn-group btn-group-sm\" role=\"group\" aria-label=\"Time range\">\n <button type=\"button\" class=\"btn btn-outline-secondary {{#is7d|bool}}active{{/is7d|bool}}\" data-action=\"set-range\" data-range=\"7d\">7D</button>\n <button type=\"button\" class=\"btn btn-outline-secondary {{#is30d|bool}}active{{/is30d|bool}}\" data-action=\"set-range\" data-range=\"30d\">30D</button>\n <button type=\"button\" class=\"btn btn-outline-secondary {{#is90d|bool}}active{{/is90d|bool}}\" data-action=\"set-range\" data-range=\"90d\">90D</button>\n </div>\n </div>\n <div class=\"card-body\" data-container=\"chart-host\"></div>\n </div>\n `;\n }\n\n async onInit() {\n this.chart = new MetricsChart({\n containerId: 'chart-host',\n slugs: SLUGS,\n account: 'incident',\n granularity: 'days',\n chartType: 'bar', // SeriesChart defaults bar = stacked\n defaultDateRange: this.range,\n compactHeader: true,\n showGranularity: false,\n showTypeSwitch: false,\n showDateRange: false,\n legendPosition: 'bottom', // mock contract: legend below chart\n height: 280,\n yAxis: { label: 'Count', beginAtZero: true },\n tooltip: { y: 'number:0' },\n colors: [\n 'rgba(13, 202, 240, 0.85)', // events — info blue\n 'rgba(220, 53, 69, 0.85)', // firewall — danger red\n 'rgba(253, 126, 20, 0.85)', // bouncer — orange\n 'rgba(179, 136, 255, 0.85)' // auth — purple\n ],\n title: ''\n });\n this.addChild(this.chart);\n\n // Re-emit bar clicks so the dashboard page can open a drawer.\n this.chart.on?.('chart:click', (data) => {\n this.emit?.('composition:bar-click', data);\n this._openDayDrawer(data);\n });\n }\n\n async onActionSetRange(event, element) {\n const range = element.dataset.range;\n if (!range || range === this.range) return;\n this.range = range;\n this._reflectRange();\n this.chart?.setQuickRange?.(range);\n await this.chart?.fetchData?.();\n await this.render();\n }\n\n async refresh() {\n return this.chart?.fetchData?.();\n }\n\n _openDayDrawer({ x, datasets } = {}) {\n // x is the label (e.g. \"Apr 22\") — the chart doesn't pass back the\n // raw date; we use the label as title context. Datasets carries\n // per-series values for that bucket.\n const title = x ? `Day Detail · ${x}` : 'Day Detail';\n const breakdown = (datasets || []).map(d =>\n `<div class=\"d-flex justify-content-between border-bottom py-1\">\n <span class=\"text-muted small\">${this._esc(d.label || '')}</span>\n <span class=\"sd-mono\">${Number(d.value || 0).toLocaleString()}</span>\n </div>`\n ).join('');\n\n Modal.drawer({\n eyebrow: 'Composition',\n title,\n meta: [\n { icon: 'bi bi-bar-chart-line', text: SLUGS.length + ' series' }\n ],\n body: `\n <p class=\"small text-muted mb-2\">Per-series totals for this bucket. To drill into the underlying events, open the Events table.</p>\n ${breakdown || '<div class=\"text-muted small\">No breakdown available.</div>'}\n <div class=\"mt-3\">\n <a href=\"?page=system/events\" class=\"btn btn-sm btn-outline-primary\"><i class=\"bi bi-list-ul me-1\"></i>Open Events</a>\n </div>\n `,\n size: 'md'\n });\n }\n\n _esc(s) {\n const d = document.createElement('div');\n d.textContent = String(s ?? '');\n return d.innerHTML;\n }\n}\n\nexport default ThreatCompositionChart;\n","/**\n * GeographyPanel — country leaderboard with slug-family selector.\n *\n * Default render is COMPACT — only the leaderboard list shows, plus a\n * \"View Map\" toolbar button that opens MetricsCountryMapView in an XL\n * modal. The map is a heavy widget that competes for screen real\n * estate; keeping it modal-on-demand cleans up the dashboard.\n *\n * Pass `inlineMap: true` to render the map in the card itself (legacy\n * behavior).\n */\n\nimport View from '@core/View.js';\nimport { MetricsCountryMapView } from '@ext/map/index.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nconst FAMILIES = [\n { key: 'events', label: 'Events', category: 'incident_events_by_country' },\n { key: 'incidents',label: 'Incidents', category: 'incidents_by_country' },\n { key: 'firewall', label: 'Firewall', category: 'firewall_blocks_by_country' },\n { key: 'logins', label: 'Logins', category: 'logins' }\n];\n\nclass GeographyPanel extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-geography ${options.className || ''}`.trim()\n });\n this.activeFamily = options.family || 'events';\n this.inlineMap = options.inlineMap === true;\n this._reflectFamilies();\n // State for the leaderboard list, populated when the map fetches.\n this.leaderboard = [];\n this.leaderboardEmpty = true;\n }\n\n _reflectFamilies() {\n this.families = FAMILIES.map(f => ({ ...f, active: f.key === this.activeFamily }));\n }\n\n async getTemplate() {\n // Inline-map mode: original side-by-side layout with the map in\n // the card. Compact mode (default): leaderboard only + a toolbar\n // button that opens the map in an XL modal.\n const inlineMapHtml = this.inlineMap ? `\n <div class=\"sd-geo-grid\">\n <div class=\"sd-geo-map-cell\" data-container=\"inline-map\"></div>\n ${this._leaderboardHtml()}\n </div>` : `\n ${this._leaderboardHtml()}`;\n\n return `\n <div class=\"card sd-card h-100\">\n <div class=\"card-header bg-transparent border-0 d-flex justify-content-between align-items-start\">\n <div>\n <h3 class=\"card-title sd-card-title mb-0\">Geography</h3>\n <span class=\"card-subtitle text-muted small\">Activity by country, last 7 days</span>\n </div>\n <div class=\"d-flex align-items-center gap-2\">\n <div class=\"btn-group btn-group-sm\" role=\"group\" aria-label=\"Slug family\">\n {{#families}}\n <button type=\"button\"\n class=\"btn btn-outline-secondary {{#active|bool}}active{{/active|bool}}\"\n data-action=\"set-family\"\n data-family=\"{{key}}\">{{label}}</button>\n {{/families}}\n </div>\n ${this.inlineMap ? '' : `\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" data-action=\"show-map\" title=\"Show map\">\n <i class=\"bi bi-globe-americas\"></i>\n </button>`}\n </div>\n </div>\n <div class=\"card-body p-0\">\n ${inlineMapHtml}\n </div>\n </div>\n `;\n }\n\n _leaderboardHtml() {\n return `\n <ol class=\"sd-geo-leader sd-geo-leader-full list-unstyled mb-0\">\n {{#leaderboardEmpty|bool}}\n <li class=\"px-3 py-3 text-muted small\">No country activity in window.</li>\n {{/leaderboardEmpty|bool}}\n {{#leaderboard}}\n <li class=\"sd-geo-leader-row\" data-action=\"open-country\" data-cc=\"{{cc}}\" data-name=\"{{name}}\" data-total=\"{{total}}\">\n <span class=\"sd-geo-cc sd-mono\">{{cc}}</span>\n <span class=\"sd-geo-name\">{{name}}</span>\n <span class=\"sd-geo-num sd-mono\">{{total}}</span>\n </li>\n {{/leaderboard}}\n </ol>\n `;\n }\n\n async onInit() {\n // Inline map only when explicitly opted in. Compact mode mounts\n // the map lazily inside the modal when \"Show map\" is clicked.\n if (this.inlineMap) {\n this.map = new MetricsCountryMapView({\n containerId: 'inline-map',\n category: this._currentCategory(),\n account: 'incident',\n granularity: 'days',\n maxCountries: 20,\n metricLabel: this._currentLabel(),\n height: 360,\n mapStyle: 'dark',\n mapOptions: { interactive: false }\n });\n this.addChild(this.map);\n }\n await this._fetchLeaderboard();\n }\n\n /**\n * Open the world map in an XL modal. Built fresh each time so the\n * MapLibre canvas always sizes correctly to the modal viewport\n * (the lib doesn't always re-measure cleanly when the host\n * container resizes).\n */\n async onActionShowMap() {\n const map = new MetricsCountryMapView({\n category: this._currentCategory(),\n account: 'incident',\n granularity: 'days',\n maxCountries: 30,\n metricLabel: this._currentLabel(),\n height: 560,\n mapStyle: 'dark',\n // Allow interaction in the modal — the map is the focal\n // point there, not a thumbnail next to other widgets.\n mapOptions: { interactive: true }\n });\n await Modal.drawer({\n eyebrow: 'Geography',\n title: this._currentLabel() + ' by Country',\n meta: [\n { icon: 'bi bi-calendar3', text: 'Last 7 days' },\n { icon: 'bi bi-cursor', text: 'Drag, zoom, click markers' }\n ],\n view: map,\n size: 'xl'\n });\n }\n\n async _fetchLeaderboard() {\n const rest = this.getApp()?.rest;\n if (!rest) {\n this.leaderboard = [];\n this.leaderboardEmpty = true;\n return;\n }\n try {\n const drStart = Math.floor((Date.now() - 7 * 86400000) / 1000);\n const resp = await rest.GET('/api/metrics/fetch', {\n category: this._currentCategory(),\n account: 'incident',\n granularity: 'days',\n with_labels: true,\n dr_start: drStart,\n _: Date.now()\n });\n const metrics = resp?.data?.data?.data || {};\n this.leaderboard = this._buildLeaderboard(metrics);\n } catch (err) {\n console.warn('[GeographyPanel] leaderboard fetch failed:', err);\n this.leaderboard = [];\n }\n this.leaderboardEmpty = this.leaderboard.length === 0;\n }\n\n _buildLeaderboard(metrics) {\n // Each metric key looks like 'incident_events:country:US'. Sum each\n // series, extract the trailing :CC, sort descending, take top 10.\n const rows = [];\n for (const [slug, series] of Object.entries(metrics)) {\n const cc = String(slug).split(':').pop()?.toUpperCase();\n if (!cc || cc.length !== 2) continue;\n const total = (Array.isArray(series) ? series : []).reduce((s, v) => s + (Number(v) || 0), 0);\n if (total <= 0) continue;\n rows.push({ cc, name: cc, total });\n }\n rows.sort((a, b) => b.total - a.total);\n return rows.slice(0, 10);\n }\n\n async onActionSetFamily(event, element) {\n const key = element.dataset.family;\n if (!key || key === this.activeFamily) return;\n this.activeFamily = key;\n this._reflectFamilies();\n if (this.map) {\n this.map.category = this._currentCategory();\n this.map.metricLabel = this._currentLabel();\n }\n await this._fetchLeaderboard();\n await this.render();\n if (this.map) await this.map.refresh();\n }\n\n async refresh() {\n await this._fetchLeaderboard();\n if (this.isMounted()) await this.render();\n if (this.map) return this.map.refresh();\n }\n\n async onActionOpenCountry(event, element) {\n const cc = element.dataset.cc;\n const name = element.dataset.name;\n const total = element.dataset.total;\n if (!cc) return;\n this.openCountryDrawer({ cc, name, total });\n }\n\n _currentCategory() {\n return FAMILIES.find(f => f.key === this.activeFamily)?.category || FAMILIES[0].category;\n }\n\n _currentLabel() {\n return FAMILIES.find(f => f.key === this.activeFamily)?.label || FAMILIES[0].label;\n }\n\n /**\n * Convenience for opening the country drawer — kept for the parent\n * page to call if it gets a click event from the map. (The map view\n * doesn't currently emit one — leaderboard click is the click target.)\n */\n openCountryDrawer({ cc, name, total }) {\n const safeCc = this._esc(cc);\n Modal.drawer({\n eyebrow: 'Country Detail',\n title: `${cc} · ${name || cc}`,\n meta: [\n { icon: 'bi bi-graph-up', text: `${total ?? '—'} ${this._currentLabel().toLowerCase()} / 7d` }\n ],\n body: `\n <p class=\"small text-muted\">\n For a fuller breakdown of events from this country, open the Events\n table filtered by country code.\n </p>\n <div class=\"mt-2\">\n <a href=\"?page=system/events&country_code=${encodeURIComponent(cc)}\"\n class=\"btn btn-sm btn-outline-primary\">\n <i class=\"bi bi-list-ul me-1\"></i>Open Events from ${safeCc}\n </a>\n </div>\n `,\n size: 'md'\n });\n }\n\n _esc(s) {\n const d = document.createElement('div');\n d.textContent = String(s ?? '');\n return d.innerHTML;\n }\n}\n\nexport default GeographyPanel;\n","/**\n * DistributionStrip — three-card row of distributions:\n * 1. Incidents by Status (PieChart donut)\n * 2. Incidents by Priority bucket (HTML/CSS horizontal bars)\n * 3. Bouncer Funnel (HTML/CSS horizontal bars, descending)\n *\n * Funnel and priority-buckets render inline (no separate FunnelChart\n * class) — they're three bars each, simpler in markup than as a chart.\n *\n * Data: fetches a recent page of incidents and aggregates client-side.\n * No backend group_by required. Bouncer funnel reads three values from\n * /api/metrics/series.\n */\n\nimport View from '@core/View.js';\nimport PieChart from '@ext/charts/PieChart.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nconst STATUS_COLORS = {\n new: 'rgba(13, 202, 240, 0.85)',\n open: 'rgba(13, 110, 253, 0.85)',\n investigating: 'rgba(255, 193, 7, 0.85)',\n paused: 'rgba(108, 117, 125, 0.85)',\n resolved: 'rgba(25, 135, 84, 0.85)',\n closed: 'rgba(73, 80, 87, 0.85)',\n ignored: 'rgba(108, 117, 125, 0.6)',\n pending: 'rgba(173, 181, 189, 0.85)'\n};\n\nconst PRIORITY_BUCKETS = [\n { key: 'critical', label: 'Critical', range: '12+', color: 'rgba(220, 53, 69, 0.85)', match: p => p >= 12 },\n { key: 'high', label: 'High', range: '8–11', color: 'rgba(253, 126, 20, 0.85)', match: p => p >= 8 && p < 12 },\n { key: 'warn', label: 'Warn', range: '4–7', color: 'rgba(255, 193, 7, 0.85)', match: p => p >= 4 && p < 8 },\n { key: 'info', label: 'Info', range: '0–3', color: 'rgba(13, 202, 240, 0.85)', match: p => p < 4 }\n];\n\nclass DistributionStrip extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-distributions ${options.className || ''}`.trim()\n });\n // State on `this` for Mustache resolution.\n this._statusData = [];\n this.priorityRows = [];\n this.priorityEmpty = true;\n this.funnelRows = [];\n this.funnelEmpty = true;\n }\n\n async getTemplate() {\n return `\n <div class=\"row g-3\">\n <div class=\"col-lg-4\">\n <div class=\"card sd-card h-100\">\n <div class=\"card-header bg-transparent border-0\">\n <h3 class=\"card-title sd-card-title mb-0\">Incidents by Status</h3>\n </div>\n <div class=\"card-body\" data-container=\"status-donut\"></div>\n </div>\n </div>\n <div class=\"col-lg-4\">\n <div class=\"card sd-card h-100\">\n <div class=\"card-header bg-transparent border-0\">\n <h3 class=\"card-title sd-card-title mb-0\">Priority Buckets</h3>\n </div>\n <div class=\"card-body\">\n {{#priorityEmpty|bool}}\n <div class=\"text-muted small\">No incidents in window.</div>\n {{/priorityEmpty|bool}}\n {{^priorityEmpty|bool}}\n <ul class=\"list-unstyled mb-0 sd-bucket-list\">\n {{#priorityRows}}\n <li class=\"sd-bucket-row\" data-action=\"open-priority\" data-bucket=\"{{key}}\">\n <span class=\"sd-bucket-label\" style=\"color:{{color}};\">\n {{label}}<span class=\"sd-bucket-range\">{{range}}</span>\n </span>\n <span class=\"sd-bucket-bar\"><span style=\"width:{{percent}}%; background:{{color}};\"></span></span>\n <span class=\"sd-bucket-num sd-mono\">{{value}}</span>\n </li>\n {{/priorityRows}}\n </ul>\n {{/priorityEmpty|bool}}\n </div>\n </div>\n </div>\n <div class=\"col-lg-4\">\n <div class=\"card sd-card h-100\">\n <div class=\"card-header bg-transparent border-0\">\n <h3 class=\"card-title sd-card-title mb-0\">Bouncer Funnel</h3>\n <span class=\"card-subtitle text-muted small\">Last 7 days</span>\n </div>\n <div class=\"card-body\">\n <div class=\"sd-funnel\">\n {{#funnelRows}}\n <div class=\"sd-funnel-row\">\n <div class=\"sd-funnel-bar\">\n <span class=\"sd-funnel-fill\" style=\"width:{{percent}}%; background:{{color}};\">{{label}}</span>\n </div>\n <span class=\"sd-funnel-num sd-mono\">{{value}}</span>\n </div>\n {{/funnelRows}}\n </div>\n {{#funnelEmpty|bool}}\n <div class=\"text-muted small\">No bouncer activity in window.</div>\n {{/funnelEmpty|bool}}\n </div>\n </div>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n this.statusDonut = new PieChart({\n containerId: 'status-donut',\n data: [],\n cutout: 0.6,\n width: 200,\n height: 200,\n // Bottom legend keeps the card responsive — right-positioned\n // legend competes for horizontal space and overflows the card\n // edge below ~1500px viewport (where the 3-col Distribution\n // row collapses each card to a narrow column).\n legendPosition: 'bottom',\n centerLabel: ({ total }) => total,\n centerSubLabel: 'TOTAL'\n });\n this.statusDonut.on?.('chart:click', ({ slice }) => this._openStatusDrawer(slice));\n this.addChild(this.statusDonut);\n\n // Fetch BEFORE first render so {{priorityRows}} / {{funnelRows}}\n // resolve from this on the first template pass.\n await this._fetch();\n }\n\n async _fetch() {\n const rest = this.getApp()?.rest;\n if (!rest) return;\n\n // Status donut + priority buckets via the generic _mode=top\n // aggregation surface. One round-trip each — no incident-page\n // download + client-side aggregation.\n const [statusRows, priorityRows] = await Promise.all([\n this._fetchTopByField('status'),\n this._fetchTopByField('priority')\n ]);\n\n const totalIncidents = statusRows.reduce((s, r) => s + r.value, 0);\n this._statusData = this._buildStatusSlices(statusRows);\n this.priorityRows = this._bucketByPriority(priorityRows);\n // Hide the rows entirely when the backend reports zero incidents.\n // Otherwise show the bucket scaffolding even when individual tiers\n // are 0 — useful \"no Critical right now\" at-a-glance.\n this.priorityEmpty = totalIncidents === 0;\n\n // Bouncer funnel — sum the last 7 days of each stage so the\n // funnel reflects cumulative activity, not just today's bucket.\n // /api/metrics/series returns the current bucket only, which\n // shows zeros if there's been no bouncer traffic today even\n // when the week has plenty.\n try {\n const drStart = Math.floor((Date.now() - 7 * 86400000) / 1000);\n const resp = await rest.GET('/api/metrics/fetch', {\n slugs: 'bouncer:assessments,bouncer:monitors,bouncer:blocks',\n account: 'incident',\n granularity: 'days',\n with_labels: true,\n dr_start: drStart,\n _: Date.now()\n });\n // /api/metrics/fetch nests one level deeper than /series:\n // resp.data = { status, data: { labels:[], data: { slug: [...values] } } }\n const seriesData = resp?.data?.data?.data || {};\n const sums = {};\n for (const [slug, values] of Object.entries(seriesData)) {\n sums[slug] = (Array.isArray(values) ? values : []).reduce((s, v) => s + (Number(v) || 0), 0);\n }\n this.funnelRows = this._buildFunnel(sums);\n } catch (err) {\n console.warn('[DistributionStrip] bouncer fetch failed:', err);\n this.funnelRows = [];\n }\n this.funnelEmpty = this.funnelRows.length === 0 || this.funnelRows.every(r => r.value === 0);\n\n // Donut updates directly via setData (no need to wait for render).\n this.statusDonut?.setData(this._statusData);\n }\n\n async refresh() {\n await this._fetch();\n if (this.isMounted()) await this.render();\n }\n\n /**\n * Single _mode=top fetch returning [{name, value}] for any field\n * on /api/incident/incident.\n */\n async _fetchTopByField(field) {\n const rest = this.getApp()?.rest;\n if (!rest) return [];\n try {\n const resp = await rest.GET('/api/incident/incident', {\n _mode: 'top',\n _field: field,\n _size: 50, // status has ~7 values; priority is 1..12 — generous cap\n _: Date.now()\n });\n const rows = resp?.data?.data;\n if (!Array.isArray(rows)) return [];\n return rows.map(r => ({ key: String(r.key ?? ''), value: Number(r.value) || 0 }));\n } catch (err) {\n console.warn(`[DistributionStrip] _mode=top fetch failed for ${field}:`, err);\n return [];\n }\n }\n\n /** Map status rows from _mode=top to donut slices with colors. */\n _buildStatusSlices(rows) {\n return rows\n .filter(r => r.key)\n .map(r => {\n const k = r.key.toLowerCase();\n return {\n label: k.charAt(0).toUpperCase() + k.slice(1),\n value: r.value,\n color: STATUS_COLORS[k] || 'rgba(108, 117, 125, 0.6)'\n };\n })\n .sort((a, b) => b.value - a.value);\n }\n\n /**\n * Bucket priority rows from _mode=top (one row per priority value\n * 1..12) into the 4 severity tiers (Critical/High/Warn/Info).\n */\n _bucketByPriority(rows) {\n const buckets = PRIORITY_BUCKETS.map(b => ({ ...b, value: 0 }));\n for (const row of rows) {\n const p = parseInt(row.key, 10);\n if (!Number.isFinite(p)) continue;\n const bucket = buckets.find(b => b.match(p));\n if (bucket) bucket.value += row.value;\n }\n const max = Math.max(1, ...buckets.map(b => b.value));\n return buckets.map(b => ({\n key: b.key,\n label: b.label,\n range: b.range,\n color: b.color,\n value: b.value,\n percent: Math.round((b.value / max) * 100)\n }));\n }\n\n _buildFunnel(seriesData) {\n const stages = [\n { key: 'bouncer:assessments', label: 'Assessments', color: 'rgba(76, 201, 240, 0.95)' },\n { key: 'bouncer:monitors', label: 'Monitors', color: 'rgba(245, 165, 36, 0.95)' },\n { key: 'bouncer:blocks', label: 'Blocks', color: 'rgba(255, 90, 90, 0.95)' }\n ];\n const values = stages.map(s => ({ ...s, value: Number(seriesData[s.key] ?? 0) }));\n // If all zero, return empty so the empty-state message renders cleanly\n // (instead of three 0-width bars with vertically-wrapped labels).\n if (values.every(v => v.value === 0)) return [];\n const max = Math.max(1, ...values.map(s => s.value));\n return values.map(s => ({\n ...s,\n value: s.value.toLocaleString(), // thousand separators\n percent: Math.max(12, Math.round((s.value / max) * 100)) // floor 12% so labels don't squish\n }));\n }\n\n _openStatusDrawer(slice) {\n if (!slice) return;\n const status = String(slice.label).toLowerCase();\n const safeLabel = this._esc(slice.label);\n Modal.drawer({\n eyebrow: 'Status Filter',\n title: `Incidents · ${slice.label}`,\n meta: [\n { icon: 'bi bi-pie-chart', text: `${slice.value} (${slice.pct.toFixed(1)}%)` }\n ],\n body: `\n <p class=\"small text-muted\">View the full incident list filtered by this status.</p>\n <a href=\"?page=system/incidents&status=${encodeURIComponent(status)}\" class=\"btn btn-sm btn-outline-primary\">\n <i class=\"bi bi-list-ul me-1\"></i>Open Incidents (${safeLabel})\n </a>\n `,\n size: 'md'\n });\n }\n\n _esc(s) {\n const d = document.createElement('div');\n d.textContent = String(s ?? '');\n return d.innerHTML;\n }\n\n async onActionOpenPriority(event, element) {\n const bucket = element.dataset.bucket;\n if (!bucket) return;\n const def = PRIORITY_BUCKETS.find(b => b.key === bucket);\n if (!def) return;\n // Build the ?priority__gte=...&priority__lte=... filter from the bucket range.\n const [lo, hi] = def.range === '12+' ? [12, null] : def.range.split('–').map(Number);\n const params = `priority__gte=${lo}` + (hi != null ? `&priority__lte=${hi}` : '');\n Modal.drawer({\n eyebrow: 'Priority Bucket',\n title: `Incidents · ${def.label} (${def.range})`,\n view: null,\n body: `\n <p class=\"small text-muted\">View the full incident list filtered by priority.</p>\n <a href=\"?page=system/incidents&${params}\" class=\"btn btn-sm btn-outline-primary\">\n <i class=\"bi bi-list-ul me-1\"></i>Open Incidents\n </a>\n `,\n size: 'sm'\n });\n }\n}\n\nexport default DistributionStrip;\n","/**\n * TopSourcesPanel — two side-by-side rank lists:\n * 1. Top Source IPs (last 7d)\n * 2. Top Categories (last 7d)\n *\n * Backend-aggregated via the generic _mode=top surface on\n * /api/incident/event. One round-trip per list, no client-side\n * fallback — the backend does the GROUP BY + ORDER BY + LIMIT.\n *\n * Each row click opens a filtered events drawer. If `allowBlock` is\n * true (manage_security), Top IPs gets an inline \"Block IP\" action.\n */\n\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\n// Escape backend-sourced values before interpolating them into raw HTML\n// strings passed as Modal.drawer's `body:` parameter. The body is injected\n// as innerHTML by ModalView, so any unescaped value is a stored-XSS vector.\nconst escHtml = (s) => {\n const d = document.createElement('div');\n d.textContent = String(s ?? '');\n return d.innerHTML;\n};\n\nconst WINDOW_DAYS = 7;\nconst TOP_N = 10;\n\n// Categories to exclude from the Top Categories list. Operational /\n// infra-noise categories that would otherwise dominate the rankings\n// without telling the operator anything actionable. Pre-filters\n// server-side via category__not_in so the GROUP BY never sees them.\nconst DEFAULT_CATEGORY_EXCLUDES = ['ossec'];\n\nclass TopSourcesPanel extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-top-sources ${options.className || ''}`.trim()\n });\n this.allowBlock = options.allowBlock !== false;\n this.excludeCategories = Array.isArray(options.excludeCategories)\n ? options.excludeCategories\n : DEFAULT_CATEGORY_EXCLUDES;\n // State on `this` for Mustache resolution.\n this.ips = [];\n this.cats = [];\n this.ipsEmpty = true;\n this.catsEmpty = true;\n }\n\n async getTemplate() {\n return `\n <div class=\"row g-3\">\n <div class=\"col-lg-6\">\n <div class=\"card sd-card h-100\">\n <div class=\"card-header bg-transparent border-0\">\n <h3 class=\"card-title sd-card-title mb-0\">Top Source IPs</h3>\n <span class=\"card-subtitle text-muted small\">Last ${WINDOW_DAYS} days</span>\n </div>\n <ul class=\"list-unstyled mb-0 sd-rank-list\">\n {{#ipsEmpty|bool}}<li class=\"px-3 py-4 text-muted small\">No source IPs in window.</li>{{/ipsEmpty|bool}}\n {{#ips}}\n <li class=\"d-flex align-items-center gap-2 px-3 py-2 border-top sd-rank-row\" data-action=\"open-ip\" data-ip=\"{{name}}\">\n <span class=\"text-muted small sd-mono\" style=\"width:1.5rem; text-align:right;\">{{rank}}</span>\n <span class=\"flex-grow-1 sd-mono\">{{name}}</span>\n <div class=\"progress sd-progress\" style=\"height:6px; width:96px;\">\n <div class=\"progress-bar bg-danger\" style=\"width:{{percent}}%\"></div>\n </div>\n <span class=\"sd-mono small\">{{value}}</span>\n {{#allowBlock|bool}}\n <button type=\"button\" class=\"btn btn-sm btn-link text-danger p-1\" data-action=\"block-ip\" data-ip=\"{{name}}\" title=\"Block IP\">\n <i class=\"bi bi-shield-fill-x\"></i>\n </button>\n {{/allowBlock|bool}}\n </li>\n {{/ips}}\n </ul>\n </div>\n </div>\n <div class=\"col-lg-6\">\n <div class=\"card sd-card h-100\">\n <div class=\"card-header bg-transparent border-0\">\n <h3 class=\"card-title sd-card-title mb-0\">Top Categories</h3>\n <span class=\"card-subtitle text-muted small\">Last ${WINDOW_DAYS} days</span>\n </div>\n <ul class=\"list-unstyled mb-0 sd-rank-list\">\n {{#catsEmpty|bool}}<li class=\"px-3 py-4 text-muted small\">No category activity in window.</li>{{/catsEmpty|bool}}\n {{#cats}}\n <li class=\"d-flex align-items-center gap-2 px-3 py-2 border-top sd-rank-row\" data-action=\"open-category\" data-cat=\"{{name}}\">\n <span class=\"text-muted small sd-mono\" style=\"width:1.5rem; text-align:right;\">{{rank}}</span>\n <span class=\"flex-grow-1 sd-mono\">{{name}}</span>\n <div class=\"progress sd-progress\" style=\"height:6px; width:96px;\">\n <div class=\"progress-bar\" style=\"width:{{percent}}%; background-color: rgba(179, 136, 255, 0.85);\"></div>\n </div>\n <span class=\"sd-mono small\">{{value}}</span>\n </li>\n {{/cats}}\n </ul>\n </div>\n </div>\n </div>\n `;\n }\n\n _withRank(items) {\n const max = Math.max(1, ...items.map(i => i.value));\n return items.map((item, i) => ({\n ...item,\n rank: i + 1,\n percent: Math.round((item.value / max) * 100)\n }));\n }\n\n async onInit() {\n await this._fetch();\n }\n\n async _fetch() {\n const drStart = Math.floor((Date.now() - WINDOW_DAYS * 86400000) / 1000);\n // Top Categories pre-filters out noise categories server-side\n // via category__not_in — keeps ossec etc. out of the rankings.\n const catFilters = this.excludeCategories.length\n ? { category__not_in: this.excludeCategories.join(',') }\n : {};\n const [ipsRaw, catsRaw] = await Promise.all([\n this._fetchTop('source_ip', drStart),\n this._fetchTop('category', drStart, catFilters)\n ]);\n this.ips = this._withRank(ipsRaw);\n this.cats = this._withRank(catsRaw);\n this.ipsEmpty = this.ips.length === 0;\n this.catsEmpty = this.cats.length === 0;\n }\n\n async refresh() {\n await this._fetch();\n if (this.isMounted()) await this.render();\n }\n\n /**\n * Fetch top-N for a single field via the framework's _mode=top\n * aggregation surface. Returns [{name, value}] sorted by value desc.\n * `extraFilters` lets the caller tack on Django-style lookups\n * (e.g. category__not_in=ossec) that pre-filter records before the\n * GROUP BY runs.\n */\n async _fetchTop(field, drStart, extraFilters = {}) {\n const rest = this.getApp()?.rest;\n if (!rest) return [];\n try {\n const resp = await rest.GET('/api/incident/event', {\n _mode: 'top',\n _field: field,\n _size: TOP_N,\n dr_start: drStart,\n ...extraFilters,\n _: Date.now()\n });\n // _mode=top response: { status, graph, field, agg, size,\n // data: [{key, value, ...}] }\n const rows = resp?.data?.data;\n if (!Array.isArray(rows)) return [];\n return rows\n .filter(r => r.key && r.key !== '—')\n .map(r => ({ name: String(r.key), value: Number(r.value) || 0 }));\n } catch (err) {\n console.warn(`[TopSourcesPanel] _mode=top fetch failed for ${field}:`, err);\n return [];\n }\n }\n\n async onActionOpenIp(event, element) {\n if (event.target.closest('[data-action=\"block-ip\"]')) return;\n const ip = element.dataset.ip;\n if (!ip) return;\n const safeIp = escHtml(ip);\n Modal.drawer({\n eyebrow: 'Source IP',\n title: ip,\n meta: [{ icon: 'bi bi-clock', text: `Last ${WINDOW_DAYS} days` }],\n body: `\n <p class=\"small text-muted\">Open the events table filtered by this source IP.</p>\n <a href=\"?page=system/events&source_ip=${encodeURIComponent(ip)}\" class=\"btn btn-sm btn-outline-primary\">\n <i class=\"bi bi-list-ul me-1\"></i>Open Events from ${safeIp}\n </a>\n `,\n size: 'sm'\n });\n }\n\n async onActionOpenCategory(event, element) {\n const cat = element.dataset.cat;\n if (!cat) return;\n const safeCat = escHtml(cat);\n Modal.drawer({\n eyebrow: 'Category',\n title: cat,\n meta: [{ icon: 'bi bi-clock', text: `Last ${WINDOW_DAYS} days` }],\n body: `\n <p class=\"small text-muted\">Open the events table filtered by this category.</p>\n <a href=\"?page=system/events&category=${encodeURIComponent(cat)}\" class=\"btn btn-sm btn-outline-primary\">\n <i class=\"bi bi-list-ul me-1\"></i>Open Events · ${safeCat}\n </a>\n `,\n size: 'sm'\n });\n }\n\n async onActionBlockIp(event, element) {\n event.stopPropagation();\n const ip = element.dataset.ip;\n if (!ip) return;\n // Modal.confirm renders message as raw HTML — escape backend-sourced ip.\n const ok = await Modal.confirm(`Add ${escHtml(ip)} to the firewall block list?`);\n if (!ok) return;\n const rest = this.getApp()?.rest;\n if (!rest) return;\n try {\n await rest.POST('/api/system/geoip', { ip, is_blocked: true });\n this.getApp()?.toast?.success?.(`Blocked ${ip}`);\n } catch (err) {\n this.getApp()?.toast?.error?.(err?.message || 'Failed to block IP');\n }\n }\n}\n\nexport default TopSourcesPanel;\n","/**\n * AuthFailuresPanel — auth:failures aggregate slug chart + 4 sub-tiles.\n *\n * Uses the backend's auth:failures aggregate slug directly (no\n * client-side composition). Chart is a 30-day MetricsChart in compact\n * header mode. Sub-tiles below the chart show 24-hour counts for four\n * specific event categories.\n */\n\nimport View from '@core/View.js';\nimport MetricsChart from '@ext/charts/MetricsChart.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\n// Punchy labels keep all four sub-tiles on a single line at typical\n// dashboard widths. The full category strings live in `key`.\nconst SUB_TILE_CATEGORIES = [\n { key: 'password_reset', label: 'Pwd Resets' },\n { key: 'totp:login_failed', label: 'TOTP Fails' },\n { key: 'sessions:revoked', label: 'Revoked' },\n { key: 'account:deactivated',label: 'Deactivated' }\n];\n\nclass AuthFailuresPanel extends View {\n constructor(options = {}) {\n super({\n ...options,\n className: `sd-auth-failures ${options.className || ''}`.trim()\n });\n this.tiles = SUB_TILE_CATEGORIES.map(c => ({ ...c, value: null, display: '—' }));\n }\n\n async getTemplate() {\n return `\n <div class=\"card sd-card\">\n <div class=\"card-header bg-transparent border-0\">\n <h3 class=\"card-title sd-card-title mb-0\">Auth Failures</h3>\n <span class=\"card-subtitle text-muted small\">Aggregate slug <code>auth:failures</code> · last 30 days</span>\n </div>\n <div class=\"card-body\">\n <div data-container=\"chart-host\" class=\"mb-3\"></div>\n <div class=\"row g-2\">\n {{#tiles}}\n <div class=\"col-6 col-lg-3\">\n <button type=\"button\"\n class=\"card w-100 text-start border sd-auth-tile\"\n data-action=\"open-sub-tile\"\n data-category=\"{{key}}\">\n <div class=\"card-body py-2 px-3\">\n <div class=\"sd-auth-tile-label\">{{label}} <span class=\"sd-auth-tile-suffix\">24H</span></div>\n <div class=\"sd-mono sd-auth-tile-value\">{{display}}</div>\n </div>\n </button>\n </div>\n {{/tiles}}\n </div>\n </div>\n </div>\n `;\n }\n\n async onInit() {\n this.chart = new MetricsChart({\n containerId: 'chart-host',\n slugs: ['auth:failures'],\n account: 'incident',\n granularity: 'days',\n defaultDateRange: '30d',\n chartType: 'bar',\n compactHeader: true,\n showDateRange: false,\n showGranularity: false,\n showTypeSwitch: false,\n showLegend: false, // single series — legend would just say \"Default\"\n height: 200,\n colors: ['rgba(179, 136, 255, 0.85)'],\n yAxis: { label: 'Failures', beginAtZero: true },\n tooltip: { y: 'number:0' },\n title: ''\n });\n this.addChild(this.chart);\n await this._fetchSubTiles();\n }\n\n async refresh() {\n await Promise.allSettled([\n this.chart?.fetchData?.(),\n this._fetchSubTiles()\n ]);\n if (this.isMounted()) await this.render();\n }\n\n async _fetchSubTiles() {\n const rest = this.getApp()?.rest;\n if (!rest) return;\n const drStart = Math.floor((Date.now() - 86400000) / 1000);\n\n const next = await Promise.all(this.tiles.map(async (tile) => {\n try {\n const resp = await rest.GET('/api/incident/event', {\n category: tile.key,\n dr_start: drStart,\n size: 0,\n _: Date.now()\n });\n const count = resp?.data?.count ?? resp?.data?.data?.count ?? null;\n const value = typeof count === 'number' ? count : 0;\n return { ...tile, value, display: String(value) };\n } catch (_e) {\n return { ...tile, value: null, display: '—' };\n }\n }));\n this.tiles = next;\n }\n\n async onActionOpenSubTile(event, element) {\n const cat = element.dataset.category;\n if (!cat) return;\n // Defensive escape — today `cat` is always a hardcoded constant from\n // SUB_TILE_CATEGORIES, but data attributes get re-decoded on read so\n // belt-and-suspenders here keeps the pattern XSS-safe regardless.\n const safeCat = this._esc(cat);\n Modal.drawer({\n eyebrow: 'Auth Failure Category',\n title: cat,\n meta: [{ icon: 'bi bi-clock', text: 'Last 24 hours' }],\n body: `\n <p class=\"small text-muted\">Open the events table filtered by this category.</p>\n <a href=\"?page=system/events&category=${encodeURIComponent(cat)}\" class=\"btn btn-sm btn-outline-primary\">\n <i class=\"bi bi-list-ul me-1\"></i>Open Events · ${safeCat}\n </a>\n `,\n size: 'sm'\n });\n }\n\n _esc(s) {\n const d = document.createElement('div');\n d.textContent = String(s ?? '');\n return d.innerHTML;\n }\n}\n\nexport default AuthFailuresPanel;\n","/**\n * HealthStrip — collapsed system-health summary.\n *\n * One call to /api/incident/health/summary returns one row per\n * `system:health:*` category. Renders as a `<details>` element that's\n * open by default (per the mockup).\n *\n * Each row: colored dot from level (≥10 red, 6–9 yellow, else green),\n * category name, detail snippet, relative time. If the row carries an\n * `incident_id`, click drills into that incident.\n *\n * Empty `data` is the healthy state — render the strip with a\n * \"all systems healthy\" hint.\n */\n\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { Incident } from '@ext/admin/models/Incident.js';\nimport IncidentView from '../IncidentView.js';\n\nclass HealthStrip extends View {\n constructor(options = {}) {\n super({\n ...options,\n tagName: 'div',\n className: `sd-health ${options.className || ''}`.trim()\n });\n // State on `this` for Mustache resolution.\n this.rows = [];\n this.dots = [{ dot: 'good' }];\n this.summary = 'Loading…';\n this.empty = false;\n this._fetchedOnce = false;\n }\n\n async getTemplate() {\n return `\n <details class=\"card sd-card sd-health-card\" open>\n <summary class=\"card-header bg-transparent border-0 d-flex justify-content-between align-items-center sd-health-summary\">\n <div class=\"d-flex align-items-center gap-3\">\n <span class=\"sd-eyebrow\">System Health</span>\n <span class=\"text-muted small d-inline-flex align-items-center gap-2\">\n {{#dots}}<span class=\"sd-dot sd-dot-{{dot}}\"></span>{{/dots}}\n <span class=\"ms-1\">{{summary}}</span>\n </span>\n </div>\n <i class=\"bi bi-chevron-up sd-health-toggle\"></i>\n </summary>\n <ul class=\"list-unstyled mb-0 sd-health-list\">\n {{#empty|bool}}\n <li class=\"px-3 py-3 text-success small\"><i class=\"bi bi-check-circle me-1\"></i>All systems healthy.</li>\n {{/empty|bool}}\n {{#rows}}\n <li class=\"px-3 py-2 border-top d-flex align-items-center gap-3 sd-health-row {{#hasIncident|bool}}sd-health-row-link{{/hasIncident|bool}}\"\n {{#hasIncident|bool}}data-action=\"open-incident\" data-id=\"{{incidentId}}\"{{/hasIncident|bool}}>\n <span class=\"sd-dot sd-dot-{{dot}}\"></span>\n <div class=\"flex-grow-1 min-w-0\">\n <div class=\"sd-mono small\">{{category}}</div>\n <div class=\"text-muted small text-truncate\">{{details}}</div>\n </div>\n <span class=\"text-muted small\">{{when}}</span>\n <span class=\"badge text-bg-light sd-mono\">level {{level}}</span>\n </li>\n {{/rows}}\n </ul>\n </details>\n `;\n }\n\n async onInit() {\n await this._fetch();\n }\n\n async refresh() {\n await this._fetch();\n if (this.isMounted()) await this.render();\n }\n\n async _fetch() {\n const rest = this.getApp()?.rest;\n if (!rest) {\n this.rows = [];\n this._fetchedOnce = true;\n this._reflectState();\n return;\n }\n try {\n const resp = await rest.GET('/api/incident/health/summary', { _: Date.now() });\n const rows = resp?.data?.data || [];\n this.rows = rows.map(r => this._normalize(r));\n } catch (err) {\n console.warn('[HealthStrip] fetch failed:', err);\n this.rows = [];\n } finally {\n this._fetchedOnce = true;\n this._reflectState();\n }\n }\n\n _reflectState() {\n this.empty = this.rows.length === 0 && this._fetchedOnce;\n this.dots = this.rows.length ? this.rows.map(r => ({ dot: r.dot })) : [{ dot: 'good' }];\n this.summary = this._buildSummary();\n }\n\n _normalize(row) {\n const level = parseInt(row.level, 10) || 0;\n const dot = level >= 10 ? 'crit' : (level >= 6 ? 'warn' : 'good');\n return {\n category: row.category || '—',\n details: row.details || row.title || '',\n level,\n dot,\n when: this._relativeTime(row.last_seen),\n incidentId: row.incident_id || null,\n hasIncident: !!row.incident_id\n };\n }\n\n _buildSummary() {\n if (!this.rows.length) {\n return this._fetchedOnce ? 'All systems healthy' : 'Loading…';\n }\n const crit = this.rows.filter(r => r.dot === 'crit').length;\n const warn = this.rows.filter(r => r.dot === 'warn').length;\n const good = this.rows.filter(r => r.dot === 'good').length;\n const parts = [];\n if (crit) parts.push(`${crit} critical`);\n if (warn) parts.push(`${warn} warning${warn > 1 ? 's' : ''}`);\n if (good) parts.push(`${good} healthy`);\n return parts.join(' · ');\n }\n\n _relativeTime(timestamp) {\n if (!timestamp) return '—';\n const t = typeof timestamp === 'number' ? timestamp * 1000 : new Date(timestamp).getTime();\n if (!t) return '—';\n const diff = Math.floor((Date.now() - t) / 1000);\n if (diff < 60) return `${diff}s ago`;\n if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;\n if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;\n return `${Math.floor(diff / 86400)}d ago`;\n }\n\n async onActionOpenIncident(event, element) {\n const id = element.dataset.id;\n if (!id) return;\n const model = new Incident({ id });\n await model.fetch();\n if (!model.id) return;\n const view = new IncidentView({ model });\n await Modal.show(view, { size: 'xl', header: false });\n }\n}\n\nexport default HealthStrip;\n","/**\n * SecurityDashboardPage — single-page mission-control dashboard.\n *\n * Replaces the older tabbed IncidentDashboardPage with a scrolling\n * layout designed around one question: \"what should I be doing right now?\"\n *\n * Layout (top-to-bottom):\n * 1. Pulse — 8 KPI tiles via one batched series-with-deltas fetch\n * 2. Needs Attention + Threat Composition (2-col hero)\n * 3. Geography (map + leaderboard + slug-family selector) — lazy\n * 4. Distributions (status donut + priority buckets + bouncer funnel) — lazy\n * 5. Top Sources (top IPs + top categories) — lazy\n * 6. Auth Failures (auth:failures slug + 4 sub-tiles) — lazy\n * 7. System Health (collapsed details + per-category indicator) — lazy\n *\n * Sections 3–7 use addChild({ lazyMount: true }) so they don't fetch\n * until scrolled into view — keeps first-paint fast.\n *\n * Refresh tiers:\n * - 'fast' (60s) → pulse + priority queue\n * - 'slow' (5min) → everything else (panels manage their own refresh\n * when their data is on screen)\n *\n * Permission gate: 'view_security'. Sub-panels that need 'manage_security'\n * conditionally render their controls.\n */\n\nimport Page from '@core/Page.js';\nimport StatusStripPanel from './StatusStripPanel.js';\nimport PriorityQueueView from './PriorityQueueView.js';\nimport TicketsQueueView from './TicketsQueueView.js';\nimport ThreatCompositionChart from './ThreatCompositionChart.js';\nimport GeographyPanel from './GeographyPanel.js';\nimport DistributionStrip from './DistributionStrip.js';\nimport TopSourcesPanel from './TopSourcesPanel.js';\nimport AuthFailuresPanel from './AuthFailuresPanel.js';\nimport HealthStrip from './HealthStrip.js';\n\nclass SecurityDashboardPage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n title: 'Security Dashboard',\n className: 'security-dashboard-page'\n });\n }\n\n async getTemplate() {\n return `\n <div class=\"security-dashboard\">\n <header class=\"sd-page-head\">\n <div>\n <span class=\"sd-eyebrow\">Security</span>\n <h1 class=\"sd-page-title\">Security Dashboard</h1>\n </div>\n <div class=\"sd-page-controls\">\n <span class=\"sd-updated text-muted small me-2\">\n <i class=\"bi bi-circle-fill text-success me-1\" style=\"font-size:0.5rem;\"></i>\n Live\n </span>\n <button type=\"button\"\n class=\"btn btn-outline-secondary btn-sm\"\n data-action=\"refresh-all\"\n title=\"Refresh all panels\">\n <i class=\"bi bi-arrow-clockwise\"></i> Refresh\n </button>\n </div>\n </header>\n\n <section class=\"sd-section\">\n <div data-container=\"status-strip\"></div>\n </section>\n\n <section class=\"sd-section sd-grid sd-grid-2\">\n <div data-container=\"priority-queue\"></div>\n <div data-container=\"tickets-queue\"></div>\n </section>\n\n <section class=\"sd-section sd-grid sd-grid-2-3\">\n <div data-container=\"geography\"></div>\n <div data-container=\"composition\"></div>\n </section>\n\n <section class=\"sd-section\">\n <div data-container=\"distributions\"></div>\n </section>\n\n <section class=\"sd-section\">\n <div data-container=\"top-sources\"></div>\n </section>\n\n <section class=\"sd-section\">\n <div data-container=\"auth-failures\"></div>\n </section>\n\n <section class=\"sd-section\">\n <div data-container=\"health-strip\"></div>\n </section>\n </div>\n `;\n }\n\n async onInit() {\n const app = this.getApp();\n const canManageSecurity = !!app?.activeUser?.hasPermission?.('manage_security');\n\n // ── Section 1 — Pulse (always above the fold) ────────────────\n this.statusStrip = new StatusStripPanel({ containerId: 'status-strip' });\n this.addChild(this.statusStrip);\n\n // ── Section 2 — Needs Attention (Incidents | Tickets) ───────\n // Two parallel queues: incidents (system noticed) on the left,\n // tickets (human action items) on the right.\n this.priorityQueue = new PriorityQueueView({\n containerId: 'priority-queue',\n allowActions: canManageSecurity\n });\n this.addChild(this.priorityQueue);\n\n this.ticketsQueue = new TicketsQueueView({\n containerId: 'tickets-queue',\n allowActions: canManageSecurity\n });\n this.addChild(this.ticketsQueue);\n\n // ── Section 3 — Geography (compact) | Threat Composition ────\n // Geography defaults to compact mode — leaderboard only, with a\n // \"show map\" toolbar button that opens the world map in an XL\n // modal. Keeps the dashboard cleaner.\n this.composition = new ThreatCompositionChart({ containerId: 'composition' });\n this.addChild(this.composition);\n\n this.geography = new GeographyPanel({ containerId: 'geography' });\n this.addChild(this.geography);\n\n this.distributions = new DistributionStrip({ containerId: 'distributions' });\n this.addChild(this.distributions);\n\n this.topSources = new TopSourcesPanel({\n containerId: 'top-sources',\n allowBlock: canManageSecurity\n });\n this.addChild(this.topSources);\n\n this.authFailures = new AuthFailuresPanel({ containerId: 'auth-failures' });\n this.addChild(this.authFailures);\n\n this.healthStrip = new HealthStrip({ containerId: 'health-strip' });\n this.addChild(this.healthStrip);\n }\n\n async onEnter() {\n await super.onEnter();\n // Tiered refresh — only the always-visible top section ticks fast.\n this.scheduleRefresh(() => this.statusStrip?.refresh(), 60_000, { tier: 'fast' });\n this.scheduleRefresh(() => this.priorityQueue?.refresh(), 60_000, { tier: 'fast' });\n this.scheduleRefresh(() => this.ticketsQueue?.refresh(), 60_000, { tier: 'fast' });\n this.scheduleRefresh(() => this.composition?.refresh(), 300_000, { tier: 'slow' });\n this.scheduleRefresh(() => this._refreshLazyMounted(), 300_000, { tier: 'slow' });\n }\n\n async onActionRefreshAll(event, element) {\n const button = element || event?.currentTarget || null;\n const icon = button?.querySelector?.('i');\n icon?.classList.add('bi-spin');\n if (button) button.disabled = true;\n try {\n await this.runScheduledRefreshes();\n } finally {\n icon?.classList.remove('bi-spin');\n if (button) button.disabled = false;\n }\n }\n\n /**\n * Fire refresh on lazy panels that have actually mounted.\n * Avoids triggering a fetch-on-scroll-in-via-refresh.\n */\n async _refreshLazyMounted() {\n const lazy = [this.geography, this.distributions, this.topSources, this.authFailures, this.healthStrip];\n await Promise.allSettled(\n lazy.filter(v => v?._lazyTriggered && typeof v.refresh === 'function').map(v => v.refresh())\n );\n }\n}\n\nexport default SecurityDashboardPage;\n","/**\n * IncidentTablePage - Incident management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { IncidentList, IncidentForms } from '@ext/admin/models/Incident.js';\nimport IncidentView from './IncidentView.js';\n\nclass IncidentTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_incidents',\n pageName: 'Manage Incidents',\n router: \"admin/incidents\",\n Collection: IncidentList,\n\n formCreate: IncidentForms.create,\n formEdit: IncidentForms.edit,\n itemViewClass: IncidentView,\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: '-id',\n status: \"new\",\n },\n\n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n width: '60px',\n sortable: true,\n class: 'text-muted'\n },\n {\n key: 'status', label: \"Status\",\n filter: {\n type: 'multiselect',\n options: [\"new\", \"open\", \"paused\", \"resolved\", \"qa\", \"ignored\"],\n }\n },\n {\n key: 'created',\n label: 'Created',\n formatter: \"epoch|datetime\",\n filter: {\n type: 'daterange',\n }\n },\n {\n key: 'scope',\n label: 'Scope',\n sortable: true,\n filter: {type:\"text\"}\n },\n {\n key: 'category',\n label: 'Category',\n sortable: true,\n filter: {type:\"text\"}\n },\n {\n key: 'priority',\n label: 'Priority',\n filter: {type:\"text\"}\n },\n {\n key: 'title',\n label: 'title',\n formatter: \"truncate(100)|default('No description')\"\n }\n ],\n\n filters: [\n {\n key: 'category__not',\n label: 'Not Category',\n filter: {type:\"text\"}\n },\n {\n key: 'priority__gt',\n label: 'Priority Greater Than',\n filter: {type:\"number\"}\n },\n {\n key: 'priority__lt',\n label: 'Priority Less Than',\n filter: {type:\"number\"}\n },\n {\n key: 'metadata__rule_id',\n label: 'Rule ID',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__key',\n label: 'Metadata Key',\n filter: {type:\"text\"}\n },\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No incidents found. Click \"Add Incident\" to create your first incident.',\n\n // Batch actions\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Open\", icon: \"bi bi-folder2-open\", action: \"open\" },\n { label: \"Resolve\", icon: \"bi bi-check-circle\", action: \"resolve\" },\n { label: \"Pause\", icon: \"bi bi-pause-circle\", action: \"pause\" },\n { label: \"Ignore\", icon: \"bi bi-x-circle\", action: \"ignore\" },\n { label: \"Merge\", icon: \"bi bi-merge\", action: \"merge\" },\n { label: \"Protect\", icon: \"bi bi-shield-fill-check\", action: \"protect\" }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionBatchResolve(event, element) {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const app = this.getApp();\n const result = await app.confirm(`Are you sure you want to close ${selected.length} incidents?`);\n if (!result) return;\n await Promise.all(selected.map(item => item.model.save({status: 'resolved'})));\n this.tableView.collection.fetch();\n }\n\n async onActionBatchOpen(event, element) {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const app = this.getApp();\n const result = await app.confirm(`Are you sure you want to open ${selected.length} incidents?`);\n if (!result) return;\n await Promise.all(selected.map(item => item.model.save({status: 'open'})));\n this.tableView.collection.fetch();\n }\n\n async onActionBatchPause(event, element) {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const app = this.getApp();\n const result = await app.confirm(`Are you sure you want to pause ${selected.length} incidents?`);\n if (!result) return;\n await Promise.all(selected.map(item => item.model.save({status: 'paused'})));\n this.tableView.collection.fetch();\n }\n\n async onActionBatchIgnore(event, element) {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const app = this.getApp();\n const result = await app.confirm(`Are you sure you want to ignore ${selected.length} incidents?`);\n if (!result) return;\n await Promise.all(selected.map(item => item.model.save({status: 'ignored'})));\n this.tableView.collection.fetch();\n }\n\n async onActionBatchMerge(_event, _element) {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const app = this.getApp();\n const result = await app.showForm({\n title: `Merge ${selected.length} incidents`,\n fields: [\n {\n name: 'merge',\n type: 'select',\n label: 'Select Parent Incident',\n options: selected.map(item => ({value: item.model.id, label: item.model.id})),\n required: true\n }\n ]\n });\n if (!result) return;\n\n // Find the parent model from selected items\n const parentModel = selected.find(item => item.model.id == result.merge)?.model;\n if (!parentModel) return;\n\n // Get list of all IDs to merge (excluding the parent)\n const mergeIds = selected\n .map(item => item.model.id)\n .filter(id => id != result.merge);\n\n // Save the merge operation to the parent model\n await parentModel.save({ merge: mergeIds });\n this.tableView.collection.fetch();\n }\n\n async onActionBatchProtect(event, element) {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const app = this.getApp();\n const result = await app.confirm(`Protect ${selected.length} incident(s) from deletion?`);\n if (!result) return;\n await Promise.all(selected.map(item => item.model.save({ metadata: { do_not_delete: true } })));\n app.toast?.success(`${selected.length} incident(s) protected`);\n this.tableView.collection.fetch();\n }\n}\n\nexport default IncidentTablePage;\n","/**\n * EventView - Detailed view for an IncidentEvent record\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport StackTraceView from '@core/views/data/StackTraceView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { Incident, IncidentEvent } from '@ext/admin/models/Incident.js';\nimport { User, UserDevice, UserDeviceLocation } from '@core/models/User.js';\nimport { GeoLocatedIP } from '@core/models/System.js';\nimport { Member } from '@core/models/Member.js';\nimport { Ticket } from '@ext/admin/models/Tickets.js';\nimport { Job } from '@ext/admin/models/Job.js';\nimport { Log } from '@core/models/Log.js';\nimport { ApiKey } from '@core/models/ApiKey.js';\nimport IncidentView from './IncidentView.js';\n\n/**\n * Map of model_name strings (as returned by the API) to Model classes.\n * Used to resolve the related model for \"View Related Model\" actions.\n */\nconst MODEL_REGISTRY = {\n user: User,\n userdevice: UserDevice,\n userdevicelocation: UserDeviceLocation,\n geolocatedip: GeoLocatedIP,\n member: Member,\n incident: Incident,\n incidentevent: IncidentEvent,\n ticket: Ticket,\n job: Job,\n log: Log,\n apikey: ApiKey,\n};\n\nclass EventView extends View {\n constructor(options = {}) {\n super({\n className: 'event-view',\n ...options\n });\n\n this.model = options.model || new IncidentEvent(options.data || {});\n this.eventIcon = this.getIconForEvent(this.model.get('level'));\n\n this.template = `\n <div class=\"event-view-container\">\n <!-- Header -->\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 <!-- Body -->\n <div data-container=\"event-tabs\"></div>\n </div>\n `;\n }\n\n getIconForEvent(level) {\n if (level >= 40) return { icon: 'bi-exclamation-octagon-fill', color: 'text-danger' }; // Error\n if (level >= 30) return { icon: 'bi-exclamation-triangle-fill', color: 'text-warning' }; // Warning\n if (level >= 20) return { icon: 'bi-info-circle-fill', color: 'text-info' }; // Info\n return { icon: 'bi-bell-fill', color: 'text-secondary' }; // Debug/Default\n }\n\n async onInit() {\n // Overview Tab\n this.overviewView = new DataView({\n model: this.model,\n className: \"p-3\",\n columns: 2,\n fields: [\n { name: 'id', label: 'Event ID' },\n { name: 'level', label: 'Level' },\n { name: 'hostname', label: 'Hostname' },\n { name: 'incident', label: 'Incident ID' },\n { name: 'model_name', label: 'Related Model' },\n { name: 'model_id', label: 'Related Model ID' },\n { name: 'details', label: 'Details', columns: 12 },\n ]\n });\n\n const tabs = { 'Overview': this.overviewView };\n \n const metadata = this.model.get('metadata') || {};\n \n // Add Stack Trace tab if present\n if (metadata.stack_trace) {\n this.stackTraceView = new StackTraceView({\n stackTrace: metadata.stack_trace\n });\n tabs['Stack Trace'] = this.stackTraceView;\n }\n \n // Add Metadata tab if there's metadata\n if (Object.keys(metadata).length > 0) {\n this.metadataView = new View({\n model: this.model,\n template: `<pre class=\"bg-light p-3 border rounded\"><code>{{{model.metadata|json}}}</code></pre>`\n });\n tabs['Metadata'] = this.metadataView;\n }\n\n this.tabView = new TabView({\n containerId: 'event-tabs',\n tabs: tabs,\n activeTab: 'Overview'\n });\n this.addChild(this.tabView);\n\n // ContextMenu\n const menuItems = [\n { label: 'View Incident', action: 'view-incident', icon: 'bi-shield-exclamation', disabled: !this.model.get('incident') },\n { label: 'View Related Model', action: 'view-model', icon: 'bi-box-arrow-up-right', disabled: !this.model.get('model_id') },\n { type: 'divider' },\n { label: 'Delete Event', action: 'delete-event', icon: 'bi-trash', danger: true }\n ];\n\n const eventMenu = new ContextMenu({\n containerId: 'event-context-menu',\n className: \"context-menu-view header-menu-absolute\",\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: menuItems\n }\n });\n this.addChild(eventMenu);\n }\n\n async onActionViewIncident() {\n const incidentId = this.model.get('incident') || this.model.get('incident_id');\n if (!incidentId) {\n this.getApp()?.toast?.warning('No incident linked to this event');\n return true;\n }\n const incident = new Incident({ id: incidentId });\n const view = new IncidentView({ model: incident });\n await Modal.dialog({\n title: 'Incident Details',\n body: view,\n size: 'xl',\n scrollable: true,\n header: false,\n buttons: [{ text: 'Close', class: 'btn-secondary', dismiss: true }]\n });\n }\n\n async onActionViewModel() {\n const modelName = this.model.get('model_name') || this.model.get('model_class');\n const objectId = this.model.get('model_id') || this.model.get('object_id');\n if (!modelName || !objectId) {\n this.getApp()?.toast?.warning('No related model linked to this event');\n return true;\n }\n\n const key = modelName.toLowerCase().replace(/[^a-z]/g, '');\n const ModelClass = MODEL_REGISTRY[key];\n\n if (!ModelClass) {\n this.getApp()?.toast?.warning(`Unknown model type: ${modelName}`);\n return true;\n }\n\n if (!ModelClass.VIEW_CLASS) {\n this.getApp()?.toast?.warning(`No detail view available for ${modelName}`);\n return true;\n }\n\n await Modal.showModelById(ModelClass, objectId);\n }\n\n async onActionDeleteEvent() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete this event? This action cannot be undone.`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n if (confirmed) {\n const resp = await this.model.destroy();\n if (resp.success) {\n this.emit('event:deleted', { model: this.model });\n }\n }\n }\n}\n\nIncidentEvent.VIEW_CLASS = EventView;\n\nexport default EventView;\n","/**\n * EventTablePage - System events management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { IncidentEventList, IncidentEventForms } from '@ext/admin/models/Incident.js';\nimport EventView from './EventView.js';\n\nclass EventTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_events',\n pageName: 'System Events',\n router: \"admin/events\",\n Collection: IncidentEventList,\n\n formEdit: IncidentEventForms.edit,\n itemViewClass: EventView,\n\n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n\n defaultQuery: {\n sort: '-id',\n category__not: \"ossec\",\n },\n\n // Column definitions\n columns: [\n {\n key: 'created', label: 'Timestamp',\n sortable: true, formatter: 'datetime',\n filter: {\n type: 'daterange',\n }\n },\n {\n key: 'level', label: 'Level',\n sortable: true, formatter: 'badge',\n filter: {\n type: \"select\",\n options: [\n { value: '5', label: 'Critical' },\n { value: '4', label: 'Warning' },\n { value: '3', label: 'Info' },\n { value: '2', label: 'Debug' },\n { value: '1', label: 'Trace' }\n ]\n }\n },\n {\n key: 'scope',\n label: 'Scope',\n sortable: true, formatter: 'badge',\n filter: {\n type: \"combobox\",\n options: [\n { value: 'account', label: 'Account' },\n { value: 'incident', label: 'Incident' },\n { value: 'ossec', label: 'OSSEC' },\n { value: 'fileman', label: 'File Manager' },\n { value: 'metrics', label: 'Metrics' },\n { value: 'jobs', label: 'Jobs' },\n { value: 'aws', label: 'AWS' }\n\n ]\n }\n },\n {\n key: 'category',\n label: 'Category',\n sortable: true, formatter: 'badge',\n filter: {\n type: \"combobox\",\n options: [\n { value: 'rest_error', label: 'Rest Error' },\n { value: 'api_error', label: 'API Error' },\n { value: 'auth', label: 'Auth' },\n { value: 'database', label: 'Database' }\n ]\n }\n },\n { key: 'title', label: 'Title', sortable: true, formatter: 'truncate(50)' },\n {\n key: 'source_ip', label: 'Source IP', sortable: true,\n filter: {\n type: \"text\"\n }\n },\n {\n key: 'metadata.server', label: 'Server',\n sortable: true,\n filter: {\n type: \"text\"\n }\n },\n ],\n\n filters: [\n {\n key: 'category__not',\n label: 'Not Category',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__http_url__icontains',\n label: 'URL Contains',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__http_path__icontains',\n label: 'Path Contains',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__http_query_string__icontains',\n label: 'Query String Contains',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__rule_id',\n label: 'Rule ID',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__country_code',\n label: 'Country',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__region',\n label: 'Region',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__city__icontains',\n label: 'City',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__http_status',\n label: 'HTTP Status',\n filter: {type:\"text\"}\n },\n {\n key: 'model_name',\n label: 'Model Name',\n filter: {type:\"text\"}\n },\n {\n key: 'model_id',\n label: 'Model ID',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__user_email',\n label: 'User Email',\n filter: {type:\"text\"}\n },\n {\n key: 'metadata__http_user_agent__icontains',\n label: 'User Agent Contains',\n filter: {type:\"text\"}\n },\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No events found.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default EventTablePage;\n","/**\n * TicketTablePage - Ticket management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { TicketList, TicketForms, TicketCategories } from '@ext/admin/models/Tickets.js';\nimport TicketView from './TicketView.js';\n\nclass TicketTablePage extends TablePage {\n constructor(options = {}) {\n super({\n name: 'admin_tickets',\n pageName: 'Tickets',\n router: \"admin/tickets\",\n Collection: TicketList,\n\n formCreate: TicketForms.create,\n formEdit: TicketForms.edit,\n itemViewClass: TicketView,\n\n viewDialogOptions: {\n header: false\n },\n\n defaultQuery: {\n sort: '-priority',\n status__in: \"new,open\"\n },\n\n // Column definitions\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'title', label: 'Title', sortable: true},\n {\n key: 'status', label: 'Status', sortable: true,\n editable: true,\n editableOptions: {\n type: \"select\",\n options: [\n \"new\", \"open\", \"paused\", \"resolved\", \"qa\", \"ignored\"\n ]\n },\n filter: {\n type: \"multiselect\",\n placeHolder: \"Select Status\",\n options: [\n \"new\", \"open\", \"paused\", \"resolved\", \"qa\", \"ignored\"\n ]\n }\n },\n { key: 'priority', label: 'Priority', sortable: true },\n {\n key: 'category', label: 'Category', sortable: true,\n editable: true,\n editableOptions: {\n type: \"select\",\n options: [\n ... Object.keys(TicketCategories)\n ]\n },\n filter: {\n type: \"multiselect\",\n placeHolder: \"Select Category\",\n options: [\n ... Object.keys(TicketCategories)\n ]\n }\n },\n { key: 'assignee.display_name', label: 'Assignee', sortable: true, formatter: \"default('Unassigned')\" },\n { key: 'incident.id', label: 'Incident ID', sortable: true },\n { key: 'created', label: 'Created', sortable: true, formatter: 'datetime' }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No tickets found.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n },\n ...options,\n });\n }\n}\n\nexport default TicketTablePage;\n","import TablePage from '@core/pages/TablePage.js';\nimport { RuleSetList, RuleSetForms, BundleByOptions } from '@ext/admin/models/Incident.js';\nimport RuleSetView from './RuleSetView.js';\n\nclass RuleSetTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_rulesets',\n pageName: 'Rule Engine',\n router: \"admin/rulesets\",\n Collection: RuleSetList,\n itemViewClass: RuleSetView,\n formCreate: RuleSetForms.create,\n formEdit: RuleSetForms.edit,\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: 'priority',\n },\n\n columns: [\n { key: 'id', label: 'ID', width: '60px', sortable: true, class: 'text-muted' },\n {\n key: 'is_active', label: 'Active', width: '70px', sortable: true,\n formatter: 'yesnoicon',\n filter: {\n type: 'select',\n options: [\n { value: 'true', label: 'Active' },\n { value: 'false', label: 'Inactive' }\n ]\n }\n },\n {\n key: 'metadata.delete_on_resolution', label: 'Auto-Delete', width: '90px',\n formatter: 'yesnoicon'\n },\n { key: 'name', label: 'Name', sortable: true },\n {\n key: 'category', label: 'Category', sortable: true, formatter: 'badge',\n filter: { type: 'text', placeholder: 'e.g., auth:failed' }\n },\n { key: 'priority', label: 'Priority', sortable: true, width: '80px' },\n {\n key: 'bundle_by', label: 'Bundle By', width: '140px',\n formatter: (value) => {\n const opt = BundleByOptions.find(o => o.value === value);\n return opt ? opt.label : String(value);\n }\n },\n {\n key: 'trigger_count', label: 'Trigger', width: '80px',\n formatter: (value) => value != null ? String(value) : '<span class=\"text-muted\">—</span>'\n },\n {\n key: 'handler', label: 'Handler',\n formatter: \"truncate(40)|default('—')\"\n }\n ],\n\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n emptyMessage: 'No rule sets found. Create one to start matching events automatically.',\n\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Enable\", icon: \"bi bi-toggle-on\", action: \"enable\" },\n { label: \"Disable\", icon: \"bi bi-toggle-off\", action: \"disable\" },\n { label: \"Delete\", icon: \"bi bi-trash\", action: \"delete\", danger: true }\n ],\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionBatchEnable() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Enable ${selected.length} ruleset(s)?`);\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.save({ is_active: true })));\n this.getApp().toast.success(`${selected.length} ruleset(s) enabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDisable() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Disable ${selected.length} ruleset(s)?`);\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.save({ is_active: false })));\n this.getApp().toast.success(`${selected.length} ruleset(s) disabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDelete() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Delete ${selected.length} ruleset(s)? This cannot be undone.`);\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.destroy()));\n this.getApp().toast.success(`${selected.length} ruleset(s) deleted`);\n this.tableView.collection.fetch();\n }\n}\n\nexport default RuleSetTablePage;\n","/**\n * EmailDomainTablePage - Admin page for managing SES/SNS Email Domains\n * Clean implementation following simplified TablePage pattern\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport View from '@core/View.js';\nimport { EmailDomain, EmailDomainList, EmailDomainForms } from '@ext/admin/models/Email.js';\n\nclass EmailDomainTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_email_domains',\n pageName: 'Email Domains',\n router: 'admin/email/domains',\n Collection: EmailDomainList,\n formCreate: EmailDomainForms.create,\n formEdit: EmailDomainForms.edit,\n\n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n width: '70px',\n sortable: true,\n class: 'text-muted'\n },\n {\n key: 'name',\n label: 'Domain',\n sortable: true\n },\n {\n key: 'region',\n label: 'Region',\n sortable: true,\n formatter: \"default('—')\"\n },\n {\n key: 'receiving_enabled',\n label: 'Receiving',\n formatter: \"boolean|badge\"\n },\n {\n key: 'can_send',\n label: 'Send Verified',\n formatter: \"boolean|badge\"\n },\n {\n key: 'can_recv',\n label: 'Recv Verified',\n formatter: \"boolean|badge\"\n },\n {\n key: 'created',\n label: 'Created',\n formatter: \"epoch|datetime\"\n }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No domains found. Click \"Add Domain\" to get started.',\n\n // Context menu configuration\n contextMenu: [\n {\n icon: 'bi-shield-check',\n action: 'edit-aws-creds',\n label: 'Edit AWS Credentials'\n },\n {\n icon: 'bi-rocket-takeoff',\n action: 'onboard',\n label: 'Onboard'\n },\n {\n icon: 'bi-shield-check',\n action: 'audit',\n label: 'Audit'\n },\n {\n icon: 'bi-arrow-repeat',\n action: 'reconcile',\n label: 'Reconcile'\n }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionEditAwsCreds(event, element) {\n const item = this.collection.get(element.dataset.id);\n const result = await Modal.modelForm({\n model: item,\n formConfig: EmailDomainForms.credentials,\n }\n );\n return true;\n }\n\n async onActionOnboard(event, element) {\n const item = this.collection.get(element.dataset.id);\n const model = new EmailDomain({ id: item.id });\n\n const formData = await Modal.form(EmailDomainForms.onboard);\n if (!formData) return;\n\n try {\n const resp = await model.onboard(formData);\n if (!resp.success) {\n throw new Error(resp.message || 'Network error during onboarding');\n }\n if (!resp.data?.status) {\n throw new Error(resp.data?.error || 'Onboarding failed');\n }\n\n this.getApp()?.toast?.success('Domain onboarding completed successfully');\n await this.refresh();\n } catch (err) {\n console.error('Onboard error:', err);\n this.showError(err.message || 'Failed to onboard domain');\n }\n }\n\n async onActionAudit(event, element) {\n const item = this.collection.get(element.dataset.id);\n const model = new EmailDomain({ id: item.id });\n\n try {\n const resp = await model.audit();\n if (!resp.success) {\n throw new Error(resp.message || 'Network error during audit');\n }\n if (!resp.data?.status) {\n throw new Error(resp.data?.error || 'Audit failed');\n }\n\n const result = resp.data?.data || {};\n await Modal.dialog({\n title: `Audit Report - ${item.name}`,\n body: new View({\n 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 `,\n data: { result }\n }),\n size: 'lg'\n });\n } catch (err) {\n console.error('Audit error:', err);\n this.showError(err.message || 'Failed to audit domain');\n }\n }\n\n async onActionReconcile(event, element) {\n const item = this.collection.get(element.dataset.id);\n const model = new EmailDomain({ id: item.id });\n\n try {\n const resp = await model.reconcile();\n if (!resp.success) {\n throw new Error(resp.message || 'Network error during reconcile');\n }\n if (!resp.data?.status) {\n throw new Error(resp.data?.error || 'Reconcile failed');\n }\n\n this.getApp()?.toast?.success('Reconcile completed successfully');\n await this.refresh();\n } catch (err) {\n console.error('Reconcile error:', err);\n this.showError(err.message || 'Failed to reconcile domain');\n }\n }\n}\n\nexport default EmailDomainTablePage;\n","/**\n * EmailMailboxTablePage - Admin page for managing Mailboxes\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { MailboxList, MailboxForms, Mailbox } from '@ext/admin/models/Email.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass EmailMailboxTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_email_mailboxes',\n pageName: 'Mailboxes',\n router: 'admin/email/mailboxes',\n Collection: MailboxList,\n\n formCreate: MailboxForms.create,\n formEdit: MailboxForms.edit,\n clickAction: \"edit\",\n\n // Table columns\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'email', label: 'Email', sortable: true },\n { key: 'domain.name', label: 'Domain', sortable: true, formatter: \"default('—')\" },\n { key: 'allow_inbound', label: 'Inbound', formatter: \"boolean|badge\" },\n { key: 'allow_outbound', label: 'Outbound', formatter: \"boolean|badge\" },\n { key: 'is_system_default', label: 'System Default', formatter: \"boolean|badge\" },\n { key: 'is_domain_default', label: 'Domain Default', formatter: \"boolean|badge\" }\n ],\n\n // Features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No mailboxes found. Click \"Add Mailbox\" to create one.',\n\n // Context menu configuration\n contextMenu: [\n { icon: 'bi-envelope', action: 'send-email', label: 'Send Email' },\n { icon: 'bi-envelope', action: 'send-template-email', label: 'Send Template Email' }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionSendEmail(event, element) {\n const item = this.collection.get(element.dataset.id);\n // Implement send email action\n const data = await Modal.form({\n title: 'Send Email',\n fields: [\n { name: 'to', label: 'To', type: 'email', required: true },\n { name: 'subject', label: 'Subject', type: 'text', required: true },\n { name: 'body_html', label: 'Body', type: 'textarea', required: true }\n ]\n });\n data.from_email = item.get('email');\n const result = await Mailbox.sendEmail(data);\n if (result.success) {\n this.getApp().toast.success('Email sent successfully');\n } else {\n let msg = \"Failed to send email\";\n if (result.data.details) {\n msg = result.data.details;\n } else if (result.data.error) {\n msg = result.data.error;\n }\n console.log(result);\n this.getApp().toast.error(msg);\n }\n }\n\n async onActionSendTemplateEmail(event, element) {\n const item = this.collection.get(element.dataset.id);\n // Implement send email action\n const data = await Modal.form({\n title: 'Send Email',\n fields: [\n { name: 'to', label: 'To', type: 'email', required: true },\n { name: 'subject', label: 'Subject', type: 'text', required: true },\n { name: 'template_name', label: 'Template', type: 'text', required: true },\n { name: 'template_context', label: 'Context', type: 'textarea', required: true }\n ]\n });\n data.from_email = item.get('email');\n const result = await Mailbox.sendEmail(data);\n if (result.success) {\n this.getApp().toast.success('Email sent successfully');\n } else {\n let msg = \"Failed to send email\";\n if (result.data.details) {\n msg = result.data.details;\n } else if (result.data.error) {\n msg = result.data.error;\n }\n console.log(result);\n this.getApp().toast.error(msg);\n }\n }\n}\n\nexport default EmailMailboxTablePage;\n","/**\n * EmailTemplateView - A clean view for previewing email templates\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport { EmailTemplate } from '@ext/admin/models/Email.js';\n\n/**\n * EmailHtmlPreviewView - Renders HTML email template in a sandboxed iframe\n */\nclass EmailHtmlPreviewView extends View {\n constructor(options = {}) {\n super({\n className: 'email-html-preview',\n template: `\n <div class=\"email-html-preview-container\">\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\n <small class=\"text-muted\">HTML Preview (sandboxed)</small>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" data-action=\"refresh-preview\">\n <i class=\"bi bi-arrow-clockwise\"></i> Refresh\n </button>\n </div>\n <iframe\n id=\"email-preview-frame\"\n class=\"border rounded w-100\"\n style=\"height: 500px; background: white;\"\n sandbox=\"allow-same-origin\"\n frameborder=\"0\"\n ></iframe>\n </div>\n `,\n ...options\n });\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n this.renderHtmlInIframe();\n }\n\n renderHtmlInIframe() {\n const iframe = this.element.querySelector('#email-preview-frame');\n if (!iframe) return;\n\n const htmlContent = this.model.get('html_template') || '';\n\n // Get iframe document\n const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;\n\n // Write HTML content\n iframeDoc.open();\n iframeDoc.write(htmlContent);\n iframeDoc.close();\n }\n\n async onActionRefreshPreview(event, element) {\n this.renderHtmlInIframe();\n }\n}\n\nclass EmailTemplateView extends View {\n constructor(options = {}) {\n super({\n className: 'email-template-view',\n ...options\n });\n\n this.model = options.model || new EmailTemplate(options.data || {});\n this.hasHtml = !!this.model.get('html_template');\n this.hasText = !!this.model.get('text_template');\n this.hasMetadata = this.model.get('metadata') && Object.keys(this.model.get('metadata')).length > 0;\n\n this.template = `\n <div class=\"email-template-view-container p-3\">\n <!-- Header -->\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 <!-- Tabs -->\n <div data-container=\"template-tabs\"></div>\n </div>\n `;\n }\n\n async onInit() {\n const tabs = {};\n\n if (this.hasHtml) {\n tabs['HTML Preview'] = new EmailHtmlPreviewView({\n model: this.model\n });\n }\n\n if (this.hasText) {\n tabs['Text Version'] = new View({\n model: this.model,\n 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>`\n });\n }\n\n if (this.hasMetadata) {\n tabs['Metadata'] = new View({\n model: this.model,\n template: `<pre class=\"email-metadata-content p-3 bg-light border rounded\"><code>{{model.metadata|json}}</code></pre>`\n });\n }\n\n this.tabView = new TabView({\n containerId: 'template-tabs',\n tabs: tabs,\n activeTab: Object.keys(tabs)[0] || ''\n });\n this.addChild(this.tabView);\n }\n}\n\nexport default EmailTemplateView;\n","/**\n * EmailTemplateTablePage - Admin page for managing Email Templates\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { EmailTemplateList, EmailTemplateForms } from '@ext/admin/models/Email.js';\nimport EmailTemplateView from './EmailTemplateView.js';\n\nclass EmailTemplateTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_email_templates',\n pageName: 'Email Templates',\n router: 'admin/email/templates',\n Collection: EmailTemplateList,\n\n formCreate: EmailTemplateForms.create,\n formEdit: EmailTemplateForms.edit,\n itemViewClass: EmailTemplateView,\n clickAction: \"edit\",\n\n viewDialogOptions: {\n header: false,\n size: 'xl',\n scrollable: true\n },\n\n formDialogConfig: { // Dialog options for forms\n size: 'fullscreen',\n },\n\n // Table columns\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'name', label: 'Name', sortable: true },\n { key: 'created', label: 'Created', formatter: 'datetime' },\n { key: 'modified', label: 'Modified', formatter: 'datetime' }\n ],\n\n // Features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No email templates found. Click \"Add Template\" to create your first one.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default EmailTemplateTablePage;\n","/**\n * EmailView - A slick, email-client-like view for sent messages\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport { SentMessage } from '@ext/admin/models/Email.js';\n\nclass EmailView extends View {\n constructor(options = {}) {\n super({\n className: 'email-view',\n ...options\n });\n\n this.model = options.model || new SentMessage(options.data || {});\n this.hasHtml = !!this.model.get('body_html');\n this.hasText = !!this.model.get('body_text');\n this.hasContext = this.model.get('template_context') && Object.keys(this.model.get('template_context')).length > 0;\n\n this.template = `\n <div class=\"email-view-container p-3\">\n <!-- Email Header -->\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 <!-- Email Body Tabs -->\n <div data-container=\"email-tabs\"></div>\n </div>\n `;\n }\n\n async onInit() {\n const tabs = {};\n\n if (this.hasHtml) {\n tabs['HTML'] = new View({\n model: this.model,\n template: `<div class=\"email-html-content border rounded p-3\" style=\"height: 500px; overflow-y: auto;\">{{{model.body_html}}}</div>`\n });\n }\n\n if (this.hasText) {\n tabs['Text'] = new View({\n model: this.model,\n 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>`\n });\n }\n\n if (this.hasContext) {\n tabs['Context'] = new View({\n model: this.model,\n template: `<pre class=\"email-context-content p-3 bg-light border rounded\"><code>{{model.template_context|json}}</code></pre>`\n });\n }\n\n this.tabView = new TabView({\n containerId: 'email-tabs',\n tabs: tabs,\n activeTab: this.hasHtml ? 'HTML' : (this.hasText ? 'Text' : 'Context')\n });\n this.addChild(this.tabView);\n }\n}\n\nSentMessage.VIEW_CLASS = EmailView;\n\n\nexport default EmailView;\n","/**\n * SentMessageTablePage - Admin page for viewing sent emails\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { SentMessageList } from '@ext/admin/models/Email.js';\nimport EmailView from './EmailView.js';\n\nclass SentMessageTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_email_sent',\n pageName: 'Sent Messages',\n router: 'admin/email/sent',\n Collection: SentMessageList,\n \n itemViewClass: EmailView,\n viewDialogOptions: {\n header: false,\n size: 'xl',\n scrollable: true\n },\n\n // Table columns\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'mailbox.email', label: 'From', sortable: true },\n { key: 'to_addresses', label: 'To', sortable: false, formatter: \"list\" },\n { key: 'subject', label: 'Subject', sortable: true },\n { key: 'status', label: 'Status', formatter: 'badge' },\n { key: 'status_reason', label: 'Reason', formatter: \"truncate(80)|default('—')\" },\n { key: 'created', label: 'Created', formatter: 'datetime' }\n ],\n\n // Features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No sent messages found.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default SentMessageTablePage;","/**\n * PublicMessageView - Detail view for a PublicMessage record.\n *\n * Read + status-toggle only. Metadata is rendered generically so new\n * kinds and client-supplied tracking keys appear without code changes.\n */\n\nimport View from '@core/View.js';\nimport DataView from '@core/views/data/DataView.js';\nimport {\n PublicMessage,\n PublicMessageKindOptions,\n PublicMessageMetadataLabels,\n} from '@ext/admin/models/PublicMessage.js';\n\nconst STATUS_BADGE = {\n open: 'bg-warning text-dark',\n closed: 'bg-secondary',\n};\n\nfunction humanizeKey(key) {\n return String(key || '')\n .replace(/[_-]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, c => c.toUpperCase());\n}\n\nclass PublicMessageView extends View {\n constructor(options = {}) {\n super({\n className: 'public-message-view',\n ...options,\n });\n\n this.model = options.model || new PublicMessage(options.data || {});\n\n this.template = `\n <div class=\"public-message-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start mb-3\">\n <div class=\"flex-grow-1\" style=\"min-width: 0;\">\n <div class=\"d-flex align-items-center gap-2 mb-1\">\n <span class=\"badge bg-primary\">{{kindLabel}}</span>\n <span class=\"badge {{statusBadgeClass}}\">{{statusLabel}}</span>\n {{#model.created}}\n <span class=\"text-muted small\"><i class=\"bi bi-clock me-1\"></i>{{model.created|relative}}</span>\n {{/model.created}}\n </div>\n <h5 class=\"mb-1\">{{#model.subject}}{{model.subject}}{{/model.subject}}{{^model.subject}}<span class=\"text-muted fst-italic\">No subject</span>{{/model.subject}}</h5>\n <div class=\"text-muted small d-flex align-items-center gap-3 flex-wrap\">\n <span><i class=\"bi bi-person-fill me-1\"></i>{{model.name}}</span>\n <span><i class=\"bi bi-envelope me-1\"></i><a href=\"mailto:{{safeMailtoEmail}}\">{{model.email}}</a></span>\n {{#model.group.name}}\n <span><i class=\"bi bi-diagram-3 me-1\"></i>{{model.group.name}}</span>\n {{/model.group.name}}\n </div>\n </div>\n </div>\n\n <!-- Submitter -->\n <div class=\"card mb-3\">\n <div class=\"card-header py-2\"><h6 class=\"mb-0\"><i class=\"bi bi-person-lines-fill me-1\"></i>Submitter</h6></div>\n <div data-container=\"submitter\"></div>\n </div>\n\n <!-- Details (metadata) -->\n {{#hasMetadata|bool}}\n <div class=\"card mb-3\">\n <div class=\"card-header py-2\"><h6 class=\"mb-0\"><i class=\"bi bi-tags me-1\"></i>Details</h6></div>\n <div data-container=\"details\"></div>\n </div>\n {{/hasMetadata|bool}}\n\n <!-- Message body -->\n <div class=\"card mb-3\">\n <div class=\"card-header py-2\"><h6 class=\"mb-0\"><i class=\"bi bi-chat-left-text me-1\"></i>Message</h6></div>\n <div class=\"card-body\">\n <pre class=\"mb-0\" style=\"white-space: pre-wrap; word-wrap: break-word; font-family: inherit;\">{{model.message}}</pre>\n </div>\n </div>\n\n <!-- Actions -->\n <div class=\"d-flex align-items-center gap-2\">\n <button class=\"btn {{toggleBtnClass}} btn-sm\" data-action=\"toggle-status\">\n <i class=\"bi {{toggleBtnIcon}} me-1\"></i>{{toggleBtnLabel}}\n </button>\n <a class=\"btn btn-outline-secondary btn-sm\" href=\"mailto:{{safeMailtoEmail}}?subject={{replySubject}}\">\n <i class=\"bi bi-reply me-1\"></i>Reply via Email\n </a>\n </div>\n </div>\n `;\n }\n\n async onBeforeRender() {\n const kindVal = this.model.get('kind') || '';\n const kindOpt = PublicMessageKindOptions.find(o => o.value === kindVal);\n this.kindLabel = kindOpt ? kindOpt.label : kindVal;\n\n const status = this.model.get('status') || 'open';\n this.statusLabel = status.charAt(0).toUpperCase() + status.slice(1);\n this.statusBadgeClass = STATUS_BADGE[status] || 'bg-secondary';\n\n const metadata = this.model.get('metadata') || {};\n this.hasMetadata = Object.keys(metadata).length > 0;\n\n if (status === 'open') {\n this.toggleBtnClass = 'btn-success';\n this.toggleBtnIcon = 'bi-check-circle';\n this.toggleBtnLabel = 'Mark Closed';\n } else {\n this.toggleBtnClass = 'btn-outline-warning';\n this.toggleBtnIcon = 'bi-arrow-counterclockwise';\n this.toggleBtnLabel = 'Mark Open';\n }\n\n const subject = this.model.get('subject') || '';\n this.replySubject = encodeURIComponent(subject ? `Re: ${subject}` : 'Re: your message');\n this.safeMailtoEmail = encodeURIComponent(this.model.get('email') || '');\n }\n\n async onInit() {\n this.submitterView = new DataView({\n containerId: 'submitter',\n model: this.model,\n className: 'p-3',\n columns: 2,\n showEmptyValues: false,\n fields: [\n { name: 'name', label: 'Name', colSize: 6 },\n { name: 'email', label: 'Email', type: 'email', colSize: 6 },\n { name: 'ip_address', label: 'IP', colSize: 6 },\n { name: 'user_agent', label: 'User Agent', colSize: 12 },\n { name: 'group.name', label: 'Group', colSize: 6 },\n ],\n });\n this.addChild(this.submitterView);\n\n const metadata = this.model.get('metadata') || {};\n const metadataKeys = Object.keys(metadata);\n if (metadataKeys.length) {\n const detailFields = metadataKeys.map(key => ({\n name: key,\n label: PublicMessageMetadataLabels[key] || humanizeKey(key),\n colSize: 6,\n }));\n this.detailsView = new DataView({\n containerId: 'details',\n data: metadata,\n className: 'p-3',\n columns: 2,\n showEmptyValues: false,\n fields: detailFields,\n });\n this.addChild(this.detailsView);\n }\n }\n\n async onActionToggleStatus() {\n const next = this.model.get('status') === 'open' ? 'closed' : 'open';\n try {\n await this.model.save({ status: next });\n this.getApp()?.toast?.success(`Message marked ${next}`);\n await this.render();\n } catch (_e) {\n this.getApp()?.toast?.error('Failed to update status');\n }\n }\n}\n\nPublicMessage.VIEW_CLASS = PublicMessageView;\n\nexport default PublicMessageView;\n","/**\n * PublicMessageTablePage - Admin page for reviewing visitor-submitted\n * contact and support messages (bouncer-gated /contact submissions).\n *\n * Read + status-update only. No create, no delete.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport {\n PublicMessageList,\n PublicMessageKindOptions,\n PublicMessageStatusOptions,\n} from '@ext/admin/models/PublicMessage.js';\nimport PublicMessageView from './PublicMessageView.js';\n\nclass PublicMessageTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_public_messages',\n pageName: 'Contact Messages',\n router: 'admin/messaging/public-messages',\n Collection: PublicMessageList,\n\n itemViewClass: PublicMessageView,\n viewDialogOptions: {\n header: false,\n size: 'lg',\n scrollable: true,\n },\n\n defaultQuery: {\n sort: '-created',\n },\n\n columns: [\n {\n key: 'status',\n label: 'Status',\n sortable: true,\n formatter: 'badge',\n filter: {\n type: 'multiselect',\n placeHolder: 'Select Status',\n options: PublicMessageStatusOptions.map(o => o.value),\n },\n },\n {\n key: 'kind',\n label: 'Kind',\n sortable: true,\n formatter: 'badge',\n filter: {\n type: 'multiselect',\n placeHolder: 'Select Kind',\n options: PublicMessageKindOptions.map(o => o.value),\n },\n },\n { key: 'name', label: 'Name', sortable: true, formatter: 'truncate(30)' },\n { key: 'email', label: 'Email', sortable: true },\n { key: 'subject', label: 'Subject', sortable: true, formatter: \"truncate(60)|default('—')\" },\n { key: 'group.name', label: 'Group', sortable: true, formatter: \"default('—')\" },\n { key: 'created', label: 'Created', sortable: true, formatter: 'relative' },\n ],\n\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n emptyMessage: 'No contact or support messages yet. Visitors submit these through the bouncer-gated /contact page.',\n\n batchBarLocation: 'top',\n batchActions: [\n { label: 'Mark Closed', icon: 'bi bi-check-circle', action: 'mark-closed' },\n ],\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false,\n },\n });\n }\n\n async onActionBatchMarkClosed() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const confirmed = await this.getApp().confirm(\n `Mark ${selected.length} message${selected.length > 1 ? 's' : ''} as closed?`\n );\n if (!confirmed) return;\n\n const results = await Promise.allSettled(selected.map(item =>\n item.model.save({ status: 'closed' })\n ));\n const failed = results.filter(r => r.status === 'rejected').length;\n const succeeded = results.length - failed;\n if (succeeded) {\n this.getApp().toast.success(`${succeeded} message(s) closed`);\n }\n if (failed) {\n this.getApp().toast.error(`${failed} message(s) failed to update`);\n }\n this.tableView.collection.fetch();\n }\n}\n\nexport default PublicMessageTablePage;\n","/**\n * PhoneNumberView - Detailed view for a PhoneHub PhoneNumber record\n *\n * Mirrors GeoIPView structure:\n * - Header with primary identifier and quick info\n * - Tabbed content using TabView and DataView sections\n * - Minimal context menu for refresh and delete\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { PhoneNumber } from '@ext/admin/models/Phonehub.js';\n\nclass PhoneNumberView extends View {\n constructor(options = {}) {\n super({\n className: 'phone-number-view',\n ...options\n });\n\n this.model = options.model || new PhoneNumber(options.data || {});\n\n this.template = `\n <div class=\"phone-number-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-center mb-4\">\n <!-- Left Side: Icon & Info -->\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 <!-- Right Side: Actions -->\n <div class=\"d-flex align-items-center gap-4\">\n <div data-container=\"phone-context-menu\"></div>\n </div>\n </div>\n\n <!-- Tabs -->\n <div data-container=\"phone-tabs\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // Overview Tab\n this.overviewView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'phone_number', label: 'Phone Number', cols: 6 },\n { name: 'country_code', label: 'Country Code', cols: 6 },\n { name: 'region', label: 'Region', cols: 6 },\n { name: 'state', label: 'State', cols: 6 },\n { name: 'registered_owner', label: 'Registered Owner', cols: 6 },\n { name: 'owner_type', label: 'Owner Type', formatter: 'capitalize', cols: 6 },\n { name: 'is_valid', label: 'Valid', formatter: 'yesnoicon', cols: 4 },\n { name: 'is_mobile', label: 'Mobile', formatter: 'yesnoicon', cols: 4 },\n { name: 'is_voip', label: 'VOIP', formatter: 'yesnoicon', cols: 4 },\n ]\n });\n\n // Carrier & Status Tab\n this.carrierView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'carrier', label: 'Carrier', cols: 6 },\n { name: 'line_type', label: 'Line Type', formatter: 'capitalize', cols: 6 },\n { name: 'lookup_provider', label: 'Lookup Provider', formatter: 'capitalize', cols: 6 },\n { name: 'lookup_count', label: 'Lookup Count', cols: 6 },\n { name: 'last_lookup_at', label: 'Last Lookup', formatter: 'datetime', cols: 6 },\n { name: 'lookup_expires_at', label: 'Cache Expires', formatter: 'datetime', cols: 6 }\n ]\n });\n\n // Address Tab\n this.addressView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'address_line1', label: 'Address Line 1', cols: 12 },\n { name: 'address_city', label: 'City', cols: 4 },\n { name: 'address_state', label: 'State', cols: 4 },\n { name: 'address_zip', label: 'ZIP', cols: 4 },\n { name: 'address_country', label: 'Country', cols: 6 }\n ]\n });\n\n // Metadata Tab\n this.metadataView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'id', label: 'Record ID', cols: 6 },\n { name: 'created', label: 'Created', formatter: 'datetime', cols: 6 },\n { name: 'modified', label: 'Last Modified', formatter: 'datetime', cols: 6 }\n ]\n });\n\n const tabs = {\n 'Overview': this.overviewView,\n 'Carrier': this.carrierView,\n 'Address': this.addressView,\n 'Metadata': this.metadataView\n };\n\n this.tabView = new TabView({\n containerId: 'phone-tabs',\n tabs,\n activeTab: 'Overview'\n });\n this.addChild(this.tabView);\n\n // Minimal ContextMenu\n const menuItems = [\n { label: 'Refresh Lookup', action: 'refresh-lookup', icon: 'bi-arrow-repeat' },\n { type: 'divider' },\n { label: 'Delete Record', action: 'delete-phone', icon: 'bi-trash', danger: true }\n ];\n\n const ctxMenu = new ContextMenu({\n containerId: 'phone-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: menuItems\n }\n });\n this.addChild(ctxMenu);\n }\n\n // Actions\n\n async onActionRefreshLookup() {\n const number = this.model.get('phone_number');\n if (!number) {\n this.getApp()?.toast?.warning?.('No phone number to lookup');\n return;\n }\n\n try {\n this.getApp()?.toast?.info?.('Refreshing lookup...');\n // Force refresh lookup and update current model\n const resp = await PhoneNumber.lookup(number, { force_refresh: true });\n if (resp.success && resp.data) {\n this.model.set(resp.data);\n await this.render();\n this.getApp()?.toast?.success?.('Lookup refreshed');\n } else {\n const msg = resp.error || 'Lookup failed';\n this.getApp()?.toast?.error?.(msg);\n }\n } catch (e) {\n this.getApp()?.toast?.error?.(e.message || 'Lookup failed');\n }\n }\n\n async onActionDeletePhone() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete the record for \"${this.model.get('phone_number') || 'this number'}\"?`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n\n if (!confirmed) return;\n\n try {\n const resp = await this.model.destroy();\n if (resp?.success) {\n this.emit('phone:deleted', { model: this.model });\n } else {\n this.getApp()?.toast?.error?.('Delete failed');\n }\n } catch (e) {\n this.getApp()?.toast?.error?.(e.message || 'Delete failed');\n }\n }\n\n static async show(phone_number) {\n const resp = await PhoneNumber.lookup(phone_number);\n if (resp?.model) {\n const view = new PhoneNumberView({ model: resp.model });\n await Modal.show(view, { size: 'lg', header: false });\n return;\n }\n Modal.alert({ message: `Could not find phone data for number: ${phone_number}`, type: 'warning' });\n }\n}\n\nPhoneNumberView.MODEL_CLASS = PhoneNumber;\n\nexport default PhoneNumberView;\n","/**\n * PhoneNumberTablePage - PhoneHub numbers management using TablePage component\n * Mirrors the GeoLocatedIPTablePage pattern with minimal, consistent configuration.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { PhoneNumberList, PhoneNumber } from '@ext/admin/models/Phonehub.js';\nimport PhoneNumberView from './PhoneNumberView.js';\n\nclass PhoneNumberTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n\n // Identity\n name: 'admin_phonehub_numbers',\n pageName: 'Phone Numbers',\n router: 'admin/phonehub/numbers',\n\n // Data source\n Collection: PhoneNumberList,\n\n // Item view configuration\n itemView: PhoneNumberView,\n viewDialogOptions: {\n header: false,\n // size: 'xl'\n },\n\n // Column definitions\n columns: [\n { key: 'phone_number', label: 'Phone Number', sortable: true },\n { key: 'carrier', label: 'Carrier', sortable: true, formatter: \"default('—')\" },\n { key: 'line_type', label: 'Line Type', sortable: true, formatter: \"capitalize\" },\n { key: 'is_mobile', label: 'Mobile', formatter: 'yesnoicon' },\n { key: 'is_voip', label: 'VOIP', formatter: 'yesnoicon' },\n { key: 'is_valid', label: 'Valid', formatter: 'yesnoicon' },\n { key: 'registered_owner', label: 'Owner', sortable: true, formatter: \"default('—')\" },\n { key: 'owner_type', label: 'Owner Type', formatter: \"capitalize\" },\n { key: 'last_lookup_at|relative', label: 'Last Lookup', sortable: true},\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Row action\n clickAction: 'view',\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No phone numbers found.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n },\n\n tableViewOptions: {\n addButtonLabel: \"Lookup\",\n addButtonIcon: 'bi-search',\n onAdd: (evt) => {\n evt.preventDefault();\n // Implement the logic for adding a new record\n this.onLookup();\n }\n }\n });\n }\n\n async onLookup() {\n // Implement the logic for adding a new record\n const data = await this.getApp().showForm({\n title: \"Lookup Phone Number\",\n fields: [\n {\n name: 'number',\n type: 'text',\n required: true\n }\n ]\n });\n if (data && data.number) {\n const resp = await PhoneNumber.lookup(data.number);\n if (resp.model) {\n this.tableView._onRowView(resp);\n }\n }\n }\n}\n\nexport default PhoneNumberTablePage;\n","/**\n * SMSView - Detailed view for a PhoneHub SMS record\n *\n * Mirrors GeoIPView structure:\n * - Header with key info (direction, from/to, status)\n * - Tabbed content using TabView and DataView sections\n * - Minimal context menu for refresh and delete\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { SMS } from '@ext/admin/models/Phonehub.js';\n\nclass SMSView extends View {\n constructor(options = {}) {\n super({\n className: 'sms-view',\n ...options\n });\n\n this.model = options.model || new SMS(options.data || {});\n\n this.template = `\n <div class=\"sms-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-center mb-4\">\n <!-- Left Side: Icon & Info -->\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 <!-- Right Side: Actions -->\n <div class=\"d-flex align-items-center gap-4\">\n <div data-container=\"sms-context-menu\"></div>\n </div>\n </div>\n\n <!-- Tabs -->\n <div data-container=\"sms-tabs\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // Message Tab\n this.messageView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'direction', label: 'Direction', formatter: 'capitalize', cols: 4 },\n { name: 'status', label: 'Status', formatter: 'capitalize', cols: 4 },\n { name: 'from_number', label: 'From', cols: 6 },\n { name: 'to_number', label: 'To', cols: 6 },\n { name: 'body', label: 'Message Body', cols: 12 }\n ]\n });\n\n // Delivery Tab\n this.deliveryView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'provider', label: 'Provider', formatter: 'capitalize', cols: 6 },\n { name: 'provider_message_id', label: 'Provider Message ID', cols: 6 },\n { name: 'sent_at', label: 'Sent At', formatter: 'datetime', cols: 6 },\n { name: 'delivered_at', label: 'Delivered At', formatter: 'datetime', cols: 6 },\n { name: 'error_code', label: 'Error Code', cols: 6 },\n { name: 'error_message', label: 'Error Message', cols: 12 }\n ]\n });\n\n // Metadata Tab\n this.metadataView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'id', label: 'Record ID', cols: 6 },\n { name: 'created', label: 'Created', formatter: 'datetime', cols: 6 },\n { name: 'modified', label: 'Last Modified', formatter: 'datetime', cols: 6 }\n ]\n });\n\n const tabs = {\n 'Message': this.messageView,\n 'Delivery': this.deliveryView,\n 'Metadata': this.metadataView\n };\n\n this.tabView = new TabView({\n containerId: 'sms-tabs',\n tabs,\n activeTab: 'Message'\n });\n this.addChild(this.tabView);\n\n // ContextMenu (minimal)\n const menuItems = [\n { label: 'Refresh', action: 'refresh-sms', icon: 'bi-arrow-repeat' },\n { type: 'divider' },\n { label: 'Delete Message', action: 'delete-sms', icon: 'bi-trash', danger: true }\n ];\n\n const ctxMenu = new ContextMenu({\n containerId: 'sms-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: menuItems\n }\n });\n this.addChild(ctxMenu);\n }\n\n // Actions\n\n async onActionRefreshSms() {\n try {\n this.getApp()?.toast?.info?.('Refreshing message...');\n await this.model.fetch();\n await this.render();\n this.getApp()?.toast?.success?.('Message refreshed');\n } catch (e) {\n this.getApp()?.toast?.error?.(e.message || 'Refresh failed');\n }\n }\n\n async onActionDeleteSms() {\n const title = 'Confirm Deletion';\n const msg = `Are you sure you want to delete this message?`;\n const confirmed = await Modal.confirm(msg, title, {\n confirmClass: 'btn-danger',\n confirmText: 'Delete'\n });\n\n if (!confirmed) return;\n\n try {\n const resp = await this.model.destroy();\n if (resp?.success) {\n this.emit('sms:deleted', { model: this.model });\n } else {\n this.getApp()?.toast?.error?.('Delete failed');\n }\n } catch (e) {\n this.getApp()?.toast?.error?.(e.message || 'Delete failed');\n }\n }\n}\n\nSMSView.MODEL_CLASS = SMS;\n\nexport default SMSView;","/**\n * SMSTablePage - PhoneHub SMS management using TablePage component\n * Mirrors the GeoLocatedIPTablePage pattern with minimal, consistent configuration.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { SMSList } from '@ext/admin/models/Phonehub.js';\nimport SMSView from './SMSView.js';\n\nclass SMSTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n\n // Identity\n name: 'admin_phonehub_sms',\n pageName: 'SMS Messages',\n router: 'admin/phonehub/sms',\n\n // Data source\n Collection: SMSList,\n\n // Item view configuration\n itemView: SMSView,\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n // Column definitions\n columns: [\n { key: 'direction', label: 'Direction', sortable: true },\n { key: 'from_number', label: 'From', sortable: true, formatter: \"default('—')\" },\n { key: 'to_number', label: 'To', sortable: true, formatter: \"default('—')\" },\n { key: 'status', label: 'Status', sortable: true },\n { key: 'provider', label: 'Provider', sortable: true, formatter: \"default('—')\" },\n { key: 'body', label: 'Message', formatter: \"default('—')\" },\n { key: 'sent_at', label: 'Sent At', sortable: true, formatter: 'datetime' },\n { key: 'delivered_at', label: 'Delivered At', sortable: true, formatter: 'datetime' },\n { key: 'created', label: 'Created', sortable: true, formatter: 'datetime' }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Row action\n clickAction: 'view',\n\n // Toolbar\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No SMS messages found.',\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default SMSTablePage;","import Page from '@core/Page.js';\nimport { MetricsChart, PieChart } from '@ext/charts/index.js';\nimport TableView from '@core/views/table/TableView.js';\nimport { PushDeliveryList } from '@ext/admin/models/Push.js';\n\nclass PushDashboardPage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n title: 'Push Notifications Dashboard',\n className: 'push-dashboard-page'\n });\n }\n\n async getTemplate() {\n return `\n <div class=\"container-fluid\">\n <h1 class=\"h3 mb-4\">Push Notifications</h1>\n <div class=\"row\">\n <!-- Stat cards -->\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 `;\n }\n\n async onInit() {\n this.deliveriesChart = new MetricsChart({\n containerId: 'deliveries-chart',\n endpoint: '/api/metrics/fetch',\n slugs: ['push_sent', 'push_failed'],\n chartType: 'line'\n });\n this.addChild(this.deliveriesChart);\n\n this.statusChart = new PieChart({\n containerId: 'status-chart',\n endpoint: '/api/account/devices/push/stats' // Assuming this returns data for pie chart\n });\n this.addChild(this.statusChart);\n\n this.recentDeliveries = new TableView({\n containerId: 'recent-deliveries',\n title: 'Recent Deliveries',\n Collection: new PushDeliveryList({ params: { _sort: '-created', _limit: 5 } }),\n columns: [{ key: 'title', label: 'Title' }, { key: 'status', label: 'Status', formatter: 'badge' }]\n });\n this.addChild(this.recentDeliveries);\n\n this.failedDeliveries = new TableView({\n containerId: 'failed-deliveries',\n title: 'Failed Deliveries',\n Collection: new PushDeliveryList({ params: { status: 'failed', _sort: '-created', _limit: 5 } }),\n columns: [{ key: 'title', label: 'Title' }, { key: 'error_message', label: 'Error' }]\n });\n this.addChild(this.failedDeliveries);\n }\n}\n\nexport default PushDashboardPage;\n","import TablePage from '@core/pages/TablePage.js';\nimport { PushConfigList, PushConfigForms } from '@ext/admin/models/Push.js';\n\nclass PushConfigTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_push_configs',\n pageName: 'Push Configurations',\n router: \"admin/push/configs\",\n Collection: PushConfigList,\n formCreate: PushConfigForms.create,\n formEdit: PushConfigForms.edit,\n\n columns: [\n { key: 'id', label: 'ID', width: '70px' },\n { key: 'name', label: 'Name' },\n { key: 'group.name', label: 'Group', formatter: \"default('Default')\" },\n { key: 'fcm_sender_id', label: 'Project ID' },\n { key: 'is_active', label: 'Active', format: 'boolean' },\n ],\n\n searchable: true,\n sortable: true,\n paginated: true,\n showRefresh: true,\n showAdd: true,\n showExport: true,\n actions: [\"edit\", \"delete\"],\n tableOptions: {\n pageSizes: [10, 25, 50],\n defaultPageSize: 25,\n emptyMessage: 'No push configurations found.',\n emptyIcon: 'bi-gear',\n\n }\n });\n }\n}\n\nexport default PushConfigTablePage;\n","import TablePage from '@core/pages/TablePage.js';\nimport { PushTemplateList, PushTemplateForms } from '@ext/admin/models/Push.js';\n\nclass PushTemplateTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_push_templates',\n pageName: 'Push Templates',\n router: \"admin/push/templates\",\n Collection: PushTemplateList,\n formCreate: PushTemplateForms.create,\n formEdit: PushTemplateForms.edit,\n\n columns: [\n { key: 'id', label: 'ID', width: '70px' },\n { key: 'name', label: 'Name' },\n { key: 'category', label: 'Category' },\n { key: 'group.name', label: 'Group', formatter: \"default('Default')\" },\n { key: 'priority', label: 'Priority' },\n { key: 'is_active', label: 'Active', format: 'boolean' },\n ],\n\n searchable: true,\n sortable: true,\n paginated: true,\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n tableOptions: {\n pageSizes: [10, 25, 50],\n defaultPageSize: 25,\n emptyMessage: 'No push templates found.',\n emptyIcon: 'bi-file-earmark-text',\n actions: [\"edit\", \"delete\"],\n }\n });\n }\n}\n\nexport default PushTemplateTablePage;\n","import View from '@core/View.js';\n\nclass PushDeliveryView extends View {\n constructor(options = {}) {\n super({\n className: 'push-delivery-view',\n ...options\n });\n this.model = options.model;\n }\n\n getTemplate() {\n 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 `;\n }\n}\n\nexport default PushDeliveryView;\n","import TablePage from '@core/pages/TablePage.js';\nimport { PushDeliveryList } from '@ext/admin/models/Push.js';\nimport PushDeliveryView from './PushDeliveryView.js';\n\nclass PushDeliveryTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_push_deliveries',\n pageName: 'Push Deliveries',\n router: \"admin/push/deliveries\",\n Collection: PushDeliveryList,\n itemViewClass: PushDeliveryView,\n viewDialogOptions: {\n header: false,\n size: 'md'\n },\n\n columns: [\n { key: 'id', label: 'ID', width: '70px' },\n { key: 'created', label: 'Timestamp', formatter: 'datetime' },\n { key: 'user.display_name', label: 'User' },\n { key: 'device.device_name', label: 'Device' },\n { key: 'title', label: 'Title' },\n { key: 'category', label: 'Category' },\n { key: 'status', label: 'Status', formatter: 'badge' },\n ],\n \n searchable: true,\n sortable: true,\n paginated: true,\n showRefresh: true,\n\n tableOptions: {\n pageSizes: [10, 25, 50],\n defaultPageSize: 25,\n emptyMessage: 'No deliveries found.',\n emptyIcon: 'bi-send',\n actions: [\"view\"],\n }\n });\n }\n}\n\nexport default PushDeliveryTablePage;\n","import View from '@core/View.js';\nimport DataView from '@core/views/data/DataView.js';\n\nclass PushDeviceView extends View {\n constructor(options = {}) {\n super({\n className: 'push-device-view',\n ...options\n });\n this.model = options.model;\n }\n\n getTemplate() {\n 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 `;\n }\n\n onInit() {\n this.dataView = new DataView({\n containerId: 'data-view',\n model: this.model,\n fields: [\n { name: 'platform', label: 'Platform', format: 'badge' },\n { name: 'push_enabled', label: 'Push Enabled', format: 'boolean' },\n { name: 'app_version', label: 'App Version' },\n { name: 'os_version', label: 'OS Version' },\n { name: 'last_seen', label: 'Last Seen', format: 'datetime' },\n { name: 'push_preferences', label: 'Preferences', format: 'json' },\n ]\n });\n this.addChild(this.dataView);\n }\n}\n\nexport default PushDeviceView;\n","import TablePage from '@core/pages/TablePage.js';\nimport { PushDeviceList } from '@ext/admin/models/Push.js';\nimport PushDeviceView from './PushDeviceView.js';\n\nclass PushDeviceTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_push_devices',\n pageName: 'Registered Devices',\n router: \"admin/push/devices\",\n Collection: PushDeviceList,\n itemViewClass: PushDeviceView,\n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n\n columns: [\n { key: 'id', label: 'ID', width: '70px' },\n { key: 'user.display_name', label: 'User' },\n { key: 'device_name', label: 'Device Name' },\n { key: 'platform', label: 'Platform', formatter: 'badge' },\n { key: 'app_version', label: 'App Version' },\n { key: 'push_enabled', label: 'Push Enabled', format: 'boolean' },\n { key: 'last_seen', label: 'Last Seen', formatter: 'datetime' },\n ],\n \n searchable: true,\n sortable: true,\n paginated: true,\n showRefresh: true,\n\n tableOptions: {\n pageSizes: [10, 25, 50],\n defaultPageSize: 25,\n emptyMessage: 'No devices found.',\n emptyIcon: 'bi-phone',\n actions: [\"view\", \"delete\"],\n }\n });\n }\n}\n\nexport default PushDeviceTablePage;\n","/**\n * JobStatsView - Job statistics overview cards\n */\n\nimport View from '@core/View.js';\nimport { Job } from '@ext/admin/models/Job.js';\n\nexport default class JobStatsView extends View {\n constructor(options = {}) {\n super({\n className: 'job-stats-section',\n ...options\n });\n\n this.stats = {\n pending: 0,\n running: 0,\n completed: 0,\n failed: 0,\n scheduled: 0\n };\n\n 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-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</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-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</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-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\">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-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</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-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</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 `;\n }\n\n _onModelChange() {\n this.loadStats();\n if (this.isMounted()) {\n this.render();\n }\n }\n\n async loadStats() {\n this.stats = this.model.attributes.totals;\n }\n}\n","/**\n * JobsRunnersStrip — collapsible runner-fleet summary, top of the\n * Job Engine dashboard.\n *\n * Runners are the primary alert signal for the job system. If they're\n * not alive, nothing moves regardless of how the channels look. This\n * strip pulls /api/jobs/runners and renders one row per runner with\n * its alive state, channels served, job counts, and last heartbeat.\n *\n * Header reads \"X of Y runners alive · N jobs processed\".\n *\n * Empty list (no runners registered) renders a critical-state hint —\n * jobs will not be processed without a runner.\n */\n\nimport View from '@core/View.js';\n\nclass JobsRunnersStrip extends View {\n constructor(options = {}) {\n super({\n ...options,\n tagName: 'div',\n className: `jobs-runners-strip ${options.className || ''}`.trim()\n });\n this.rows = [];\n this.summary = 'Loading runners…';\n this.summaryClass = 'text-muted';\n this.empty = false;\n this._fetchedOnce = false;\n }\n\n async getTemplate() {\n return `\n <details class=\"card shadow-sm\" open>\n <summary class=\"card-header bg-transparent d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center gap-2\">\n <i class=\"bi bi-cpu me-1\"></i>\n <strong>Runners</strong>\n <span class=\"ms-2 small {{summaryClass}}\">{{summary}}</span>\n </div>\n <i class=\"bi bi-chevron-down\"></i>\n </summary>\n <ul class=\"list-group list-group-flush mb-0\">\n {{#empty|bool}}\n <li class=\"list-group-item text-danger small\"><i class=\"bi bi-exclamation-triangle me-2\"></i>No runners registered — jobs will not be processed.</li>\n {{/empty|bool}}\n {{#rows}}\n <li class=\"list-group-item d-flex align-items-center gap-3\">\n <i class=\"bi bi-circle-fill {{dotClass}}\" style=\"font-size:0.55rem;\"></i>\n <div class=\"flex-grow-1 min-w-0\">\n <code class=\"small\">{{runner_id}}</code>\n <div class=\"text-muted small text-truncate\">{{channelsLabel}} · {{processedLabel}}</div>\n </div>\n <span class=\"text-muted small\">{{when}}</span>\n <span class=\"badge {{badgeClass}}\">{{statusLabel}}</span>\n </li>\n {{/rows}}\n </ul>\n </details>\n `;\n }\n\n async onInit() {\n await this._fetch();\n }\n\n async refresh() {\n await this._fetch();\n if (this.isMounted()) await this.render();\n }\n\n async _fetch() {\n const rest = this.getApp()?.rest;\n if (!rest) {\n this.rows = [];\n this._fetchedOnce = true;\n this._reflectState();\n return;\n }\n try {\n const resp = await rest.GET('/api/jobs/runners', { _: Date.now() });\n const list = resp?.data?.data || [];\n this.rows = list.map(r => this._normalize(r));\n } catch (err) {\n console.warn('[JobsRunnersStrip] fetch failed:', err);\n this.rows = [];\n } finally {\n this._fetchedOnce = true;\n this._reflectState();\n }\n }\n\n _reflectState() {\n this.empty = this.rows.length === 0 && this._fetchedOnce;\n this.summary = this._buildSummary();\n const anyDown = this.rows.some(r => !r.alive);\n if (this.empty || (this.rows.length === 0 && this._fetchedOnce)) {\n this.summaryClass = 'text-danger';\n } else if (anyDown) {\n this.summaryClass = 'text-warning';\n } else {\n this.summaryClass = 'text-success';\n }\n }\n\n _normalize(r) {\n const alive = !!r.alive;\n const channels = Array.isArray(r.channels) ? r.channels : [];\n const processed = Number(r.jobs_processed) || 0;\n const failed = Number(r.jobs_failed) || 0;\n return {\n runner_id: r.runner_id || r.id || '—',\n alive,\n dotClass: alive ? 'text-success' : 'text-danger',\n badgeClass: alive ? 'text-bg-success' : 'text-bg-danger',\n statusLabel: alive ? 'alive' : 'down',\n channels,\n channelsLabel: channels.length ? channels.join(', ') : 'no channels',\n processed,\n failed,\n processedLabel: failed > 0\n ? `${processed.toLocaleString()} done · ${failed.toLocaleString()} failed`\n : `${processed.toLocaleString()} done`,\n when: this._relativeTime(r.last_heartbeat)\n };\n }\n\n _buildSummary() {\n if (!this._fetchedOnce) return 'Loading runners…';\n if (!this.rows.length) return 'No runners registered';\n const alive = this.rows.filter(r => r.alive).length;\n const total = this.rows.length;\n const processed = this.rows.reduce((s, r) => s + r.processed, 0);\n return `${alive} of ${total} runner${total === 1 ? '' : 's'} alive · ${processed.toLocaleString()} jobs processed`;\n }\n\n _relativeTime(timestamp) {\n if (!timestamp) return '—';\n const t = typeof timestamp === 'number' ? timestamp * 1000 : new Date(timestamp).getTime();\n if (!t) return '—';\n const diff = Math.floor((Date.now() - t) / 1000);\n if (diff < 60) return `${diff}s ago`;\n if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;\n if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;\n return `${Math.floor(diff / 86400)}d ago`;\n }\n}\n\nexport default JobsRunnersStrip;\n","/**\n * JobOverviewSection — throughput sparklines (jobs published / failed).\n *\n * Both widgets are MetricsMiniChartWidget instances and automatically\n * inherit the chart extension's stats modal, data-table modal, and\n * softMin/softMax bound features.\n *\n * Channel health used to live here too — it's now at the top of\n * JobDashboardPage as JobsHealthStrip since runner availability is the\n * primary alert signal.\n */\nimport View from '@core/View.js';\nimport { MetricsMiniChartWidget } from '@ext/charts/index.js';\n\nexport default class JobOverviewSection extends View {\n constructor(options = {}) {\n super({\n className: 'job-overview-section',\n template: `\n <div class=\"row g-3 align-items-stretch\">\n <div class=\"col-lg-6\" data-container=\"jobs-published-chart\"></div>\n <div class=\"col-lg-6\" data-container=\"jobs-failed-chart\"></div>\n </div>\n `,\n ...options\n });\n }\n\n async onInit() {\n this.jobsPublishedChart = new MetricsMiniChartWidget({\n containerId: 'jobs-published-chart',\n icon: 'bi bi-upload',\n title: 'Jobs Published',\n subtitle: '{{now_value}} {{now_label}}',\n granularity: 'days',\n slugs: ['jobs.published'],\n account: 'global',\n chartType: 'line',\n height: 90,\n showSettings: true,\n showTrending: true,\n showDateRange: false\n });\n this.addChild(this.jobsPublishedChart);\n\n this.jobsFailedChart = new MetricsMiniChartWidget({\n containerId: 'jobs-failed-chart',\n icon: 'bi bi-exclamation-octagon',\n title: 'Jobs Failed',\n subtitle: '{{now_value}} {{now_label}}',\n granularity: 'days',\n slugs: ['jobs.failed'],\n account: 'global',\n chartType: 'line',\n height: 90,\n showSettings: true,\n showTrending: true,\n showDateRange: false\n });\n this.addChild(this.jobsFailedChart);\n }\n}\n","/**\n * JobOperationsSection - Management action buttons\n *\n * Provides the operations card with buttons for running test jobs,\n * clearing stuck jobs, purging old data, and broadcasting commands.\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { Job } from '@ext/admin/models/Job.js';\nimport { JobRunner, JobRunnerForms } from '@ext/admin/models/JobRunner.js';\n\nexport default class JobOperationsSection extends View {\n constructor(options = {}) {\n super({\n className: 'job-operations-section',\n template: `\n <div class=\"card shadow-sm\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\"><i class=\"bi bi-tools me-2\"></i>Operations</h5>\n </div>\n <div class=\"card-body\">\n <div class=\"d-flex flex-wrap gap-2\">\n <button class=\"btn btn-outline-primary\" data-action=\"run-simple-job\">\n <i class=\"bi bi-play-circle me-2\"></i>Run Simple Job\n </button>\n <button class=\"btn btn-outline-primary\" data-action=\"run-test-jobs\">\n <i class=\"bi bi-robot me-2\"></i>Run Test Jobs\n </button>\n <button class=\"btn btn-outline-warning\" data-action=\"clear-stuck\">\n <i class=\"bi bi-wrench me-2\"></i>Clear Stuck\n </button>\n <button class=\"btn btn-outline-warning\" data-action=\"clear-channel\">\n <i class=\"bi bi-eraser me-2\"></i>Clear Channel\n </button>\n <button class=\"btn btn-outline-danger\" data-action=\"purge-jobs\">\n <i class=\"bi bi-trash me-2\"></i>Purge Jobs\n </button>\n <button class=\"btn btn-outline-info\" data-action=\"cleanup-consumers\">\n <i class=\"bi bi-people me-2\"></i>Cleanup Consumers\n </button>\n <button class=\"btn btn-outline-secondary\" data-action=\"runner-broadcast\">\n <i class=\"bi bi-wifi me-2\"></i>Broadcast Command\n </button>\n </div>\n </div>\n </div>\n `,\n ...options\n });\n }\n\n // ── Action handlers ────────────────────────────────────\n\n async onActionRunSimpleJob(event, element) {\n const confirmed = await Modal.confirm({\n title: 'Run Simple Job',\n message: 'This will run a simple test job to verify the job system is working correctly.',\n confirmText: 'Run Test',\n confirmClass: 'btn-success'\n });\n\n if (confirmed) {\n await this.executeJobAction(element, () => Job.test(), 'Test job started successfully');\n }\n }\n\n async onActionRunTestJobs(event, element) {\n const confirmed = await Modal.confirm({\n title: 'Run Test Jobs',\n message: 'This will run a suite of test jobs to verify all job functionalities.',\n confirmText: 'Run Tests',\n confirmClass: 'btn-success'\n });\n\n if (confirmed) {\n await this.executeJobAction(element, () => Job.tests(), 'Test suite started successfully');\n }\n }\n\n async onActionClearStuck(event, element) {\n const channels = this.options.getChannels?.() || [];\n const channelOptions = [\n { value: '', label: 'All Channels' },\n ...channels.map(ch => ({ value: ch.channel, label: ch.channel }))\n ];\n\n const result = await Modal.form({\n title: 'Clear Stuck Jobs',\n formConfig: {\n fields: [\n {\n name: 'channel',\n type: 'select',\n label: 'Channel',\n options: channelOptions,\n value: '',\n help: 'Select specific channel or leave empty for all channels'\n }\n ]\n }\n });\n\n if (result) {\n await this.executeJobAction(\n element,\n () => Job.clearStuck(result.channel || null),\n (response) => {\n const count = response.data.count || 0;\n const channelText = result.channel ? ` from channel \"${result.channel}\"` : '';\n return `Cleared ${count} stuck job${count !== 1 ? 's' : ''}${channelText}`;\n }\n );\n }\n }\n\n async onActionClearChannel(event, element) {\n const channels = this.options.getChannels?.() || [];\n const channelOptions = channels.map(ch => ({ value: ch.channel, label: ch.channel }));\n\n const result = await Modal.form({\n title: 'Clear Channel',\n formConfig: {\n fields: [\n {\n name: 'channel',\n type: 'select',\n label: 'Channel',\n options: channelOptions,\n required: true,\n help: 'Select the channel to clear.'\n }\n ]\n }\n });\n\n if (result) {\n await this.executeJobAction(\n element,\n () => Job.clearChannel(result.channel),\n `Channel \"${result.channel}\" cleared successfully.`\n );\n }\n }\n\n async onActionPurgeJobs(event, element) {\n const result = await Modal.form({\n title: 'Purge Old Jobs',\n formConfig: {\n fields: [\n {\n name: 'days_old',\n type: 'number',\n label: 'Days Old',\n value: 30,\n required: true,\n help: 'Delete jobs older than this many days.'\n }\n ]\n }\n });\n\n if (result) {\n await this.executeJobAction(\n element,\n () => Job.purgeJobs(result.days_old),\n (response) => {\n const count = response.data.count || 0;\n return `Purged ${count} old job(s).`;\n }\n );\n }\n }\n\n async onActionCleanupConsumers(event, element) {\n const confirmed = await Modal.confirm({\n title: 'Cleanup Consumers',\n message: 'This will remove stale consumer records from the system. This is generally safe.',\n confirmText: 'Cleanup',\n confirmClass: 'btn-warning'\n });\n\n if (confirmed) {\n await this.executeJobAction(\n element,\n () => Job.cleanConsumers(),\n (response) => {\n const count = response.data.count || 0;\n return `Cleaned up ${count} consumer(s).`;\n }\n );\n }\n }\n\n async onActionRunnerBroadcast() {\n const result = await Modal.form({\n title: 'Broadcast Command to All Runners',\n formConfig: JobRunnerForms.broadcast\n });\n\n if (result) {\n try {\n const broadcastResult = await JobRunner.broadcast(\n result.command,\n {},\n result.timeout\n );\n\n if (broadcastResult.success) {\n this.getApp().toast.success(`Broadcast command \"${result.command}\" sent successfully`);\n } else {\n this.getApp().toast.error(broadcastResult.data?.error || 'Failed to broadcast command');\n }\n } catch (error) {\n console.error('Failed to broadcast command:', error);\n this.getApp().toast.error('Error broadcasting command: ' + error.message);\n }\n }\n }\n\n // ── Helpers ─────────────────────────────────────────────\n\n async executeJobAction(element, actionFn, successMessage) {\n try {\n element.disabled = true;\n const icon = element.querySelector('i');\n icon?.classList.add('spinning');\n\n const result = await actionFn();\n if (result.success && result.data?.status) {\n const message = typeof successMessage === 'function'\n ? successMessage(result)\n : successMessage;\n this.getApp().toast.success(message);\n } else {\n this.getApp().toast.error(result.data?.error || 'Operation failed');\n }\n } catch (error) {\n console.error('Job action failed:', error);\n this.getApp().toast.error('Error: ' + error.message);\n } finally {\n element.disabled = false;\n const icon = element.querySelector('i');\n icon?.classList.remove('spinning');\n }\n }\n}\n","/**\n * JobDashboardPage — single-page Job Engine dashboard.\n *\n * Layout (top-to-bottom):\n * 1. Runners — JobsRunnersStrip (primary alert signal) — fast 60s\n * 2. Stats — JobStatsView KPI cards — fast 60s\n * 3. Throughput — JobOverviewSection sparklines\n * 4. Operations — JobOperationsSection — lazy\n *\n * Refresh tiers via Page.scheduleRefresh:\n * - 'fast' (60s) → runners + stats\n * - 'slow' (5min) → throughput refetches handled inside the chart widgets\n *\n * Route: system/jobs/dashboard\n * Permission: 'view_jobs' (write actions inside sections gate on\n * 'manage_jobs' independently).\n */\nimport Page from '@core/Page.js';\nimport { JobsEngineStats } from '@ext/admin/models/Job.js';\nimport JobStatsView from './JobStatsView.js';\nimport JobsRunnersStrip from './JobsRunnersStrip.js';\nimport JobOverviewSection from './sections/JobOverviewSection.js';\nimport JobOperationsSection from './sections/JobOperationsSection.js';\n\nexport default class JobDashboardPage extends Page {\n constructor(options = {}) {\n super({\n title: 'Job Engine Dashboard',\n pageName: 'Job Dashboard',\n className: 'job-dashboard-page',\n ...options\n });\n this.pageSubtitle = 'Async job monitoring and runner management';\n }\n\n async getTemplate() {\n return `\n <div class=\"job-dashboard-container container-fluid\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div>\n <h1 class=\"h3 mb-1\">Job Engine</h1>\n <p class=\"text-muted mb-0\">{{pageSubtitle}}</p>\n </div>\n <button type=\"button\"\n class=\"btn btn-outline-secondary btn-sm\"\n data-action=\"refresh-all\"\n title=\"Refresh all panels\">\n <i class=\"bi bi-arrow-clockwise\"></i> Refresh\n </button>\n </div>\n\n <div class=\"mb-3\" data-container=\"job-runners\"></div>\n\n <div data-container=\"job-stats\"></div>\n\n <div data-container=\"job-overview\"></div>\n\n <div class=\"mt-4\" data-container=\"job-operations\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // Shared stats model — fed to JobStatsView and any other section\n // that wants the system-wide totals snapshot.\n this.jobStats = new JobsEngineStats();\n\n // ── Section 1 — Runners (top, primary alert signal) ──────────\n this.runnersStrip = new JobsRunnersStrip({\n containerId: 'job-runners'\n });\n this.addChild(this.runnersStrip);\n\n // ── Section 2 — Stats (system-wide totals) ───────────────────\n this.jobStatsView = new JobStatsView({\n containerId: 'job-stats',\n model: this.jobStats\n });\n this.addChild(this.jobStatsView);\n\n // ── Section 3 — Throughput sparklines ────────────────────────\n this.overviewSection = new JobOverviewSection({\n containerId: 'job-overview',\n model: this.jobStats\n });\n this.addChild(this.overviewSection);\n\n // ── Section 4 — Operations (lazy-mounted, below the fold) ────\n this.operationsSection = new JobOperationsSection({\n containerId: 'job-operations',\n getChannels: () => {\n const health = this.jobStats?.attributes;\n if (!health?.channels) return [];\n return Object.values(health.channels);\n }\n });\n this.addChild(this.operationsSection, { lazyMount: true });\n\n // Kick off the initial fetch in the background — DON'T block first\n // paint. The page renders skeletons immediately; data arrives when\n // the fetch completes and triggers a re-render via the model.\n this.jobStats.fetch().catch(err => {\n console.warn('[JobDashboardPage] initial stats fetch failed:', err);\n });\n }\n\n async onEnter() {\n await super.onEnter();\n // Tiered refresh — runners and stats tick fast (alerting signals).\n this.scheduleRefresh(() => this.runnersStrip?.refresh?.(), 60_000, { tier: 'fast' });\n this.scheduleRefresh(() => this.jobStats?.fetch(), 60_000, { tier: 'fast' });\n }\n\n async onActionRefreshAll(event, element) {\n const button = element || event?.currentTarget || null;\n const icon = button?.querySelector?.('i');\n icon?.classList.add('bi-spin');\n if (button) button.disabled = true;\n try {\n await this.runScheduledRefreshes();\n } finally {\n icon?.classList.remove('bi-spin');\n if (button) button.disabled = false;\n }\n }\n}\n","/**\n * RunnerDetailsView\n *\n * Tabbed runner inspector opened via RunnerDetailsView.show(runner).\n * All tab view classes are internal to this module.\n *\n * Tabs:\n * Overview — identity, channels, uptime (runner object only, no fetch)\n * System Info — OS, CPU, memory, disk, network (GET /api/jobs/runners/sysinfo/<id>)\n * Running Jobs — jobs on this runner (GET /api/jobs/job?runner_id=&status=running)\n * Logs — aggregated logs from running jobs (GET /api/jobs/logs?job_id=)\n * Actions — ping, shutdown, broadcast, export\n */\n\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport { JobRunner } from '@ext/admin/models/JobRunner.js';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction formatUptime(seconds) {\n const d = Math.floor(seconds / 86400);\n const h = Math.floor((seconds % 86400) / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n if (d > 0) return `${d}d ${h}h ${m}m`;\n if (h > 0) return `${h}h ${m}m`;\n return `${m}m`;\n}\n\nfunction formatDuration(seconds) {\n if (seconds < 60) return `${Math.round(seconds)}s`;\n if (seconds < 3600) return `${Math.round(seconds / 60)}m ${Math.round(seconds % 60)}s`;\n return `${Math.round(seconds / 3600)}h ${Math.round((seconds % 3600) / 60)}m`;\n}\n\nfunction formatBytes(bytes) {\n if (bytes == null) return 'N/A';\n if (bytes >= 1e9) return (bytes / 1e9).toFixed(2) + ' GB';\n if (bytes >= 1e6) return (bytes / 1e6).toFixed(2) + ' MB';\n if (bytes >= 1e3) return (bytes / 1e3).toFixed(2) + ' KB';\n return bytes + ' B';\n}\n\nfunction resourceBadgeClass(pct) {\n if (pct >= 80) return 'bg-danger-subtle text-danger';\n if (pct >= 60) return 'bg-warning-subtle text-warning';\n return 'bg-success-subtle text-success';\n}\n\nfunction progressBarClass(pct) {\n if (pct >= 80) return 'bg-danger';\n if (pct >= 60) return 'bg-warning';\n return 'bg-success';\n}\n\nfunction heartbeatAgeSec(isoString) {\n if (!isoString) return null;\n return (Date.now() - new Date(isoString).getTime()) / 1000;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// RunnerOverviewTab\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass RunnerOverviewTab extends View {\n constructor(options = {}) {\n super({ className: 'runner-overview-tab', ...options });\n this.model = options.model || null;\n }\n\n async onBeforeRender() {\n const r = this.model;\n if (!r) return;\n\n this.aliveBadgeClass = r.get('alive') ? 'bg-success-subtle text-success' : 'bg-danger-subtle text-danger';\n this.aliveIcon = r.get('alive') ? 'bi-check-circle-fill' : 'bi-x-circle-fill';\n this.aliveText = r.get('alive') ? 'Alive' : 'Dead';\n\n const started = r.get('started');\n this.startedText = started ? new Date(started).toLocaleString() : 'N/A';\n\n if (started) {\n const sec = (Date.now() - new Date(started).getTime()) / 1000;\n this.uptimeText = formatUptime(sec);\n } else {\n this.uptimeText = 'N/A';\n }\n\n const ageSec = heartbeatAgeSec(r.get('last_heartbeat'));\n if (ageSec !== null) {\n this.heartbeatText = new Date(r.get('last_heartbeat')).toLocaleString();\n this.heartbeatAgeText = `${Math.round(ageSec)}s ago`;\n this.heartbeatClass = ageSec < 30 ? 'text-success' : ageSec < 120 ? 'text-warning' : 'text-danger';\n } else {\n this.heartbeatText = 'N/A';\n this.heartbeatAgeText = '';\n this.heartbeatClass = 'text-muted';\n }\n\n const jobsProcessed = r.get('jobs_processed') || 0;\n const jobsFailed = r.get('jobs_failed') || 0;\n this.errorRate = (jobsProcessed > 0)\n ? ((jobsFailed / jobsProcessed) * 100).toFixed(2) + '%'\n : '0.00%';\n }\n\n async getTemplate() {\n return `\n {{^model}}\n <div class=\"alert alert-warning\"><i class=\"bi bi-exclamation-triangle me-2\"></i>No runner data available.</div>\n {{/model}}\n\n {{#model}}\n <div class=\"card border-0 shadow-sm mb-3\">\n <div class=\"card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0 fw-semibold\">\n <i class=\"bi bi-info-circle text-primary me-2\"></i>Identity\n </h6>\n <span class=\"badge {{aliveBadgeClass}}\">\n <i class=\"bi {{aliveIcon}} me-1\"></i>{{aliveText}}\n </span>\n </div>\n <div class=\"card-body\">\n <div class=\"row g-3\">\n <div class=\"col-md-6\">\n <table class=\"table table-sm table-borderless mb-0\">\n <tbody>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase pe-3\" style=\"width:38%;white-space:nowrap;font-size:0.72rem;\">Runner ID</td>\n <td class=\"font-monospace small\">{{model.runner_id}}</td>\n </tr>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase\" style=\"font-size:0.72rem;\">Started</td>\n <td class=\"small\">{{startedText}}</td>\n </tr>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase\" style=\"font-size:0.72rem;\">Uptime</td>\n <td class=\"small fw-semibold\">{{uptimeText}}</td>\n </tr>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase\" style=\"font-size:0.72rem;\">Heartbeat</td>\n <td class=\"small {{heartbeatClass}}\">\n {{heartbeatText}}\n {{#heartbeatAgeText}}\n <span class=\"text-muted\">({{heartbeatAgeText}})</span>\n {{/heartbeatAgeText}}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <div class=\"col-md-6\">\n <table class=\"table table-sm table-borderless mb-0\">\n <tbody>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase pe-3\" style=\"width:38%;white-space:nowrap;font-size:0.72rem;\">Jobs Done</td>\n <td class=\"small fw-bold text-success\">{{model.jobs_processed|number}}</td>\n </tr>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase\" style=\"font-size:0.72rem;\">Jobs Failed</td>\n <td class=\"small fw-bold text-danger\">{{model.jobs_failed|number}}</td>\n </tr>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase\" style=\"font-size:0.72rem;\">Error Rate</td>\n <td class=\"small\">{{errorRate}}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n {{#model.channels.length}}\n <div class=\"mt-3 pt-3 border-top\">\n <div class=\"text-muted small fw-semibold text-uppercase mb-2\" style=\"font-size:0.72rem;\">Assigned Channels</div>\n <div class=\"d-flex flex-wrap gap-2\">\n {{#model.channels}}\n <span class=\"badge bg-primary-subtle text-primary px-3 py-2\">\n <i class=\"bi bi-circle-fill me-1\" style=\"font-size:0.4rem;vertical-align:middle;\"></i>{{.}}\n </span>\n {{/model.channels}}\n </div>\n </div>\n {{/model.channels.length}}\n </div>\n </div>\n\n <div class=\"alert alert-light border d-flex align-items-center gap-2 mb-0\" style=\"font-size:0.83rem;\">\n <i class=\"bi bi-cpu text-primary flex-shrink-0\"></i>\n <span>CPU, memory, disk, and network detail is in the <strong>System Info</strong> tab.</span>\n </div>\n {{/model}}\n `;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// RunnerSysinfoTab\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass RunnerSysinfoTab extends View {\n constructor(options = {}) {\n super({ className: 'runner-sysinfo-tab', ...options });\n this.model = options.model || null;\n this.sysinfo = null;\n this.sysinfoError = null;\n this.loading = false;\n this.loaded = false;\n }\n\n async onTabActivated() {\n if (this.loaded) return;\n this.loaded = true;\n this.loading = true;\n this.sysinfoError = null;\n await this.render();\n await this.loadSysinfo();\n this.loading = false;\n await this.render();\n }\n\n async loadSysinfo() {\n try {\n const resp = await this.getApp().rest.GET(\n `/api/jobs/runners/sysinfo/${this.model.get('runner_id')}`\n );\n if (resp.success && resp.data) {\n const payload = resp.data.data || resp.data;\n if (payload && payload.status === 'error') {\n this.sysinfoError = payload.error || 'Runner reported an error collecting sysinfo.';\n return;\n }\n if (!resp.data.status) {\n this.sysinfoError = resp.data.error || 'Could not load system info.';\n return;\n }\n this.sysinfo = payload.result || payload;\n this.enrichSysinfo();\n } else {\n this.sysinfoError = 'Could not load system info.';\n }\n } catch (e) {\n this.sysinfoError = e.message || 'Request failed.';\n }\n }\n\n enrichSysinfo() {\n const s = this.sysinfo;\n if (!s) return;\n\n if (s.memory) {\n s.memory.totalFmt = formatBytes(s.memory.total);\n s.memory.usedFmt = formatBytes(s.memory.used);\n s.memory.availableFmt = formatBytes(s.memory.available);\n s.memory.barClass = progressBarClass(s.memory.percent);\n s.memory.badgeClass = resourceBadgeClass(s.memory.percent);\n }\n\n if (s.disk) {\n s.disk.totalFmt = formatBytes(s.disk.total);\n s.disk.usedFmt = formatBytes(s.disk.used);\n s.disk.freeFmt = formatBytes(s.disk.free);\n s.disk.barClass = progressBarClass(s.disk.percent);\n s.disk.badgeClass = resourceBadgeClass(s.disk.percent);\n }\n\n if (s.network) {\n s.network.bytesRecvFmt = formatBytes(s.network.bytes_recv);\n s.network.bytesSentFmt = formatBytes(s.network.bytes_sent);\n s.network.errClass = (s.network.errin > 0 || s.network.errout > 0)\n ? 'text-danger fw-bold' : 'text-success';\n s.network.dropClass = (s.network.dropin > 0 || s.network.dropout > 0)\n ? 'text-warning fw-bold' : 'text-success';\n }\n\n const cpuPct = s.cpu_load || 0;\n s.cpuLoadBarClass = progressBarClass(cpuPct);\n s.cpuLoadBadgeClass = resourceBadgeClass(cpuPct);\n\n if (s.cpu && s.cpu.freq) {\n s.cpu.freqText = `${Math.round(s.cpu.freq.current).toLocaleString()} MHz current`\n + ` · ${Math.round(s.cpu.freq.max).toLocaleString()} MHz max`;\n } else if (s.cpu) {\n s.cpu.freqText = null;\n }\n\n if (s.cpus_load && s.cpus_load.length) {\n s.cpuCores = s.cpus_load.map((pct, i) => ({\n index: i,\n pct: pct.toFixed(1),\n barClass: progressBarClass(pct)\n }));\n } else {\n s.cpuCores = [];\n }\n\n s.bootDatetime = s.boot_time ? new Date(s.boot_time * 1000).toLocaleString() : null;\n s.collectedText = s.datetime || null;\n }\n\n async onActionRefreshSysinfo() {\n this.loaded = false;\n this.sysinfo = null;\n this.sysinfoError = null;\n await this.onTabActivated();\n }\n\n async getTemplate() {\n return `\n {{#loading|bool}}\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n <div class=\"mt-2 text-muted small\">Loading system info…</div>\n </div>\n {{/loading|bool}}\n\n {{^loading|bool}}\n\n {{#sysinfoError|bool}}\n <div class=\"alert alert-warning d-flex align-items-start gap-2\">\n <i class=\"bi bi-exclamation-triangle flex-shrink-0 mt-1\"></i>\n <div>\n <strong>Could not load system info</strong><br>\n <span class=\"small\">{{sysinfoError}}</span><br>\n <button class=\"btn btn-sm btn-outline-warning mt-2\" data-action=\"refresh-sysinfo\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Retry\n </button>\n </div>\n </div>\n {{/sysinfoError|bool}}\n\n {{#sysinfo|bool}}\n\n <div class=\"d-flex justify-content-end align-items-center gap-3 mb-3\">\n {{#sysinfo.collectedText}}\n <small class=\"text-muted\">Collected {{sysinfo.collectedText}}</small>\n {{/sysinfo.collectedText}}\n <button class=\"btn btn-sm btn-outline-secondary\" data-action=\"refresh-sysinfo\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Refresh\n </button>\n </div>\n\n <!-- OS -->\n <div class=\"card border-0 shadow-sm mb-3\">\n <div class=\"card-header bg-white border-bottom py-2\">\n <h6 class=\"mb-0 fw-semibold\"><i class=\"bi bi-hdd-rack text-primary me-2\"></i>Operating System</h6>\n </div>\n <div class=\"card-body p-0\">\n <table class=\"table table-sm table-borderless mb-0\">\n <tbody>\n {{#sysinfo.os}}\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase ps-3 pe-2\" style=\"width:20%;font-size:0.72rem;white-space:nowrap;\">Hostname</td>\n <td class=\"font-monospace small\">{{.hostname}}</td>\n <td class=\"text-muted small fw-semibold text-uppercase pe-2\" style=\"width:15%;font-size:0.72rem;white-space:nowrap;\">OS</td>\n <td class=\"small\">{{.system}}</td>\n </tr>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase ps-3 pe-2\" style=\"font-size:0.72rem;\">Release</td>\n <td class=\"font-monospace small\">{{.release}}</td>\n <td class=\"text-muted small fw-semibold text-uppercase pe-2\" style=\"font-size:0.72rem;\">Machine</td>\n <td class=\"font-monospace small\">{{.machine}}</td>\n </tr>\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase ps-3 pe-2\" style=\"font-size:0.72rem;\">Version</td>\n <td colspan=\"3\" class=\"font-monospace small text-muted\" style=\"font-size:0.76rem;\">{{.version}}</td>\n </tr>\n {{/sysinfo.os}}\n {{#sysinfo.bootDatetime}}\n <tr>\n <td class=\"text-muted small fw-semibold text-uppercase ps-3 pe-2\" style=\"font-size:0.72rem;\">Boot Time</td>\n <td colspan=\"3\" class=\"small\">{{sysinfo.bootDatetime}}</td>\n </tr>\n {{/sysinfo.bootDatetime}}\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- CPU -->\n <div class=\"card border-0 shadow-sm mb-3\">\n <div class=\"card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0 fw-semibold\"><i class=\"bi bi-cpu text-primary me-2\"></i>CPU</h6>\n <span class=\"badge {{sysinfo.cpuLoadBadgeClass}}\">{{sysinfo.cpu_load}}% overall</span>\n </div>\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between mb-1\">\n <small class=\"fw-semibold text-muted\">Overall Load</small>\n <small class=\"fw-bold\">{{sysinfo.cpu_load}}%</small>\n </div>\n <div class=\"progress mb-2\" style=\"height:8px;\">\n <div class=\"progress-bar {{sysinfo.cpuLoadBarClass}}\" style=\"width:{{sysinfo.cpu_load}}%;\"></div>\n </div>\n {{#sysinfo.cpu}}\n <div class=\"text-muted small mb-3\">\n {{.count}} logical cores\n {{#.freqText}} · {{.freqText}}{{/.freqText}}\n </div>\n {{/sysinfo.cpu}}\n\n {{#sysinfo.cpuCores.length}}\n <div class=\"row g-2\">\n {{#sysinfo.cpuCores}}\n <div class=\"col-6 col-md-3\">\n <div class=\"border rounded p-2 bg-light text-center\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.65rem;\">Core {{.index}}</div>\n <div class=\"fw-bold small\">{{.pct}}%</div>\n <div class=\"progress mt-1\" style=\"height:4px;\">\n <div class=\"progress-bar {{.barClass}}\" style=\"width:{{.pct}}%;\"></div>\n </div>\n </div>\n </div>\n {{/sysinfo.cpuCores}}\n </div>\n {{/sysinfo.cpuCores.length}}\n </div>\n </div>\n\n <!-- Memory -->\n {{#sysinfo.memory}}\n <div class=\"card border-0 shadow-sm mb-3\">\n <div class=\"card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0 fw-semibold\"><i class=\"bi bi-memory text-primary me-2\"></i>Memory</h6>\n <span class=\"badge {{.badgeClass}}\">{{.percent}}% used</span>\n </div>\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between mb-1\">\n <small class=\"fw-semibold text-muted\">RAM Usage</small>\n <small class=\"fw-bold\">{{.usedFmt}} / {{.totalFmt}}</small>\n </div>\n <div class=\"progress mb-3\" style=\"height:8px;\">\n <div class=\"progress-bar {{.barClass}}\" style=\"width:{{.percent}}%;\"></div>\n </div>\n <div class=\"row g-2 text-center\">\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Total</div>\n <div class=\"fw-semibold small\">{{.totalFmt}}</div>\n </div>\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Used</div>\n <div class=\"fw-semibold small\">{{.usedFmt}}</div>\n </div>\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Available</div>\n <div class=\"fw-semibold small text-success\">{{.availableFmt}}</div>\n </div>\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Percent</div>\n <div class=\"fw-semibold small\">{{.percent}}%</div>\n </div>\n </div>\n </div>\n </div>\n {{/sysinfo.memory}}\n\n <!-- Disk -->\n {{#sysinfo.disk}}\n <div class=\"card border-0 shadow-sm mb-3\">\n <div class=\"card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center\">\n <h6 class=\"mb-0 fw-semibold\"><i class=\"bi bi-hdd text-primary me-2\"></i>Disk (Root)</h6>\n <span class=\"badge {{.badgeClass}}\">{{.percent}}% used</span>\n </div>\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between mb-1\">\n <small class=\"fw-semibold text-muted\">Disk Usage</small>\n <small class=\"fw-bold\">{{.usedFmt}} / {{.totalFmt}}</small>\n </div>\n <div class=\"progress mb-3\" style=\"height:8px;\">\n <div class=\"progress-bar {{.barClass}}\" style=\"width:{{.percent}}%;\"></div>\n </div>\n <div class=\"row g-2 text-center\">\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Total</div>\n <div class=\"fw-semibold small\">{{.totalFmt}}</div>\n </div>\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Used</div>\n <div class=\"fw-semibold small\">{{.usedFmt}}</div>\n </div>\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Free</div>\n <div class=\"fw-semibold small text-success\">{{.freeFmt}}</div>\n </div>\n <div class=\"col-6 col-md-3\">\n <div class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;\">Percent</div>\n <div class=\"fw-semibold small\">{{.percent}}%</div>\n </div>\n </div>\n </div>\n </div>\n {{/sysinfo.disk}}\n\n <!-- Network -->\n {{#sysinfo.network}}\n <div class=\"card border-0 shadow-sm mb-3\">\n <div class=\"card-header bg-white border-bottom py-2\">\n <h6 class=\"mb-0 fw-semibold\"><i class=\"bi bi-diagram-3 text-primary me-2\"></i>Network</h6>\n </div>\n <div class=\"card-body\">\n <div class=\"row g-2\">\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-arrow-down text-primary me-1\"></i>Bytes Recv</div>\n <div class=\"fw-bold font-monospace small\">{{.bytesRecvFmt}}</div>\n </div>\n </div>\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-arrow-up text-primary me-1\"></i>Bytes Sent</div>\n <div class=\"fw-bold font-monospace small\">{{.bytesSentFmt}}</div>\n </div>\n </div>\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-share text-primary me-1\"></i>TCP Connections</div>\n <div class=\"fw-bold font-monospace small\">{{.tcp_cons|number}}</div>\n </div>\n </div>\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-arrow-down me-1\"></i>Packets Recv</div>\n <div class=\"fw-bold font-monospace small\">{{.packets_recv|number}}</div>\n </div>\n </div>\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-arrow-up me-1\"></i>Packets Sent</div>\n <div class=\"fw-bold font-monospace small\">{{.packets_sent|number}}</div>\n </div>\n </div>\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-exclamation-triangle me-1\"></i>Errors In / Out</div>\n <div class=\"fw-bold font-monospace small {{.errClass}}\">{{.errin}} / {{.errout}}</div>\n </div>\n </div>\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-x-circle me-1\"></i>Drops In</div>\n <div class=\"fw-bold font-monospace small {{.dropClass}}\">{{.dropin}}</div>\n </div>\n </div>\n <div class=\"col-6 col-md-4\">\n <div class=\"border rounded p-2 bg-light\">\n <div class=\"text-muted fw-semibold text-uppercase mb-1\" style=\"font-size:0.67rem;\"><i class=\"bi bi-x-circle me-1\"></i>Drops Out</div>\n <div class=\"fw-bold font-monospace small {{.dropClass}}\">{{.dropout}}</div>\n </div>\n </div>\n </div>\n </div>\n </div>\n {{/sysinfo.network}}\n\n <!-- Logged-in Users -->\n <div class=\"card border-0 shadow-sm\">\n <div class=\"card-header bg-white border-bottom py-2\">\n <h6 class=\"mb-0 fw-semibold\"><i class=\"bi bi-person-badge text-primary me-2\"></i>Logged-in Users</h6>\n </div>\n {{#sysinfo.users.length}}\n <ul class=\"list-group list-group-flush\">\n {{#sysinfo.users}}\n <li class=\"list-group-item font-monospace small\">{{.name|default:'unknown'}}\n {{#.terminal}}<span class=\"text-muted ms-2\">{{.terminal}}</span>{{/.terminal}}\n </li>\n {{/sysinfo.users}}\n </ul>\n {{/sysinfo.users.length}}\n {{^sysinfo.users.length}}\n <div class=\"card-body text-center text-muted py-4\">\n <i class=\"bi bi-person-x fs-2 d-block mb-2 opacity-50\"></i>\n <div class=\"small\">No users currently logged in</div>\n </div>\n {{/sysinfo.users.length}}\n </div>\n\n {{/sysinfo|bool}}\n {{/loading|bool}}\n `;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// RunnerJobsTab\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass RunnerJobsTab extends View {\n constructor(options = {}) {\n super({ className: 'runner-jobs-tab', ...options });\n this.model = options.model || null;\n this.jobs = [];\n this.loading = false;\n this.loaded = false;\n }\n\n async onTabActivated() {\n if (this.loaded) return;\n this.loaded = true;\n this.loading = true;\n await this.render();\n await this.loadJobs();\n this.loading = false;\n await this.render();\n }\n\n async loadJobs() {\n try {\n const resp = await this.getApp().rest.GET(\n `/api/jobs/job?runner_id=${this.model.get('runner_id')}&status=running&size=50`\n );\n if (resp.success && resp.data && resp.data.status) {\n const now = Date.now() / 1000;\n this.jobs = (resp.data.data || []).map(job => ({\n ...job,\n durationText: job.started_at\n ? formatDuration(now - new Date(job.started_at).getTime() / 1000)\n : 'N/A',\n startedText: job.started_at\n ? new Date(job.started_at).toLocaleTimeString()\n : 'N/A',\n attemptBadgeClass: job.attempt > 1\n ? 'bg-danger-subtle text-danger'\n : 'bg-warning-subtle text-warning'\n }));\n } else {\n this.jobs = [];\n }\n } catch (e) {\n this.jobs = [];\n this.showError('Could not load running jobs: ' + e.message);\n }\n }\n\n async onActionRefreshJobs() {\n this.loaded = false;\n this.jobs = [];\n await this.onTabActivated();\n }\n\n async onActionViewJob(event, element) {\n const jobId = element.dataset.jobId;\n this.emit('job:view', { jobId, runner: this.model });\n }\n\n async onActionCancelJob(event, element) {\n const jobId = element.dataset.jobId;\n const ok = await Modal.confirm(\n 'Cancel this job? The runner will receive a cooperative cancel signal.',\n 'Cancel Job',\n { confirmText: 'Cancel Job', confirmClass: 'btn-warning' }\n );\n if (!ok) return;\n\n try {\n const resp = await this.getApp().rest.POST(\n `/api/jobs/job/${jobId}`, { cancel_request: true }\n );\n if (resp.success && resp.data && resp.data.status) {\n this.showSuccess('Cancel signal sent.');\n this.loaded = false;\n await this.onTabActivated();\n } else {\n this.showError((resp.data && resp.data.error) || 'Could not cancel job.');\n }\n } catch (e) {\n this.showError('Could not cancel job: ' + e.message);\n }\n }\n\n async getTemplate() {\n return `\n {{#loading|bool}}\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n <div class=\"mt-2 text-muted small\">Loading running jobs…</div>\n </div>\n {{/loading|bool}}\n\n {{^loading|bool}}\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <small class=\"text-muted\">{{jobs.length}} job(s) currently executing on this runner</small>\n <button class=\"btn btn-sm btn-outline-secondary\" data-action=\"refresh-jobs\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Refresh\n </button>\n </div>\n\n {{#jobs.length}}\n <div class=\"card border-0 shadow-sm\">\n <div class=\"table-responsive\">\n <table class=\"table table-sm table-hover align-middle mb-0\">\n <thead class=\"table-light\">\n <tr>\n <th class=\"ps-3 border-0 text-muted fw-semibold text-uppercase\" style=\"font-size:0.72rem;letter-spacing:0.04em;\">Job ID</th>\n <th class=\"border-0 text-muted fw-semibold text-uppercase\" style=\"font-size:0.72rem;letter-spacing:0.04em;\">Function</th>\n <th class=\"border-0 text-muted fw-semibold text-uppercase\" style=\"font-size:0.72rem;letter-spacing:0.04em;\">Channel</th>\n <th class=\"border-0 text-muted fw-semibold text-uppercase\" style=\"font-size:0.72rem;letter-spacing:0.04em;\">Started</th>\n <th class=\"border-0 text-muted fw-semibold text-uppercase\" style=\"font-size:0.72rem;letter-spacing:0.04em;\">Duration</th>\n <th class=\"border-0 text-muted fw-semibold text-uppercase\" style=\"font-size:0.72rem;letter-spacing:0.04em;\">Attempt</th>\n <th class=\"border-0 text-end pe-3 text-muted fw-semibold text-uppercase\" style=\"font-size:0.72rem;letter-spacing:0.04em;\">Actions</th>\n </tr>\n </thead>\n <tbody>\n {{#jobs}}\n <tr>\n <td class=\"ps-3\">\n <span class=\"font-monospace text-primary small\" title=\"{{.id}}\">{{.id|truncate:12}}</span>\n </td>\n <td>\n <span class=\"font-monospace text-muted small\" title=\"{{.func}}\">{{.func|truncate:42}}</span>\n </td>\n <td>\n <span class=\"badge bg-primary-subtle text-primary\">{{.channel}}</span>\n </td>\n <td><small class=\"text-muted\">{{.startedText}}</small></td>\n <td><span class=\"badge bg-light text-secondary border\">{{.durationText}}</span></td>\n <td><span class=\"badge {{.attemptBadgeClass}}\">{{.attempt}}</span></td>\n <td class=\"text-end pe-3\">\n <div class=\"btn-group btn-group-sm\">\n <button class=\"btn btn-outline-primary btn-sm\" data-action=\"view-job\"\n data-job-id=\"{{.id}}\" title=\"View job details\">\n <i class=\"bi bi-eye\"></i>\n </button>\n <button class=\"btn btn-outline-warning btn-sm\" data-action=\"cancel-job\"\n data-job-id=\"{{.id}}\" title=\"Cancel job\">\n <i class=\"bi bi-x-circle\"></i>\n </button>\n </div>\n </td>\n </tr>\n {{/jobs}}\n </tbody>\n </table>\n </div>\n </div>\n {{/jobs.length}}\n\n {{^jobs.length}}\n <div class=\"text-center text-muted py-5\">\n <i class=\"bi bi-list-task fs-2 d-block mb-2 opacity-50\"></i>\n <div class=\"small\">No jobs currently executing on this runner</div>\n </div>\n {{/jobs.length}}\n {{/loading|bool}}\n `;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// RunnerLogsTab\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass RunnerLogsTab extends View {\n constructor(options = {}) {\n super({ className: 'runner-logs-tab', ...options });\n this.model = options.model || null;\n this.logs = [];\n this.filteredLogs = [];\n this.logFilter = 'all';\n this.loading = false;\n this.loaded = false;\n // precomputed filter button classes\n this.filterAllClass = 'btn-primary';\n this.filterDebugClass = 'btn-outline-secondary';\n this.filterInfoClass = 'btn-outline-primary';\n this.filterWarnClass = 'btn-outline-warning';\n this.filterErrorClass = 'btn-outline-danger';\n }\n\n async onTabActivated() {\n if (this.loaded) return;\n this.loaded = true;\n this.loading = true;\n await this.render();\n await this.loadLogs();\n this.loading = false;\n await this.render();\n }\n\n async loadLogs() {\n try {\n // Get running job IDs for this runner first\n const jobsResp = await this.getApp().rest.GET(\n `/api/jobs/job?runner_id=${this.model.get('runner_id')}&status=running&size=50`\n );\n\n const jobIds = [];\n if (jobsResp.success && jobsResp.data && jobsResp.data.status) {\n (jobsResp.data.data || []).forEach(j => jobIds.push(j.id));\n }\n\n if (!jobIds.length) {\n this.logs = [];\n return;\n }\n\n // Fetch logs for each job in parallel (cap at 5 jobs)\n const promises = jobIds.slice(0, 5).map(id =>\n this.getApp().rest.GET(`/api/jobs/logs?job_id=${id}&sort=-created&size=20`)\n .then(r => (r.success && r.data && r.data.status) ? (r.data.data || []) : [])\n .catch(() => [])\n );\n\n const results = await Promise.all(promises);\n const all = [].concat(...results);\n\n all.sort((a, b) => new Date(b.created) - new Date(a.created));\n this.logs = all.slice(0, 50).map(log => ({\n ...log,\n levelBadgeClass: this.getLogLevelClass(log.kind),\n kindDisplay: (log.kind || 'info').toUpperCase(),\n createdText: new Date(log.created).toLocaleTimeString()\n }));\n } catch (e) {\n this.logs = [];\n this.showError('Could not load logs: ' + e.message);\n }\n }\n\n getLogLevelClass(kind) {\n const map = {\n debug: 'bg-secondary-subtle text-secondary',\n info: 'bg-primary-subtle text-primary',\n warn: 'bg-warning-subtle text-warning',\n error: 'bg-danger-subtle text-danger'\n };\n return map[kind] || 'bg-secondary-subtle text-secondary';\n }\n\n async onBeforeRender() {\n this.filteredLogs = this.logFilter === 'all'\n ? this.logs\n : this.logs.filter(l => l.kind === this.logFilter);\n\n this.filterAllClass = this.logFilter === 'all' ? 'btn-primary' : 'btn-outline-secondary';\n this.filterDebugClass = this.logFilter === 'debug' ? 'btn-secondary' : 'btn-outline-secondary';\n this.filterInfoClass = this.logFilter === 'info' ? 'btn-primary' : 'btn-outline-primary';\n this.filterWarnClass = this.logFilter === 'warn' ? 'btn-warning' : 'btn-outline-warning';\n this.filterErrorClass = this.logFilter === 'error' ? 'btn-danger' : 'btn-outline-danger';\n }\n\n async onActionFilterLogs(event, element) {\n this.logFilter = element.dataset.kind || 'all';\n await this.render();\n }\n\n async onActionRefreshLogs() {\n this.loaded = false;\n this.logs = [];\n this.logFilter = 'all';\n await this.onTabActivated();\n }\n\n async getTemplate() {\n return `\n {{#loading|bool}}\n <div class=\"text-center py-5\">\n <div class=\"spinner-border text-primary\" role=\"status\"></div>\n <div class=\"mt-2 text-muted small\">Loading logs…</div>\n </div>\n {{/loading|bool}}\n\n {{^loading|bool}}\n <div class=\"card border-0 shadow-sm\">\n <div class=\"card-header bg-white border-bottom py-2 d-flex align-items-center gap-2 flex-wrap\">\n <small class=\"text-muted fw-semibold me-1\">Filter:</small>\n <button class=\"btn btn-sm {{filterAllClass}}\" data-action=\"filter-logs\" data-kind=\"all\">All</button>\n <button class=\"btn btn-sm {{filterDebugClass}}\" data-action=\"filter-logs\" data-kind=\"debug\">Debug</button>\n <button class=\"btn btn-sm {{filterInfoClass}}\" data-action=\"filter-logs\" data-kind=\"info\">Info</button>\n <button class=\"btn btn-sm {{filterWarnClass}}\" data-action=\"filter-logs\" data-kind=\"warn\">Warning</button>\n <button class=\"btn btn-sm {{filterErrorClass}}\" data-action=\"filter-logs\" data-kind=\"error\">Error</button>\n <div class=\"ms-auto d-flex align-items-center gap-2\">\n <small class=\"text-muted\">{{filteredLogs.length}} entries</small>\n <button class=\"btn btn-sm btn-outline-secondary\" data-action=\"refresh-logs\">\n <i class=\"bi bi-arrow-clockwise\"></i>\n </button>\n </div>\n </div>\n\n <div style=\"max-height:420px;overflow-y:auto;\">\n {{#filteredLogs.length}}\n {{#filteredLogs}}\n <div class=\"d-flex align-items-start gap-2 px-3 py-2 border-bottom font-monospace\" style=\"font-size:0.78rem;\">\n <span class=\"text-muted flex-shrink-0 pt-1\" style=\"min-width:65px;\">{{.createdText}}</span>\n <span class=\"badge {{.levelBadgeClass}} flex-shrink-0\" style=\"margin-top:1px;\">{{.kindDisplay}}</span>\n <span class=\"flex-grow-1 text-break\">{{.message}}</span>\n </div>\n {{/filteredLogs}}\n {{/filteredLogs.length}}\n\n {{^filteredLogs.length}}\n <div class=\"text-center text-muted py-5\">\n <i class=\"bi bi-journal fs-2 d-block mb-2 opacity-50\"></i>\n <div class=\"small\">No log entries</div>\n </div>\n {{/filteredLogs.length}}\n </div>\n </div>\n {{/loading|bool}}\n `;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// RunnerActionsTab\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass RunnerActionsTab extends View {\n constructor(options = {}) {\n super({ className: 'runner-actions-tab', ...options });\n this.model = options.model || null;\n this.pingResult = null;\n }\n\n async onActionPing() {\n this.pingResult = null;\n await this.render();\n try {\n const resp = await this.getApp().rest.POST('/api/jobs/runners/ping', {\n runner_id: this.model.get('runner_id'),\n timeout: 2.0\n });\n if (resp.success && resp.data) {\n this.pingResult = resp.data.responsive\n ? '<span class=\"text-success\"><i class=\"bi bi-check-circle-fill me-1\"></i>Runner is responsive</span>'\n : '<span class=\"text-warning\"><i class=\"bi bi-exclamation-triangle-fill me-1\"></i>Runner did not respond within 2s</span>';\n } else {\n this.pingResult = '<span class=\"text-danger\"><i class=\"bi bi-x-circle-fill me-1\"></i>Ping request failed</span>';\n }\n } catch (e) {\n this.pingResult = `<span class=\"text-danger\"><i class=\"bi bi-x-circle-fill me-1\"></i>${e.message}</span>`;\n }\n await this.render();\n }\n\n async onActionShutdown() {\n const ok = await Modal.confirm(\n `Send a graceful shutdown to <strong class=\"font-monospace\">${this.model.get('runner_id')}</strong>?`\n + '<br><br>The runner will finish its current job then exit. This is fire-and-forget.',\n 'Shutdown Runner',\n { confirmText: 'Shutdown', confirmClass: 'btn-danger' }\n );\n if (!ok) return;\n\n try {\n const resp = await this.getApp().rest.POST('/api/jobs/runners/shutdown', {\n runner_id: this.model.get('runner_id'),\n graceful: true\n });\n if (resp.success && resp.data && resp.data.status) {\n this.showSuccess('Shutdown command sent to runner.');\n this.emit('runner:shutdown', { runner: this.model });\n } else {\n this.showError((resp.data && resp.data.error) || 'Shutdown command failed.');\n }\n } catch (e) {\n this.showError('Shutdown failed: ' + e.message);\n }\n }\n\n async onActionBroadcast() {\n const commandEl = this.element && this.element.querySelector('[data-field=\"broadcast-command\"]');\n const timeoutEl = this.element && this.element.querySelector('[data-field=\"broadcast-timeout\"]');\n const command = commandEl ? commandEl.value : 'status';\n const timeout = timeoutEl ? (parseFloat(timeoutEl.value) || 2.0) : 2.0;\n\n Modal.showBusy({ message: `Broadcasting \"${command}\" to all runners…` });\n try {\n const resp = await this.getApp().rest.POST('/api/jobs/runners/broadcast', {\n command,\n timeout\n });\n Modal.hideBusy();\n\n if (resp.success && resp.data) {\n await Modal.code(\n JSON.stringify(resp.data, null, 2),\n 'json',\n { title: `Broadcast Response — ${command}`, size: 'lg' }\n );\n } else {\n this.showError((resp.data && resp.data.error) || 'Broadcast failed.');\n }\n } catch (e) {\n Modal.hideBusy();\n this.showError('Broadcast failed: ' + e.message);\n }\n }\n\n async onActionExport() {\n try {\n const exportData = {\n runner: this.model.toJSON ? this.model.toJSON() : this.model,\n exported_at: new Date().toISOString()\n };\n const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const a = Object.assign(document.createElement('a'), {\n href: url,\n download: `runner-${this.model.get('runner_id')}-${Date.now()}.json`\n });\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n this.showSuccess('Runner data exported.');\n } catch (e) {\n this.showError('Export failed: ' + e.message);\n }\n }\n\n async getTemplate() {\n return `\n <p class=\"text-muted small mb-3\">\n <i class=\"bi bi-info-circle me-1\"></i>\n Actions operate on runner <strong class=\"font-monospace\">{{model.runner_id}}</strong> unless otherwise noted.\n </p>\n\n <div class=\"d-flex align-items-center gap-2 mb-3\">\n <span class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;letter-spacing:0.09em;white-space:nowrap;\">Runner Control</span>\n <hr class=\"flex-grow-1 my-0\">\n </div>\n\n <div class=\"row g-3 mb-4\">\n\n <!-- Ping -->\n <div class=\"col-md-4\">\n <div class=\"card border-0 shadow-sm h-100\">\n <div class=\"card-body d-flex flex-column gap-3\">\n <div class=\"d-flex gap-3 align-items-start\">\n <div class=\"d-flex align-items-center justify-content-center rounded bg-success-subtle text-success flex-shrink-0\"\n style=\"width:40px;height:40px;font-size:1.1rem;\">\n <i class=\"bi bi-broadcast-pin\"></i>\n </div>\n <div>\n <div class=\"fw-semibold mb-1\">Ping Runner</div>\n <div class=\"text-muted small\">Verify this runner is truly responsive, not just alive on paper.</div>\n </div>\n </div>\n {{#pingResult|bool}}\n <div class=\"small\">{{{pingResult}}}</div>\n {{/pingResult|bool}}\n <button class=\"btn btn-sm btn-outline-success mt-auto\" data-action=\"ping\">\n <i class=\"bi bi-broadcast-pin me-1\"></i>Ping Now\n </button>\n </div>\n </div>\n </div>\n\n <!-- Shutdown -->\n <div class=\"col-md-4\">\n <div class=\"card border-0 shadow-sm h-100\">\n <div class=\"card-body d-flex flex-column gap-3\">\n <div class=\"d-flex gap-3 align-items-start\">\n <div class=\"d-flex align-items-center justify-content-center rounded bg-danger-subtle text-danger flex-shrink-0\"\n style=\"width:40px;height:40px;font-size:1.1rem;\">\n <i class=\"bi bi-power\"></i>\n </div>\n <div>\n <div class=\"fw-semibold mb-1\">Graceful Shutdown</div>\n <div class=\"text-muted small\">Runner finishes its current job then exits. Fire-and-forget.</div>\n </div>\n </div>\n <button class=\"btn btn-sm btn-outline-danger mt-auto\" data-action=\"shutdown\">\n <i class=\"bi bi-power me-1\"></i>Shutdown\n </button>\n </div>\n </div>\n </div>\n\n <!-- Export -->\n <div class=\"col-md-4\">\n <div class=\"card border-0 shadow-sm h-100\">\n <div class=\"card-body d-flex flex-column gap-3\">\n <div class=\"d-flex gap-3 align-items-start\">\n <div class=\"d-flex align-items-center justify-content-center rounded bg-secondary-subtle text-secondary flex-shrink-0\"\n style=\"width:40px;height:40px;font-size:1.1rem;\">\n <i class=\"bi bi-download\"></i>\n </div>\n <div>\n <div class=\"fw-semibold mb-1\">Export Snapshot</div>\n <div class=\"text-muted small\">Download runner identity data as a JSON file.</div>\n </div>\n </div>\n <button class=\"btn btn-sm btn-outline-secondary mt-auto\" data-action=\"export\">\n <i class=\"bi bi-download me-1\"></i>Export JSON\n </button>\n </div>\n </div>\n </div>\n\n </div>\n\n <div class=\"d-flex align-items-center gap-2 mb-3\">\n <span class=\"text-muted fw-semibold text-uppercase\" style=\"font-size:0.7rem;letter-spacing:0.09em;white-space:nowrap;\">Broadcast Command</span>\n <hr class=\"flex-grow-1 my-0\">\n </div>\n\n <div class=\"card border-0 shadow-sm\">\n <div class=\"card-body\">\n <p class=\"text-muted small mb-3\">\n Send a command to <strong>all active runners</strong> simultaneously and collect replies within the timeout window.\n </p>\n <div class=\"row g-2 align-items-end\">\n <div class=\"col-md-4\">\n <label class=\"form-label fw-semibold small text-muted mb-1\">Command</label>\n <select class=\"form-select form-select-sm\" data-field=\"broadcast-command\">\n <option value=\"status\">status</option>\n <option value=\"pause\">pause</option>\n <option value=\"resume\">resume</option>\n <option value=\"reload\">reload</option>\n <option value=\"shutdown\">shutdown</option>\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label fw-semibold small text-muted mb-1\">Timeout (s)</label>\n <input type=\"number\" class=\"form-control form-control-sm\"\n data-field=\"broadcast-timeout\" value=\"2.0\" min=\"0.5\" step=\"0.5\" />\n </div>\n <div class=\"col-md-5\">\n <button class=\"btn btn-primary btn-sm w-100\" data-action=\"broadcast\">\n <i class=\"bi bi-megaphone me-1\"></i>Broadcast to All Runners\n </button>\n </div>\n </div>\n </div>\n </div>\n `;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// RunnerDetailsView — shell (default export)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport default class RunnerDetailsView extends View {\n constructor(options = {}) {\n super({ className: 'runner-details-view', ...options });\n this.model = options.model instanceof JobRunner\n ? options.model\n : new JobRunner(options.model || options.data || {});\n }\n\n async onInit() {\n if (!this.model) return;\n\n const tabView = new TabView({\n containerId: 'runner-tabs',\n tabs: {\n 'Overview': new RunnerOverviewTab({ model: this.model }),\n 'System Info': new RunnerSysinfoTab({ model: this.model }),\n 'Running Jobs': new RunnerJobsTab({ model: this.model }),\n 'Logs': new RunnerLogsTab({ model: this.model }),\n 'Actions': new RunnerActionsTab({ model: this.model })\n }\n });\n\n this.addChild(tabView);\n }\n\n async getTemplate() {\n return `<div data-container=\"runner-tabs\"></div>`;\n }\n\n /**\n * Open this view in a Dialog.\n *\n * @param {object} runner — runner object from GET /api/jobs/runners\n * @param {object} options — extra options forwarded to Modal.dialog()\n * @returns {Promise<any>}\n */\n static async show(runner, options = {}) {\n const model = runner instanceof JobRunner ? runner : new JobRunner(runner);\n const view = new RunnerDetailsView({ model });\n\n return await Modal.dialog({\n title: `<i class=\"bi bi-cpu me-2\"></i><span class=\"font-monospace\">${model.get('runner_id')}</span>`,\n body: view,\n size: 'xl',\n scrollable: true,\n buttons: [\n { text: 'Close', class: 'btn-secondary', dismiss: true }\n ],\n ...options\n });\n }\n}\n\n\nJobRunner.VIEW_CLASS = RunnerDetailsView;\n","/**\n * JobRunnersSection - Runner heartbeat and status table\n *\n * Displays a card with a TableView of all registered job runners,\n * showing runner ID, alive/dead status badge, and relative heartbeat time.\n */\nimport View from '@core/View.js';\nimport TableView from '@core/views/table/TableView.js';\nimport { JobRunnerList } from '@ext/admin/models/JobRunner.js';\nimport RunnerDetailsView from '../RunnerDetailsView.js';\n\nexport default class JobRunnersSection extends View {\n constructor(options = {}) {\n super({\n className: 'job-runners-section',\n template: `\n <div class=\"card shadow-sm h-100\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\"><i class=\"bi bi-cpu me-2\"></i>Job Runners</h5>\n <small class=\"text-muted\">Heartbeat & status</small>\n </div>\n <div class=\"card-body p-0\" data-container=\"runner-table\"></div>\n </div>\n `,\n ...options\n });\n }\n\n async onInit() {\n this.runnersTable = new TableView({\n containerId: 'runner-table',\n Collection: JobRunnerList,\n searchable: true,\n filterable: false,\n paginated: true,\n itemView: RunnerDetailsView,\n viewDialogOptions: {\n title: 'Runner Details',\n size: 'xl',\n scrollable: true\n },\n tableOptions: {\n striped: false,\n hover: true,\n size: 'sm'\n },\n columns: [\n {\n key: 'runner_id',\n label: 'Runner',\n formatter: 'truncate_middle(16)'\n },\n {\n key: 'alive',\n label: 'Status',\n formatter: (value) => {\n const isAlive = value === true;\n const badgeClass = isAlive ? 'bg-success' : 'bg-danger';\n const icon = isAlive ? 'bi-check-circle-fill' : 'bi-x-octagon-fill';\n const text = isAlive ? 'ALIVE' : 'DEAD';\n return `<span class=\"badge ${badgeClass}\"><i class=\"${icon} me-1\"></i>${text}</span>`;\n }\n },\n {\n key: 'last_heartbeat',\n label: 'Heartbeat',\n formatter: (value) => {\n if (!value) return 'Never';\n const heartbeatTime = new Date(value);\n const now = new Date();\n const diffMs = now - heartbeatTime;\n const diffSeconds = Math.floor(diffMs / 1000);\n if (diffSeconds < 60) return `${diffSeconds}s ago`;\n if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`;\n return `${Math.floor(diffSeconds / 3600)}h ago`;\n }\n }\n ]\n });\n this.addChild(this.runnersTable);\n }\n}\n","/**\n * JobRunnersPage - Runners table with click-to-detail\n *\n * Wraps JobRunnersSection. Clicking a runner row opens\n * RunnerDetailsView in a dialog (5-tab inspector).\n *\n * Route: system/jobs/runners\n */\nimport Page from '@core/Page.js';\nimport JobRunnersSection from './sections/JobRunnersSection.js';\n\nexport default class JobRunnersPage extends Page {\n constructor(options = {}) {\n super({\n title: 'Job Runners',\n pageName: 'Job Runners',\n className: 'job-runners-page',\n ...options\n });\n\n this.template = `\n <div class=\"job-runners-container\">\n <p class=\"text-muted mb-3\">Registered job runners and their heartbeat status</p>\n <div data-container=\"runners-section\"></div>\n </div>\n `;\n }\n\n async onInit() {\n this.runnersSection = new JobRunnersSection({\n containerId: 'runners-section'\n });\n this.addChild(this.runnersSection);\n }\n}\n","/**\n * JobDetailsView - Rich job inspector modeled on GroupView.\n *\n * Header + left sidebar nav. Sections: Details, Payload, Events, Logs,\n * Similar Jobs (same `func`). Special \"scheduled\" treatment when the job\n * is pending with a future `run_at` — promoted to its own visual state\n * (orange badge + clock icon, scheduled-for relative time in the header).\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Job, JobList, JobEventList, JobLogList, JobForms } from '@ext/admin/models/Job.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass JobDetailsView extends View {\n constructor(options = {}) {\n super({\n className: 'job-details-view',\n ...options\n });\n\n this.model = options.model || new Job(options.data || {});\n\n this.template = `\n <div class=\"job-details-container\">\n <div data-container=\"job-header\"></div>\n <div data-container=\"job-sidenav\" style=\"min-height: 400px;\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // ── Header ────────────────────────────────────\n this.header = new View({\n containerId: 'job-header',\n template: `\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"d-flex align-items-center justify-content-center rounded {{model.statusBgSubtle}}\" style=\"width: 64px; height: 64px;\">\n <i class=\"bi {{model.statusIcon}} {{model.statusTextClass}}\" style=\"font-size: 1.75rem;\"></i>\n </div>\n <div>\n <h3 class=\"mb-0 font-monospace\" style=\"font-size:1.1rem;\">{{model.func|default('unknown.task')}}</h3>\n <div class=\"d-flex flex-wrap align-items-center gap-2 mt-1\">\n <span class=\"badge bg-primary bg-opacity-10 text-primary\" style=\"font-size:0.72rem;\">{{model.channel}}</span>\n <span class=\"text-muted small font-monospace\">{{model.id|truncate(16)}}</span>\n {{#model.runner_id}}\n <span class=\"text-muted small\">\n <i class=\"bi bi-cpu me-1\"></i>{{model.runner_id|truncate(20)}}\n </span>\n {{/model.runner_id}}\n </div>\n <div class=\"text-muted small mt-1\">\n {{#model.isScheduled|bool}}\n <i class=\"bi bi-clock-fill text-warning me-1\"></i>\n Scheduled for {{model.runAtAbs}} <span class=\"text-warning\">({{model.runAtRel}})</span>\n {{/model.isScheduled|bool}}\n {{^model.isScheduled|bool}}\n {{#model.started_at}}<i class=\"bi bi-play-fill me-1\"></i>Started {{model.started_at|relative}} · {{/model.started_at}}\n <i class=\"bi bi-calendar-plus me-1\"></i>Created {{model.created|relative}}\n {{/model.isScheduled|bool}}\n </div>\n </div>\n </div>\n <div class=\"d-flex align-items-start gap-3\">\n <div class=\"text-end\">\n <span class=\"badge {{model.statusBadgeClass}} fs-6\">\n <i class=\"bi {{model.statusIcon}}\"></i> {{model.statusLabel}}\n </span>\n {{#model.cancel_requested|bool}}\n <div class=\"mt-1\">\n <span class=\"badge bg-warning bg-opacity-25 text-warning\">\n <i class=\"bi bi-exclamation-triangle\"></i> Cancel requested\n </span>\n </div>\n {{/model.cancel_requested|bool}}\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 this.header.setModel(this.model);\n this.addChild(this.header);\n\n // ── Details section ──────────────────────────\n const detailsView = new View({\n model: this.model,\n template: `\n <style>\n .jv-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--bs-secondary-color); margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .jv-section-label:first-child { margin-top: 0; }\n .jv-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid var(--bs-border-color-translucent); }\n .jv-field-row:last-child { border-bottom: none; }\n .jv-field-label { width: 160px; font-size: 0.78rem; color: var(--bs-secondary-color); flex-shrink: 0; }\n .jv-field-value { flex: 1; font-size: 0.88rem; color: var(--bs-body-color); word-break: break-all; }\n .jv-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82rem; }\n .jv-error { color: var(--bs-danger); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82rem; white-space: pre-wrap; }\n </style>\n\n <div class=\"jv-section-label\">Identity</div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Job ID</div>\n <div class=\"jv-field-value jv-mono\">{{model.id}}</div>\n </div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Function</div>\n <div class=\"jv-field-value jv-mono\">{{model.func|default('—')}}</div>\n </div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Channel</div>\n <div class=\"jv-field-value\"><span class=\"badge bg-primary bg-opacity-10 text-primary\">{{model.channel}}</span></div>\n </div>\n {{#model.runner_id}}\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Runner</div>\n <div class=\"jv-field-value jv-mono\">{{model.runner_id}}</div>\n </div>\n {{/model.runner_id}}\n {{#model.broadcast|bool}}\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Broadcast</div>\n <div class=\"jv-field-value\"><span class=\"badge bg-info bg-opacity-25 text-info\"><i class=\"bi bi-broadcast me-1\"></i>All runners</span></div>\n </div>\n {{/model.broadcast|bool}}\n\n <div class=\"jv-section-label\">Status</div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">State</div>\n <div class=\"jv-field-value\">\n <span class=\"badge {{model.statusBadgeClass}}\"><i class=\"bi {{model.statusIcon}} me-1\"></i>{{model.statusLabel}}</span>\n {{#model.cancel_requested|bool}}<span class=\"badge bg-warning bg-opacity-25 text-warning ms-2\"><i class=\"bi bi-exclamation-triangle me-1\"></i>Cancel requested</span>{{/model.cancel_requested|bool}}\n </div>\n </div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Attempt</div>\n <div class=\"jv-field-value\">{{model.attempt|default(0)}} of {{model.max_retries|default(0)}}</div>\n </div>\n {{#model.last_error}}\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Last error</div>\n <div class=\"jv-field-value jv-error\">{{model.last_error}}</div>\n </div>\n {{/model.last_error}}\n\n {{#model.isScheduled|bool}}\n <div class=\"jv-section-label\">Schedule</div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Run at</div>\n <div class=\"jv-field-value\">\n {{model.runAtAbs}}\n <span class=\"text-warning ms-2\"><i class=\"bi bi-clock-fill\"></i> {{model.runAtRel}}</span>\n </div>\n </div>\n {{/model.isScheduled|bool}}\n\n <div class=\"jv-section-label\">Timeline</div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Created</div>\n <div class=\"jv-field-value\">{{model.created|datetime|default('—')}}</div>\n </div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Modified</div>\n <div class=\"jv-field-value\">{{model.modified|datetime|default('—')}}</div>\n </div>\n {{#model.started_at}}\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Started</div>\n <div class=\"jv-field-value\">{{model.started_at|datetime}}</div>\n </div>\n {{/model.started_at}}\n {{#model.finished_at}}\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Finished</div>\n <div class=\"jv-field-value\">{{model.finished_at|datetime}}</div>\n </div>\n {{/model.finished_at}}\n {{#model.duration_ms}}\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Duration</div>\n <div class=\"jv-field-value\">{{model.formattedDuration}}</div>\n </div>\n {{/model.duration_ms}}\n {{#model.expires_at}}\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Expires at</div>\n <div class=\"jv-field-value\">{{model.expires_at|datetime}}</div>\n </div>\n {{/model.expires_at}}\n\n <div class=\"jv-section-label\">Limits</div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Max retries</div>\n <div class=\"jv-field-value\">{{model.max_retries|default(0)}}</div>\n </div>\n <div class=\"jv-field-row\">\n <div class=\"jv-field-label\">Max execution</div>\n <div class=\"jv-field-value\">{{model.max_exec_seconds|default('—')}}{{#model.max_exec_seconds}}s{{/model.max_exec_seconds}}</div>\n </div>\n `\n });\n\n // ── Payload section ──────────────────────────\n const payloadView = new View({\n model: this.model,\n template: `\n <div class=\"job-payload-tab\">\n <pre class=\"bg-body-tertiary p-3 rounded border\" style=\"max-height: 60vh; overflow:auto;\"><code>{{{model.payload|json}}}</code></pre>\n </div>\n `\n });\n\n // ── Events ────────────────────────────────────\n const eventsView = new TableView({\n collection: new JobEventList({\n params: { job: this.model.get('id'), size: 25, sort: '-at' }\n }),\n hideActivePillNames: ['job'],\n columns: [\n { key: 'at', label: 'Timestamp', formatter: 'datetime', sortable: true, width: '180px' },\n { key: 'event', label: 'Event', formatter: 'badge' },\n { key: 'runner_id', label: 'Runner', formatter: (v) => v ? `<span class=\"font-monospace small\">${v}</span>` : '<span class=\"text-muted\">—</span>' },\n { key: 'attempt', label: 'Attempt' },\n { key: 'details|json', label: 'Details' }\n ]\n });\n\n // ── Logs ──────────────────────────────────────\n const logsView = new TableView({\n collection: new JobLogList({\n params: { job_id: this.model.get('id'), size: 25, sort: '-created' }\n }),\n hideActivePillNames: ['job_id'],\n columns: [\n { key: 'created', label: 'Timestamp', formatter: 'datetime', sortable: true, width: '180px' },\n { key: 'kind', label: 'Kind', formatter: 'badge', width: '100px' },\n { key: 'message', label: 'Message' }\n ]\n });\n\n // ── Similar Jobs (same func) ─────────────────\n // Only render when `func` is known — otherwise the API filter\n // would return everything.\n let similarView = null;\n const func = this.model.get('func');\n if (func) {\n similarView = new TableView({\n collection: new JobList({\n params: { func, size: 15, sort: '-created' }\n }),\n hideActivePillNames: ['func'],\n clickAction: 'view',\n itemView: JobDetailsView,\n viewDialogOptions: {\n title: 'Job Details',\n size: 'xl',\n scrollable: true\n },\n columns: [\n {\n key: 'id',\n label: 'Job',\n template: `\n <div class=\"fw-semibold font-monospace small\">{{model.id|truncate_middle(16)}}</div>\n <div class=\"text-muted small\">{{model.channel}}</div>\n `\n },\n {\n key: 'status',\n label: 'Status',\n formatter: (value, context) => {\n const job = context.row;\n const badgeClass = job.getStatusBadgeClass ? job.getStatusBadgeClass() : 'bg-secondary';\n const icon = job.getStatusIcon ? job.getStatusIcon() : 'bi-question';\n return `<span class=\"badge ${badgeClass}\"><i class=\"${icon} me-1\"></i>${value?.toUpperCase() || 'UNKNOWN'}</span>`;\n }\n },\n { key: 'created', label: 'Created', formatter: 'relative', sortable: true },\n { key: 'duration_ms', label: 'Duration', formatter: 'duration' }\n ]\n });\n }\n\n // ── SideNavView ───────────────────────────────\n const sections = [\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView },\n { key: 'payload', label: 'Payload', icon: 'bi-braces', view: payloadView },\n { type: 'divider', label: 'Activity' },\n { key: 'events', label: 'Events', icon: 'bi-calendar-event', view: eventsView },\n { key: 'logs', label: 'Logs', icon: 'bi-journal-text', view: logsView }\n ];\n if (similarView) {\n sections.push({ type: 'divider', label: 'Related' });\n sections.push({ key: 'similar', label: 'Similar Jobs', icon: 'bi-files', view: similarView });\n }\n this.sideNavView = new SideNavView({\n containerId: 'job-sidenav',\n activeSection: 'details',\n navWidth: 180,\n contentPadding: '1.25rem 1.5rem',\n enableResponsive: true,\n minWidth: 600,\n sections\n });\n this.addChild(this.sideNavView);\n\n // ── Context menu ──────────────────────────────\n const menuItems = [\n { label: 'Refresh', action: 'refresh-job', icon: 'bi-arrow-clockwise' }\n ];\n if (this.model.canCancel?.()) {\n menuItems.push({ type: 'divider' });\n menuItems.push({ label: 'Cancel Job', action: 'cancel-job', icon: 'bi-x-circle', class: 'text-danger' });\n }\n if (this.model.canRetry?.()) {\n menuItems.push({ type: 'divider' });\n menuItems.push({ label: 'Retry Job', action: 'retry-job', icon: 'bi-arrow-repeat', class: 'text-primary' });\n }\n const jobMenu = new ContextMenu({\n containerId: 'job-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: menuItems\n }\n });\n this.addChild(jobMenu);\n\n // Pull the full record on open (graph=detail returns everything\n // including last_error, metadata, duration_ms).\n await this.model.fetch({ params: { graph: 'detail' } });\n }\n\n async onBeforeRender() {\n this._prepareViewData();\n }\n\n /**\n * Stash view-only computed fields on `model._` so templates can read\n * them. Called before each render so the labels stay current.\n */\n _prepareViewData() {\n if (!this.model) return;\n const m = this.model;\n\n const isScheduled = m.isScheduled?.() === true;\n m._.isScheduled = isScheduled;\n\n // Status badge / label / icon. Promote scheduled to its own state\n // (orange + calendar icon) so the user can see at a glance that\n // this job isn't actively waiting in the queue.\n if (isScheduled) {\n m._.statusLabel = 'SCHEDULED';\n m._.statusBadgeClass = 'bg-warning';\n m._.statusIcon = 'bi-clock-fill';\n m._.statusBgSubtle = 'bg-warning bg-opacity-25';\n m._.statusTextClass = 'text-warning';\n } else {\n const status = m.get('status') || 'unknown';\n m._.statusLabel = status.toUpperCase();\n m._.statusBadgeClass = m.getStatusBadgeClass?.() || 'bg-secondary';\n m._.statusIcon = m.getStatusIcon?.() || 'bi-question-circle';\n m._.statusBgSubtle = `${m._.statusBadgeClass.replace('bg-', 'bg-')} bg-opacity-25`;\n m._.statusTextClass = m._.statusBadgeClass.replace('bg-', 'text-');\n }\n\n m._.formattedDuration = m.getFormattedDuration?.() || '';\n\n // Absolute and relative `run_at` for the scheduled-state header.\n const runAt = m.get('run_at');\n if (runAt) {\n const ms = (typeof runAt === 'number' && runAt < 1e11) ? runAt * 1000 : new Date(runAt).getTime();\n const d = new Date(ms);\n if (Number.isFinite(d.getTime())) {\n m._.runAtAbs = d.toLocaleString();\n m._.runAtRel = this._relativeFuture(ms);\n }\n }\n }\n\n _relativeFuture(timestampMs) {\n const diffSec = Math.round((timestampMs - Date.now()) / 1000);\n if (diffSec <= 0) {\n const past = -diffSec;\n if (past < 60) return `${past}s ago`;\n if (past < 3600) return `${Math.floor(past / 60)}m ago`;\n if (past < 86400) return `${Math.floor(past / 3600)}h ago`;\n return `${Math.floor(past / 86400)}d ago`;\n }\n if (diffSec < 60) return `in ${diffSec}s`;\n if (diffSec < 3600) return `in ${Math.floor(diffSec / 60)}m`;\n if (diffSec < 86400) return `in ${Math.floor(diffSec / 3600)}h`;\n return `in ${Math.floor(diffSec / 86400)}d`;\n }\n\n // ── Actions ───────────────────────────────────\n\n async onActionRefreshJob() {\n await this.model.fetch({ params: { graph: 'detail' } });\n await this.render();\n }\n\n async onActionCancelJob() {\n const ok = await Modal.confirm(\n `Cancel job <code>${this.model.get('id')}</code>?`,\n 'Cancel Job'\n );\n if (!ok) return true;\n\n try {\n const resp = await this.model.cancel();\n if (resp.success) {\n this.getApp()?.toast?.success('Cancellation requested');\n await this.model.fetch({ params: { graph: 'detail' } });\n await this.render();\n this.emit('job-cancelled', { job: this.model });\n } else {\n this.getApp()?.toast?.error(resp.data?.error || 'Failed to cancel job');\n }\n } catch (err) {\n console.error('[JobDetailsView] cancel failed:', err);\n this.getApp()?.toast?.error(err.message || 'Failed to cancel job');\n }\n return true;\n }\n\n async onActionRetryJob() {\n const retryData = await Modal.form({\n title: 'Retry Job',\n formConfig: JobForms?.retry\n });\n if (!retryData) return true;\n\n try {\n const resp = await this.model.retry(retryData.delay || 0);\n if (resp.success) {\n this.getApp()?.toast?.success('Retry scheduled');\n this.emit('job-retried', { job: this.model, newJobId: resp.newJobId });\n } else {\n this.getApp()?.toast?.error(resp.data?.error || 'Failed to retry job');\n }\n } catch (err) {\n console.error('[JobDetailsView] retry failed:', err);\n this.getApp()?.toast?.error(err.message || 'Failed to retry job');\n }\n return true;\n }\n\n // Prevent full re-render on model.set() inside our refresh paths;\n // we render manually after fetches complete.\n _onModelChange() {}\n}\n\nJob.VIEW_CLASS = JobDetailsView;\n\nexport default JobDetailsView;\n","/**\n * JobTableSection - Reusable filtered job table section\n *\n * Wraps a TableView for a specific job status filter. Accepts status,\n * sort, extraParams, columns, and batch action configuration via options.\n */\nimport View from '@core/View.js';\nimport TableView from '@core/views/table/TableView.js';\nimport { JobList } from '@ext/admin/models/Job.js';\nimport JobDetailsView from '../JobDetailsView.js';\n\n// ── Default column configs per status ──────────────────────\n\nconst COLUMNS = {\n running: [\n {\n key: 'id',\n label: 'Job',\n template: `\n <div class=\"fw-semibold font-monospace\">{{model.id|truncate_middle(12)}}</div>\n <div class=\"text-muted small\">{{model.channel}} · {{model.func|truncate_middle(28)|default('n/a')}}</div>\n `\n },\n {\n key: 'runner_id',\n label: 'Runner',\n template: `\n <span class=\"font-monospace\">{{model.runner_id|truncate_middle(12)|default('n/a')}}</span>\n `\n },\n {\n key: 'status',\n label: 'State',\n formatter: (value, context) => {\n const job = context.row;\n const badgeClass = job.getStatusBadgeClass ? job.getStatusBadgeClass() : 'bg-secondary';\n const icon = job.getStatusIcon ? job.getStatusIcon() : 'bi-question';\n return `<span class=\"badge ${badgeClass}\"><i class=\"${icon} me-1\"></i>${value.toUpperCase()}</span>`;\n }\n },\n {\n key: 'created',\n label: 'Started',\n formatter: 'datetime'\n }\n ],\n\n pending: [\n {\n key: 'id',\n label: 'Job',\n template: `\n <div class=\"fw-semibold font-monospace\">{{model.id|truncate_middle(12)}}</div>\n <div class=\"text-muted small\">{{model.channel}} · {{model.func|truncate_middle(28)|default('n/a')}}</div>\n `\n },\n {\n key: 'priority',\n label: 'Priority',\n formatter: (value = 0) => {\n const badge = value >= 8 ? 'bg-danger' : value >= 5 ? 'bg-warning' : 'bg-secondary';\n return `<span class=\"badge ${badge}\">${value}</span>`;\n }\n },\n {\n key: 'modified',\n label: 'Queued',\n formatter: 'relative'\n }\n ],\n\n scheduled: [\n {\n key: 'id',\n label: 'Job',\n formatter: 'truncate_middle(12)'\n },\n {\n key: 'run_at',\n label: 'Scheduled For',\n formatter: 'datetime'\n },\n {\n key: 'channel',\n label: 'Channel',\n formatter: 'badge'\n }\n ],\n\n failed: [\n {\n key: 'id',\n label: 'Job',\n template: `\n <div class=\"fw-semibold font-monospace\">{{model.id|truncate_middle(12)}}</div>\n <div class=\"text-muted small\">{{model.channel}} · {{model.func|truncate_middle(28)|default('n/a')}}</div>\n `\n },\n {\n key: 'last_error',\n label: 'Error',\n template: `\n <div class=\"text-danger small\">{{model.last_error|truncate(80)|default('Unknown error')}}</div>\n `\n },\n {\n key: 'modified',\n label: 'Failed',\n formatter: 'relative'\n }\n ],\n\n all: [\n {\n key: 'id',\n label: 'Job',\n template: `\n <div class=\"fw-semibold font-monospace\">{{model.id}}</div>\n <div class=\"text-muted small\">{{model.func|default('Unknown')}}</div>\n `\n },\n {\n key: 'channel',\n label: 'Channel',\n formatter: 'badge'\n },\n {\n key: 'status',\n label: 'Status',\n formatter: (value, context) => {\n const job = context.row;\n const badgeClass = job.getStatusBadgeClass ? job.getStatusBadgeClass() : 'bg-secondary';\n const icon = job.getStatusIcon ? job.getStatusIcon() : 'bi-question';\n return `<span class=\"badge ${badgeClass}\"><i class=\"${icon} me-1\"></i>${value?.toUpperCase() || 'UNKNOWN'}</span>`;\n }\n },\n {\n key: 'run_at',\n label: 'Scheduled',\n formatter: (value) => {\n if (!value) return '<span class=\"text-muted small\">—</span>';\n const ms = (typeof value === 'number' && value < 1e11) ? value * 1000 : value;\n const d = new Date(ms);\n if (isNaN(d.getTime())) return '<span class=\"text-muted small\">—</span>';\n const future = d.getTime() > Date.now();\n const cls = future ? 'text-warning' : 'text-muted';\n const icon = future ? 'bi-clock-fill' : 'bi-clock-history';\n return `<span class=\"${cls} small\"><i class=\"${icon} me-1\"></i>${d.toLocaleString()}</span>`;\n }\n },\n {\n key: 'created',\n label: 'Created',\n formatter: 'datetime'\n },\n {\n key: 'finished_at',\n label: 'Finished',\n formatter: 'datetime'\n },\n {\n key: 'duration_ms',\n label: 'Duration',\n formatter: 'duration'\n }\n ]\n};\n\nconst ALL_FILTERS = [\n {\n key: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Pending', value: 'pending' },\n { label: 'Running', value: 'running' },\n { label: 'Completed', value: 'completed' },\n { label: 'Failed', value: 'failed' },\n { label: 'Canceled', value: 'canceled' },\n { label: 'Expired', value: 'expired' }\n ]\n },\n {\n key: 'channel',\n label: 'Channel',\n type: 'text'\n },\n {\n key: 'func__icontains',\n label: 'Function',\n type: 'text'\n }\n];\n\nconst CANCEL_BATCH_ACTIONS = [\n {\n icon: 'bi-x-circle-fill',\n label: 'Cancel Jobs',\n action: 'cancel-jobs'\n }\n];\n\n// ── Section view ───────────────────────────────────────────\n\nexport default class JobTableSection extends View {\n constructor(options = {}) {\n const {\n status,\n sort = '-created',\n extraParams = {},\n columns,\n title,\n selectable = false,\n batchActions,\n ...viewOptions\n } = options;\n\n super({\n className: 'job-table-section',\n template: `<div data-container=\"job-table\"></div>`,\n ...viewOptions\n });\n\n this.status = status;\n this.sort = sort;\n this.extraParams = extraParams;\n this.columnConfig = columns;\n this.sectionTitle = title;\n this.selectable = selectable;\n this.batchActionConfig = batchActions;\n }\n\n async onInit() {\n // Build collection params\n const collectionParams = {\n size: 25,\n sort: this.sort,\n ...this.extraParams\n };\n if (this.status) {\n collectionParams.status = this.status;\n }\n\n // Resolve columns — columnConfig can be a string key (e.g. 'scheduled') or an array\n const columns = (typeof this.columnConfig === 'string'\n ? COLUMNS[this.columnConfig]\n : this.columnConfig) || COLUMNS[this.status] || COLUMNS.all;\n\n // Resolve selectable / batch actions\n const selectable = this.selectable;\n const batchActions = this.batchActionConfig\n || (selectable ? CANCEL_BATCH_ACTIONS : undefined);\n\n // Determine filterable based on status\n const isAllView = !this.status;\n\n const tableConfig = {\n containerId: 'job-table',\n Collection: JobList,\n collectionParams,\n columns,\n searchable: true,\n filterable: isAllView,\n paginated: true,\n itemView: JobDetailsView,\n hideActivePills: this.status ? ['status'] : [],\n viewDialogOptions: {\n title: 'Job Details',\n size: 'xl',\n scrollable: true\n },\n tableOptions: {\n striped: false,\n hover: true,\n size: 'sm'\n }\n };\n\n if (selectable) {\n tableConfig.selectable = true;\n tableConfig.batchBarLocation = 'top';\n tableConfig.batchActions = batchActions;\n }\n\n if (isAllView) {\n tableConfig.filters = ALL_FILTERS;\n tableConfig.tableOptions.striped = true;\n tableConfig.tableOptions.responsive = true;\n }\n\n this.tableView = new TableView(tableConfig);\n\n // Wire up batch cancel action\n if (selectable) {\n this.tableView.on(\"action:batch-cancel-jobs\", async (action, event, element) => {\n const items = this.tableView.getSelectedItems();\n await Promise.all(items.map(item => item.model.cancel()));\n this.getApp().toast.success(\"Jobs cancelled successfully\");\n this.tableView.collection.fetch();\n });\n }\n\n this.addChild(this.tableView);\n }\n}\n","/**\n * JobsTablePage - All jobs, filterable by status\n *\n * Wraps JobTableSection with no status filter (shows all jobs)\n * and full column config, search, and filter support.\n *\n * Route: system/jobs/list\n */\nimport Page from '@core/Page.js';\nimport JobTableSection from './sections/JobTableSection.js';\n\nexport default class JobsTablePage extends Page {\n constructor(options = {}) {\n super({\n title: 'Jobs',\n pageName: 'Jobs',\n className: 'jobs-table-page',\n ...options\n });\n\n this.template = `\n <div class=\"jobs-table-container\">\n <p class=\"text-muted mb-3\">All jobs across all channels and statuses</p>\n <div data-container=\"jobs-section\"></div>\n </div>\n `;\n }\n\n async onInit() {\n this.jobTableSection = new JobTableSection({\n containerId: 'jobs-section',\n sort: '-created',\n title: 'All Jobs',\n // Enable per-row checkboxes + the top batch-action bar with\n // \"Cancel Jobs\" wired up. JobTableSection's onInit handles the\n // batch-cancel-jobs action by calling model.cancel() on each\n // selected row in parallel.\n selectable: true\n });\n this.addChild(this.jobTableSection);\n }\n}\n","/**\n * BlockedIPsTablePage - Active IP blocks management\n *\n * Filtered view of GeoLocatedIP records where is_blocked=true.\n * Provides unblock and whitelist actions from the table.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { GeoLocatedIPList } from '@core/models/System.js';\nimport GeoIPView from '../account/devices/GeoIPView.js';\n\nexport default class BlockedIPsTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_blocked_ips',\n pageName: 'Blocked IPs',\n router: 'admin/security/blocked-ips',\n Collection: GeoLocatedIPList,\n itemViewClass: GeoIPView,\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: '-modified',\n is_blocked: true,\n },\n\n columns: [\n {\n key: 'ip_address',\n label: 'IP Address',\n sortable: true,\n template: '<code>{{model.ip_address}}</code>'\n },\n {\n key: 'threat_level',\n label: 'Threat Level',\n sortable: true,\n filter: {\n type: 'select',\n options: ['none', 'low', 'medium', 'high', 'critical']\n }\n },\n {\n key: 'country_code',\n label: 'Country',\n sortable: true,\n filter: { type: 'text' }\n },\n {\n key: 'city',\n label: 'City',\n sortable: true\n },\n {\n key: 'blocked_reason',\n label: 'Reason',\n formatter: 'truncate(40)'\n },\n {\n key: 'blocked_at',\n label: 'Blocked At',\n formatter: 'datetime',\n sortable: true\n },\n {\n key: 'blocked_until',\n label: 'Expires',\n formatter: 'datetime',\n sortable: true\n }\n ],\n\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n selectable: true,\n\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n emptyMessage: 'No blocked IPs. The firewall has no active IP blocks.',\n\n batchBarLocation: 'top',\n batchActions: [\n { label: 'Unblock', icon: 'bi bi-unlock', action: 'unblock' },\n { label: 'Whitelist', icon: 'bi bi-check-circle', action: 'whitelist' },\n ],\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionBatchUnblock() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const confirmed = await this.getApp().confirm(\n `Unblock ${selected.length} IP${selected.length > 1 ? 's' : ''}?`\n );\n if (!confirmed) return;\n\n await Promise.all(selected.map(item =>\n item.model.save({ unblock: 'Bulk unblock from admin' })\n ));\n this.getApp().toast.success(`${selected.length} IP(s) unblocked`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchWhitelist() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const confirmed = await this.getApp().confirm(\n `Whitelist ${selected.length} IP${selected.length > 1 ? 's' : ''}?`\n );\n if (!confirmed) return;\n\n await Promise.all(selected.map(item =>\n item.model.save({ whitelist: 'Bulk whitelist from admin' })\n ));\n this.getApp().toast.success(`${selected.length} IP(s) whitelisted`);\n this.tableView.collection.fetch();\n }\n}\n","/**\n * LogView - Detailed view for a Log record\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Log } from '@core/models/Log.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport DeviceView from '../account/devices/DeviceView.js';\nimport GeoIPView from '../account/devices/GeoIPView.js';\n\nclass LogView extends View {\n constructor(options = {}) {\n super({\n className: 'log-view',\n ...options\n });\n\n this.model = options.model || new Log(options.data || {});\n this.logIcon = this.getIconForLog(this.model.get('level'));\n\n this.template = `\n <div class=\"log-view-container\">\n <!-- Header -->\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 <!-- Tabs -->\n <div data-container=\"log-tabs\"></div>\n </div>\n `;\n }\n\n getIconForLog(level) {\n const lvl = level?.toLowerCase();\n if (lvl === 'error' || lvl === 'critical') return { icon: 'bi-x-octagon-fill', color: 'text-danger' };\n if (lvl === 'warning') return { icon: 'bi-exclamation-triangle-fill', color: 'text-warning' };\n if (lvl === 'info') return { icon: 'bi-info-circle-fill', color: 'text-info' };\n return { icon: 'bi-journal-text', color: 'text-secondary' };\n }\n\n async onInit() {\n // Overview Tab\n this.overviewView = new DataView({\n model: this.model,\n className: \"p-3\",\n columns: 2,\n fields: [\n { name: 'id', label: 'Log ID' },\n { name: 'level', label: 'Level', format: 'badge' },\n { name: 'kind', label: 'Kind' },\n { name: 'ip', label: 'IP Address', template: '<a href=\"#\" data-action=\"view-ip\">{{model.ip}}</a>' },\n { name: 'uid', label: 'User ID' },\n { name: 'username', label: 'Username' },\n { name: 'duid', label: 'Device ID', template: '<a href=\"#\" data-action=\"view-device\">{{model.duid|truncate_middle(32)}}</a>' },\n { name: 'model_name', label: 'Related Model' },\n { name: 'model_id', label: 'Related Model ID' },\n { name: 'user_agent', label: 'User Agent', columns: 12 },\n ]\n });\n\n // Log Content Tab\n const logContent = this.model.get('log');\n let formattedLog = logContent;\n try {\n // Attempt to parse and prettify if it's a JSON string\n const parsed = JSON.parse(logContent);\n formattedLog = JSON.stringify(parsed, null, 2);\n } catch (e) {\n // Not a valid JSON string, display as is\n }\n\n this.logContentView = new View({\n 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>${formattedLog}</code></pre>\n </div>\n `,\n onActionCopyLog: () => {\n navigator.clipboard.writeText(formattedLog);\n this.getApp()?.toast?.success('Log content copied to clipboard.');\n }\n });\n\n // TabView\n this.tabView = new TabView({\n containerId: 'log-tabs',\n tabs: {\n 'Log': this.logContentView,\n 'Details': this.overviewView,\n\n },\n activeTab: 'Log'\n });\n this.addChild(this.tabView);\n\n // ContextMenu\n const logMenu = new ContextMenu({\n containerId: 'log-context-menu',\n className: \"context-menu-view header-menu-absolute\",\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'View User', action: 'view-user', icon: 'bi-person', disabled: !this.model.get('uid') },\n { label: 'View Device', action: 'view-device', icon: 'bi-phone', disabled: !this.model.get('duid') },\n { type: 'divider' },\n { label: 'Delete Log', action: 'delete-log', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(logMenu);\n }\n\n async onActionViewIp(event) {\n event.preventDefault();\n const ip = this.model.get('ip');\n if (ip) {\n GeoIPView.show(ip);\n }\n }\n\n async onActionViewDevice(event) {\n event.preventDefault();\n const duid = this.model.get('duid');\n if (duid) {\n DeviceView.show(duid);\n }\n }\n\n async onActionViewUser() {\n console.log(\"TODO: View user\", this.model.get('uid'));\n }\n\n async onActionDeleteLog() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete this log entry? This action cannot be undone.`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n if (confirmed) {\n const resp = await this.model.destroy();\n if (resp.success) {\n this.emit('log:deleted', { model: this.model });\n }\n }\n }\n}\n\nLog.VIEW_CLASS = LogView;\n\nexport default LogView;\n","/**\n * FirewallLogTablePage - Firewall activity log\n *\n * Shows log entries filtered to kind__startswith=firewall: so admins\n * can see block/unblock/whitelist history.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { LogList } from '@core/models/Log.js';\nimport LogView from '../monitoring/LogView.js';\n\nexport default class FirewallLogTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_firewall_log',\n pageName: 'Firewall Log',\n router: 'admin/security/firewall-log',\n Collection: LogList,\n itemViewClass: LogView,\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: '-created',\n kind__startswith: 'firewall:',\n },\n\n columns: [\n {\n key: 'created',\n label: 'Timestamp',\n formatter: 'datetime',\n sortable: true,\n filter: { type: 'daterange' }\n },\n {\n key: 'kind',\n label: 'Action',\n sortable: true,\n filter: { type: 'text' }\n },\n {\n key: 'message',\n label: 'Details',\n formatter: 'truncate(80)'\n },\n {\n key: 'path',\n label: 'IP / Path',\n formatter: 'truncate(40)'\n },\n {\n key: 'user',\n label: 'Admin',\n sortable: true\n }\n ],\n\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n emptyMessage: 'No firewall log entries found.',\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n","/**\n * BouncerSignalView - Signal assessment detail view\n *\n * Shows full signal payload including raw_signals, server_signals,\n * linked device and GeoIP information for a bouncer assessment.\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\n\nexport default class BouncerSignalView extends View {\n constructor(options = {}) {\n super({\n className: 'bouncer-signal-view',\n ...options\n });\n\n this.template = `\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\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: 72px; height: 72px;\">\n <i class=\"bi bi-activity text-secondary\" style=\"font-size: 36px;\"></i>\n </div>\n <div>\n <h3 class=\"mb-1\">Signal Assessment</h3>\n <div class=\"text-muted small\">\n <span>IP: <code>{{model.ip_address}}</code></span>\n <span class=\"mx-2\">|</span>\n <span>Stage: {{model.stage}}</span>\n <span class=\"mx-2\">|</span>\n <span>Page: {{model.page_type|default('—')}}</span>\n </div>\n <div class=\"mt-1\">\n <span class=\"badge {{decisionBadge}} me-1\">{{model.decision|uppercase}}</span>\n <span class=\"text-muted small\">Risk Score: <strong>{{model.risk_score}}</strong></span>\n </div>\n </div>\n </div>\n <div data-container=\"signal-context-menu\"></div>\n </div>\n <div data-container=\"signal-tabs\"></div>\n `;\n }\n\n get decisionBadge() {\n const classes = { allow: 'bg-success', monitor: 'bg-warning', block: 'bg-danger' };\n return classes[this.model?.get('decision')] || 'bg-secondary';\n }\n\n async onInit() {\n await this.model.fetch({ params: { graph: 'detail' } });\n\n const overviewView = new View({\n model: this.model,\n template: `\n <div class=\"row\">\n <div class=\"col-md-6\">\n <div class=\"card border-0 bg-light mb-3\">\n <div class=\"card-body\">\n <h6 class=\"fw-bold mb-3\">Assessment</h6>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Decision</label><div><span class=\"badge {{decisionBadge}}\">{{model.decision|uppercase}}</span></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Risk Score</label><div>{{model.risk_score}}</div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Stage</label><div>{{model.stage}}</div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Page Type</label><div>{{model.page_type|default('—')}}</div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">IP Address</label><div><code>{{model.ip_address}}</code></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">MUID</label><div><code>{{model.muid|default('—')}}</code></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Created</label><div>{{model.created|datetime}}</div></div>\n </div>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"card border-0 bg-light mb-3\">\n <div class=\"card-body\">\n <h6 class=\"fw-bold mb-3\">Triggered Signals</h6>\n {{#model.triggered_signals.length}}\n <div class=\"d-flex flex-wrap gap-1\">\n {{#model.triggered_signals}}\n <span class=\"badge bg-warning text-dark\">{{.}}</span>\n {{/model.triggered_signals}}\n </div>\n {{/model.triggered_signals.length}}\n {{^model.triggered_signals.length}}\n <span class=\"text-muted\">No signals triggered</span>\n {{/model.triggered_signals.length}}\n </div>\n </div>\n </div>\n </div>\n `\n });\n\n const rawSignalsView = new View({\n model: this.model,\n template: `\n <div class=\"card border-0 bg-light\">\n <div class=\"card-body\">\n <h6 class=\"fw-bold mb-3\">Raw Signals (Client)</h6>\n <pre class=\"bg-white p-3 rounded border\"><code>{{{model.raw_signals|json}}}</code></pre>\n </div>\n </div>\n `\n });\n\n const serverSignalsView = new View({\n model: this.model,\n template: `\n <div class=\"card border-0 bg-light\">\n <div class=\"card-body\">\n <h6 class=\"fw-bold mb-3\">Server Signals</h6>\n <pre class=\"bg-white p-3 rounded border\"><code>{{{model.server_signals|json}}}</code></pre>\n </div>\n </div>\n `\n });\n\n this.tabView = new TabView({\n tabs: {\n 'Overview': overviewView,\n 'Raw Signals': rawSignalsView,\n 'Server Signals': serverSignalsView,\n },\n activeTab: 'Overview',\n containerId: 'signal-tabs'\n });\n this.addChild(this.tabView);\n\n const contextMenu = new ContextMenu({\n containerId: 'signal-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Refresh', action: 'refresh', icon: 'bi-arrow-clockwise' },\n ]\n }\n });\n this.addChild(contextMenu);\n }\n\n async onActionRefresh() {\n await this.model.fetch({ params: { graph: 'detail' } });\n }\n}\n","/**\n * BouncerSignalTablePage - Bot detection assessment audit trail\n *\n * Shows bouncer signal assessments with risk scores, decisions,\n * and triggered signals for each request evaluated.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { BouncerSignalList } from '@ext/admin/models/Bouncer.js';\nimport BouncerSignalView from './BouncerSignalView.js';\n\nexport default class BouncerSignalTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_bouncer_signals',\n pageName: 'Bouncer Signals',\n router: 'admin/security/bouncer-signals',\n Collection: BouncerSignalList,\n itemViewClass: BouncerSignalView,\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: '-created',\n },\n\n columns: [\n {\n key: 'created',\n label: 'Timestamp',\n formatter: 'datetime',\n sortable: true,\n filter: { type: 'daterange' }\n },\n {\n key: 'ip_address',\n label: 'IP',\n template: '<code>{{model.ip_address}}</code>',\n filter: { type: 'text' }\n },\n {\n key: 'decision',\n label: 'Decision',\n formatter: (value) => {\n const classes = { allow: 'bg-success', monitor: 'bg-warning', block: 'bg-danger' };\n return `<span class=\"badge ${classes[value] || 'bg-secondary'}\">${(value || 'unknown').toUpperCase()}</span>`;\n },\n filter: {\n type: 'select',\n options: ['allow', 'monitor', 'block']\n }\n },\n {\n key: 'risk_score',\n label: 'Risk',\n sortable: true,\n formatter: (value) => {\n const score = value || 0;\n const color = score >= 80 ? 'danger' : score >= 50 ? 'warning' : score >= 20 ? 'info' : 'success';\n return `<span class=\"text-${color} fw-semibold\">${score}</span>`;\n }\n },\n {\n key: 'page_type',\n label: 'Page',\n filter: { type: 'text' }\n },\n {\n key: 'stage',\n label: 'Stage',\n filter: {\n type: 'select',\n options: ['assess', 'submit', 'event']\n }\n },\n {\n key: 'muid',\n label: 'Device',\n formatter: 'truncate_middle(12)'\n }\n ],\n\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n emptyMessage: 'No bouncer signals recorded yet.',\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n","/**\n * BouncerDeviceView - Device reputation detail view\n *\n * Shows device details with risk tier, linked MUIDs,\n * and tabs for related signals and incidents.\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { BouncerSignalList } from '@ext/admin/models/Bouncer.js';\nimport { IncidentList } from '@ext/admin/models/Incident.js';\n\nexport default class BouncerDeviceView extends View {\n constructor(options = {}) {\n super({\n className: 'bouncer-device-view',\n ...options\n });\n\n this.template = `\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\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: 72px; height: 72px;\">\n <i class=\"bi bi-fingerprint text-secondary\" style=\"font-size: 36px;\"></i>\n </div>\n <div>\n <h3 class=\"mb-1\">Device</h3>\n <div class=\"text-muted small\">\n <span>MUID: <code>{{model.muid}}</code></span>\n {{#model.duid}}\n <span class=\"mx-2\">|</span>\n <span>DUID: <code>{{model.duid|truncate_middle(16)}}</code></span>\n {{/model.duid}}\n </div>\n <div class=\"mt-1\">\n <span class=\"badge {{riskBadge}} me-1\">{{model.risk_tier|uppercase|default('UNKNOWN')}}</span>\n <span class=\"text-muted small\">\n {{model.event_count}} events · {{model.block_count}} blocks\n </span>\n </div>\n </div>\n </div>\n <div data-container=\"device-context-menu\"></div>\n </div>\n <div data-container=\"device-tabs\"></div>\n `;\n }\n\n get riskBadge() {\n const classes = {\n blocked: 'bg-danger', high: 'bg-danger',\n medium: 'bg-warning', low: 'bg-success', unknown: 'bg-secondary'\n };\n return classes[this.model?.get('risk_tier')] || 'bg-secondary';\n }\n\n async onInit() {\n await this.model.fetch({ params: { graph: 'detail' } });\n\n // Overview tab\n const overviewView = new View({\n model: this.model,\n template: `\n <div class=\"row\">\n <div class=\"col-md-6\">\n <div class=\"card border-0 bg-light mb-3\">\n <div class=\"card-body\">\n <h6 class=\"fw-bold mb-3\">Device Info</h6>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">MUID</label><div><code>{{model.muid}}</code></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">DUID</label><div><code>{{model.duid|default('—')}}</code></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Fingerprint ID</label><div><code>{{model.fingerprint_id|default('—')}}</code></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Risk Tier</label><div><span class=\"badge {{riskBadge}}\">{{model.risk_tier|uppercase|default('UNKNOWN')}}</span></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Last Seen IP</label><div><code>{{model.last_seen_ip|default('—')}}</code></div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Last Seen</label><div>{{model.last_seen|datetime|default('—')}}</div></div>\n </div>\n </div>\n </div>\n <div class=\"col-md-6\">\n <div class=\"card border-0 bg-light mb-3\">\n <div class=\"card-body\">\n <h6 class=\"fw-bold mb-3\">Activity</h6>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Event Count</label><div>{{model.event_count}}</div></div>\n <div class=\"mb-2\"><label class=\"form-label text-muted small fw-bold\">Block Count</label><div>{{model.block_count}}</div></div>\n <h6 class=\"fw-bold mb-2 mt-3\">Linked MUIDs</h6>\n {{#model.linked_muids.length}}\n <div class=\"d-flex flex-wrap gap-1\">\n {{#model.linked_muids}}\n <code class=\"badge bg-light text-dark border\">{{.}}</code>\n {{/model.linked_muids}}\n </div>\n {{/model.linked_muids.length}}\n {{^model.linked_muids.length}}\n <span class=\"text-muted\">No linked devices</span>\n {{/model.linked_muids.length}}\n </div>\n </div>\n </div>\n </div>\n `\n });\n\n // Signals tab\n const signalsTable = new TableView({\n Collection: BouncerSignalList,\n collectionParams: {\n size: 10,\n sort: '-created',\n muid: this.model.get('muid')\n },\n columns: [\n { key: 'created', label: 'Time', formatter: 'relative' },\n { key: 'ip_address', label: 'IP', template: '<code>{{model.ip_address}}</code>' },\n {\n key: 'decision', label: 'Decision',\n formatter: (v) => {\n const cls = { allow: 'bg-success', monitor: 'bg-warning', block: 'bg-danger' };\n return `<span class=\"badge ${cls[v] || 'bg-secondary'}\">${(v || '—').toUpperCase()}</span>`;\n }\n },\n { key: 'risk_score', label: 'Risk' },\n { key: 'page_type', label: 'Page' }\n ],\n searchable: true,\n paginated: true,\n tableOptions: { hover: true, size: 'sm' }\n });\n\n // Incidents tab\n const incidentsTable = new TableView({\n Collection: IncidentList,\n collectionParams: {\n size: 10,\n sort: '-created',\n category__startswith: 'security:bouncer',\n search: this.model.get('muid')\n },\n columns: [\n { key: 'created', label: 'Created', formatter: 'epoch|datetime' },\n { key: 'status', label: 'Status' },\n { key: 'category', label: 'Category' },\n { key: 'title', label: 'Title', formatter: 'truncate(60)' }\n ],\n searchable: true,\n paginated: true,\n tableOptions: { hover: true, size: 'sm' }\n });\n\n this.tabView = new TabView({\n tabs: {\n 'Overview': overviewView,\n 'Signals': signalsTable,\n 'Incidents': incidentsTable,\n },\n activeTab: 'Overview',\n containerId: 'device-tabs'\n });\n this.addChild(this.tabView);\n\n const contextMenu = new ContextMenu({\n containerId: 'device-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Refresh', action: 'refresh', icon: 'bi-arrow-clockwise' },\n ]\n }\n });\n this.addChild(contextMenu);\n }\n\n async onActionRefresh() {\n await this.model.fetch({ params: { graph: 'detail' } });\n }\n}\n","/**\n * BouncerDeviceTablePage - Device reputation management\n *\n * Shows bouncer device records with risk tiers, event counts,\n * and block counts for each tracked device.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { BouncerDeviceList } from '@ext/admin/models/Bouncer.js';\nimport BouncerDeviceView from './BouncerDeviceView.js';\n\nexport default class BouncerDeviceTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_bouncer_devices',\n pageName: 'Bouncer Devices',\n router: 'admin/security/bouncer-devices',\n Collection: BouncerDeviceList,\n itemViewClass: BouncerDeviceView,\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: '-last_seen',\n },\n\n columns: [\n {\n key: 'muid',\n label: 'MUID',\n template: '<code>{{model.muid|truncate_middle(16)}}</code>',\n sortable: true,\n filter: { type: 'text' }\n },\n {\n key: 'risk_tier',\n label: 'Risk Tier',\n sortable: true,\n formatter: (value) => {\n const classes = {\n blocked: 'bg-danger',\n high: 'bg-danger',\n medium: 'bg-warning',\n low: 'bg-success',\n unknown: 'bg-secondary'\n };\n return `<span class=\"badge ${classes[value] || 'bg-secondary'}\">${(value || 'unknown').toUpperCase()}</span>`;\n },\n filter: {\n type: 'select',\n options: ['unknown', 'low', 'medium', 'high', 'blocked']\n }\n },\n {\n key: 'event_count',\n label: 'Events',\n sortable: true\n },\n {\n key: 'block_count',\n label: 'Blocks',\n sortable: true\n },\n {\n key: 'last_seen_ip',\n label: 'Last IP',\n template: '<code>{{model.last_seen_ip|default(\"—\")}}</code>'\n },\n {\n key: 'last_seen',\n label: 'Last Seen',\n formatter: 'relative',\n sortable: true\n }\n ],\n\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n emptyMessage: 'No bouncer devices tracked yet.',\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n","/**\n * BotSignatureTablePage - Bot signature management with full CRUD\n *\n * Manages auto-learned and manual bot signatures used by the\n * bouncer pre-screening system. Supports create, enable/disable, delete.\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { BouncerSignatureList, BouncerSignatureForms } from '@ext/admin/models/Bouncer.js';\n\nexport default class BotSignatureTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_bot_signatures',\n pageName: 'Bot Signatures',\n router: 'admin/security/bot-signatures',\n Collection: BouncerSignatureList,\n\n formCreate: BouncerSignatureForms.create,\n formEdit: BouncerSignatureForms.edit,\n\n viewDialogOptions: {\n size: 'lg'\n },\n\n defaultQuery: {\n sort: '-modified',\n },\n\n columns: [\n {\n key: 'sig_type',\n label: 'Type',\n formatter: 'badge',\n sortable: true,\n filter: {\n type: 'select',\n options: ['user_agent', 'ip_pattern', 'fingerprint', 'behavior', 'header', 'cookie']\n }\n },\n {\n key: 'value',\n label: 'Value',\n formatter: 'truncate(60)',\n filter: { type: 'text' }\n },\n {\n key: 'source',\n label: 'Source',\n formatter: (value) => {\n const cls = value === 'auto' ? 'bg-info' : 'bg-primary';\n return `<span class=\"badge ${cls}\">${(value || 'unknown').toUpperCase()}</span>`;\n },\n filter: {\n type: 'select',\n options: ['auto', 'manual']\n }\n },\n {\n key: 'confidence',\n label: 'Confidence',\n sortable: true,\n formatter: (value) => `${value || 0}%`\n },\n {\n key: 'hit_count',\n label: 'Hits',\n sortable: true\n },\n {\n key: 'is_active',\n label: 'Active',\n formatter: (value) => {\n return value\n ? '<span class=\"badge bg-success\">ON</span>'\n : '<span class=\"badge bg-secondary\">OFF</span>';\n },\n filter: {\n type: 'select',\n options: [\n { label: 'Active', value: 'true' },\n { label: 'Inactive', value: 'false' }\n ]\n }\n },\n {\n key: 'expires_at',\n label: 'Expires',\n formatter: 'datetime|default(\"Never\")'\n }\n ],\n\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n selectable: true,\n\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n emptyMessage: 'No bot signatures. Click \"Add\" to create a manual signature.',\n\n batchBarLocation: 'top',\n batchActions: [\n { label: 'Enable', icon: 'bi bi-check-circle', action: 'enable' },\n { label: 'Disable', icon: 'bi bi-pause-circle', action: 'disable' },\n { label: 'Delete', icon: 'bi bi-trash', action: 'delete' },\n ],\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionBatchEnable() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n await Promise.all(selected.map(item => item.model.save({ is_active: true })));\n this.getApp().toast.success(`${selected.length} signature(s) enabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDisable() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n await Promise.all(selected.map(item => item.model.save({ is_active: false })));\n this.getApp().toast.success(`${selected.length} signature(s) disabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDelete() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n\n const confirmed = await this.getApp().confirm(\n `Delete ${selected.length} signature${selected.length > 1 ? 's' : ''}? This cannot be undone.`\n );\n if (!confirmed) return;\n\n await Promise.all(selected.map(item => item.model.destroy()));\n this.getApp().toast.success(`${selected.length} signature(s) deleted`);\n this.tableView.collection.fetch();\n }\n}\n","/**\n * IPSetView - Detail view for an IPSet record\n *\n * TabView layout: Configuration overview + CIDR data viewer.\n * Context menu for fleet operations: sync, enable/disable, refresh, delete.\n */\n\nimport View from '@core/View.js';\nimport TabView from '@core/views/navigation/TabView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { IPSet, IPSetForms, IPSetKindBadgeOptions, IPSetSourceOptions } from '@ext/admin/models/IPSet.js';\n\nclass IPSetView extends View {\n constructor(options = {}) {\n super({\n className: 'ipset-view',\n ...options\n });\n\n this.model = options.model || new IPSet(options.data || {});\n\n const kind = this.model.get('kind') || '';\n const kindOpt = IPSetKindBadgeOptions.find(o => o.value === kind);\n this.kindLabel = kindOpt ? kindOpt.label : kind;\n this.isEnabled = !!this.model.get('is_enabled');\n this.enabledLabel = this.isEnabled ? 'Enabled' : 'Disabled';\n this.enabledBadge = this.isEnabled ? 'bg-success' : 'bg-secondary';\n\n this.template = `\n <div class=\"ipset-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"fs-1 text-primary\">\n <i class=\"bi bi-shield-shaded\"></i>\n </div>\n <div>\n <h4 class=\"mb-1\">{{model.name}}</h4>\n {{#model.description}}\n <div class=\"text-muted mb-1\">{{model.description}}</div>\n {{/model.description}}\n <div class=\"d-flex align-items-center gap-2\">\n <span class=\"badge bg-primary\">{{kindLabel}}</span>\n <span class=\"badge {{enabledBadge}}\">{{enabledLabel}}</span>\n {{#model.cidr_count}}\n <span class=\"badge bg-light text-dark border\">{{model.cidr_count}} CIDRs</span>\n {{/model.cidr_count}}\n </div>\n </div>\n </div>\n <div data-container=\"ipset-context-menu\"></div>\n </div>\n\n <!-- Tabs -->\n <div data-container=\"ipset-tabs\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // ── Configuration tab ──\n const sourceVal = this.model.get('source') || '';\n const sourceOpt = IPSetSourceOptions.find(o => o.value === sourceVal);\n const sourceLabel = sourceOpt ? sourceOpt.label : sourceVal;\n\n const lastSynced = this.model.get('last_synced');\n const syncError = this.model.get('sync_error');\n\n const configFields = [\n { name: 'name', label: 'Name', cols: 6 },\n { name: 'kind', label: 'Kind', template: this.kindLabel, cols: 3 },\n { name: 'is_enabled', label: 'Enabled', formatter: 'yesnoicon', cols: 3 },\n { name: 'description', label: 'Description', cols: 12 },\n { name: 'source', label: 'Source', template: sourceLabel, cols: 4 },\n { name: 'source_url', label: 'Source URL', cols: 8 },\n { name: 'cidr_count', label: 'CIDRs Loaded', cols: 4 },\n {\n name: 'last_synced',\n label: 'Last Synced',\n template: lastSynced ? new Date(lastSynced).toLocaleString() : 'Never',\n cols: 4\n },\n {\n name: 'sync_error',\n label: 'Sync Status',\n template: syncError\n ? `<span class=\"text-danger\">${syncError}</span>`\n : '<span class=\"text-success\"><i class=\"bi bi-check-circle me-1\"></i>OK</span>',\n cols: 4\n },\n ];\n\n this.configView = new DataView({\n model: this.model,\n className: 'p-3',\n columns: 2,\n showEmptyValues: true,\n emptyValueText: '—',\n fields: configFields\n });\n\n // ── CIDR Data tab ──\n this.cidrView = new View({\n className: 'p-3',\n ipsetModel: this.model,\n cidrData: null,\n cidrLoading: true,\n template: `\n {{#cidrLoading|bool}}\n <div class=\"text-center py-4 text-muted\">\n <div class=\"spinner-border spinner-border-sm me-2\" role=\"status\"></div>\n Loading CIDR data...\n </div>\n {{/cidrLoading|bool}}\n {{^cidrLoading|bool}}\n {{#cidrData}}\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\n <span class=\"text-muted small\">{{ipsetModel.cidr_count}} CIDRs loaded</span>\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"copy-cidrs\" data-bs-toggle=\"tooltip\" title=\"Copy to clipboard\">\n <i class=\"bi bi-clipboard me-1\"></i>Copy\n </button>\n </div>\n <pre class=\"bg-light border rounded p-3 small\" style=\"max-height: 500px; overflow-y: auto;\"><code>{{cidrData}}</code></pre>\n {{/cidrData}}\n {{^cidrData}}\n <div class=\"text-center py-5 text-muted\">\n <i class=\"bi bi-database fs-1 mb-2 d-block\"></i>\n <p>No CIDRs loaded.</p>\n <button class=\"btn btn-primary btn-sm\" data-action=\"refresh-source\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Refresh Source\n </button>\n </div>\n {{/cidrData}}\n {{/cidrLoading|bool}}\n `,\n async onInit() {\n try {\n const resp = await this.ipsetModel.rest.GET(\n `${this.ipsetModel.endpoint}/${this.ipsetModel.id}`,\n { graph: 'detailed' }\n );\n if (resp.success || resp.status === 200) {\n const data = resp.data?.data || resp.data;\n this.cidrData = data?.data || null;\n }\n } catch (_e) {\n // Failed to load\n }\n this.cidrLoading = false;\n },\n async onActionCopyCidrs() {\n if (this.cidrData) {\n await navigator.clipboard.writeText(this.cidrData);\n this.getApp()?.toast?.success('CIDRs copied to clipboard');\n }\n },\n async onActionRefreshSource() {\n const resp = await this.ipsetModel.save({ refresh_source: 1 });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('Refreshing source — this may take a moment');\n } else {\n this.getApp()?.toast?.error('Failed to refresh source');\n }\n }\n });\n\n // ── TabView ──\n this.tabView = new TabView({\n containerId: 'ipset-tabs',\n tabs: {\n 'Configuration': this.configView,\n 'CIDR Data': this.cidrView\n },\n activeTab: 'Configuration'\n });\n this.addChild(this.tabView);\n\n // ── Context Menu ──\n const isEnabled = this.model.get('is_enabled');\n const contextMenu = new ContextMenu({\n containerId: 'ipset-context-menu',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Sync to Fleet', action: 'sync-fleet', icon: 'bi-broadcast' },\n { label: 'Refresh Source', action: 'refresh-source', icon: 'bi-arrow-clockwise' },\n { type: 'divider' },\n isEnabled\n ? { label: 'Disable', action: 'disable-ipset', icon: 'bi-toggle-off' }\n : { label: 'Enable', action: 'enable-ipset', icon: 'bi-toggle-on' },\n { label: 'Edit IP Set', action: 'edit-ipset', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'Delete IP Set', action: 'delete-ipset', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(contextMenu);\n }\n\n // ── Actions ──\n\n async onActionSyncFleet() {\n const resp = await this.model.save({ sync: 1 });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('Syncing to fleet...');\n } else {\n this.getApp()?.toast?.error('Sync failed');\n }\n }\n\n async onActionRefreshSource() {\n const resp = await this.model.save({ refresh_source: 1 });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('Refreshing source data...');\n } else {\n this.getApp()?.toast?.error('Refresh failed');\n }\n }\n\n async onActionEnableIpset() {\n const resp = await this.model.save({ enable: 1 });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('IP Set enabled and synced');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to enable');\n }\n }\n\n async onActionDisableIpset() {\n const confirmed = await Modal.confirm(\n 'Disable this IP set? It will be removed from iptables on all fleet instances.',\n 'Disable IP Set'\n );\n if (!confirmed) return;\n\n const resp = await this.model.save({ disable: 1 });\n if (resp.success || resp.status === 200) {\n this.getApp()?.toast?.success('IP Set disabled and removed from fleet');\n await this.render();\n } else {\n this.getApp()?.toast?.error('Failed to disable');\n }\n }\n\n async onActionEditIpset() {\n const resp = await Modal.modelForm({\n title: `Edit IP Set — ${this.model.get('name')}`,\n model: this.model,\n formConfig: IPSetForms.edit,\n });\n if (resp) {\n await this.render();\n this.getApp()?.toast?.success('IP Set updated');\n }\n }\n\n async onActionDeleteIpset() {\n const confirmed = await Modal.confirm(\n `Delete IP set \"${this.model.get('name')}\"? This will remove it from all fleet instances. This cannot be undone.`,\n 'Delete IP Set',\n { confirmText: 'Delete', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n try {\n await this.model.destroy();\n this.getApp()?.toast?.success('IP Set deleted');\n this.emit('ipset:deleted', { model: this.model });\n\n // Close the modal\n const dialog = this.element?.closest('.modal');\n if (dialog) {\n const bsModal = window.bootstrap?.Modal?.getInstance(dialog);\n if (bsModal) bsModal.hide();\n }\n } catch (error) {\n this.getApp()?.toast?.error(`Delete failed: ${error.message}`);\n }\n }\n}\n\nIPSet.VIEW_CLASS = IPSetView;\n\nexport default IPSetView;\n","/**\n * IPSetTablePage - Manage kernel-level IP blocking sets\n *\n * Supports country blocking, abuse feeds (AbuseIPDB), datacenter ranges,\n * and custom CIDR lists. Fleet-wide sync via ipset + iptables.\n *\n * Route: system/security/ipsets\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { IPSet, IPSetList, IPSetForms, IPSetKindBadgeOptions, IPSetSourceOptions, CommonBlockCountries } from '@ext/admin/models/IPSet.js';\nimport IPSetView from './IPSetView.js';\n\nclass IPSetTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_ipsets',\n pageName: 'IP Sets',\n router: 'admin/security/ipsets',\n Collection: IPSetList,\n itemViewClass: IPSetView,\n formEdit: IPSetForms.edit,\n onAdd: () => this._handleAdd(),\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: 'name',\n },\n\n columns: [\n {\n key: 'is_enabled', label: 'Active', width: '70px', sortable: true,\n formatter: 'yesnoicon',\n filter: {\n type: 'select',\n options: [\n { value: 'true', label: 'Enabled' },\n { value: 'false', label: 'Disabled' }\n ]\n }\n },\n { key: 'name', label: 'Name', sortable: true },\n {\n key: 'kind', label: 'Kind', sortable: true, width: '120px',\n formatter: (value) => {\n const opt = IPSetKindBadgeOptions.find(o => o.value === value);\n return `<span class=\"badge bg-primary bg-opacity-75\">${opt ? opt.label : value}</span>`;\n },\n filter: {\n type: 'select',\n options: IPSetKindBadgeOptions\n }\n },\n {\n key: 'description', label: 'Description',\n formatter: \"truncate(40)|default('—')\"\n },\n { key: 'cidr_count', label: 'CIDRs', width: '80px', sortable: true },\n {\n key: 'source', label: 'Source', width: '110px',\n formatter: (value) => {\n const opt = IPSetSourceOptions.find(o => o.value === value);\n return opt ? opt.label : (value || '—');\n }\n },\n {\n key: 'last_synced|datetime', label: 'Last Synced', width: '160px', sortable: true,\n },\n {\n key: 'sync_error', label: 'Status', width: '80px',\n formatter: (value) => {\n if (value) return '<span class=\"text-danger\" title=\"' + value + '\"><i class=\"bi bi-exclamation-triangle\"></i> Error</span>';\n return '<span class=\"text-success\"><i class=\"bi bi-check-circle\"></i></span>';\n }\n },\n ],\n\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n emptyMessage: 'No IP sets configured. Create one to start blocking traffic at the network level.',\n\n batchBarLocation: 'top',\n batchActions: [\n { label: 'Enable', icon: 'bi bi-toggle-on', action: 'enable' },\n { label: 'Disable', icon: 'bi bi-toggle-off', action: 'disable' },\n { label: 'Sync to Fleet', icon: 'bi bi-broadcast', action: 'sync' },\n { label: 'Refresh Source', icon: 'bi bi-arrow-clockwise', action: 'refresh' },\n { label: 'Delete', icon: 'bi bi-trash', action: 'delete', danger: true },\n ],\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n /**\n * Custom add handler — transforms country_code into proper IPSet fields\n */\n async _handleAdd() {\n const result = await Modal.form({\n ...IPSetForms.create\n });\n if (!result) return;\n\n // Transform form data based on kind\n if (result.kind === 'country' && result.country_code) {\n const cc = result.country_code;\n const countryOpt = CommonBlockCountries.find(o => o.value === cc);\n result.name = `country_${cc}`;\n result.source = 'ipdeny';\n result.description = countryOpt ? `Country block: ${countryOpt.label}` : `Country block: ${cc.toUpperCase()}`;\n delete result.country_code;\n } else if (result.kind === 'abuse') {\n result.source = 'abuseipdb';\n } else if (result.kind === 'datacenter' || result.kind === 'custom') {\n result.source = 'manual';\n }\n\n const model = new IPSet();\n const resp = await model.save(result);\n if (resp?.data?.status) {\n this.getApp()?.toast?.success('IP Set created');\n this.tableView?.collection?.fetch();\n } else {\n Modal.showError(resp?.data?.error || 'Failed to create IP Set');\n }\n }\n\n // ── Batch Actions ──\n\n async onActionBatchEnable() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Enable ${selected.length} IP set(s)? They will be synced to the fleet.`);\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.save({ enable: 1 })));\n this.getApp().toast.success(`${selected.length} IP set(s) enabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDisable() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Disable ${selected.length} IP set(s)? They will be removed from the fleet.`);\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.save({ disable: 1 })));\n this.getApp().toast.success(`${selected.length} IP set(s) disabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchSync() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Sync ${selected.length} IP set(s) to all fleet instances?`);\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.save({ sync: 1 })));\n this.getApp().toast.success(`${selected.length} IP set(s) syncing to fleet`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchRefresh() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Refresh source data for ${selected.length} IP set(s)?`);\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.save({ refresh_source: 1 })));\n this.getApp().toast.success(`${selected.length} IP set(s) refreshing`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDelete() {\n const selected = this.tableView.getSelectedItems();\n if (!selected.length) return;\n const confirmed = await Modal.confirm(\n `Delete ${selected.length} IP set(s)? They will be removed from all fleet instances. This cannot be undone.`,\n 'Delete IP Sets',\n { confirmText: 'Delete', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n await Promise.all(selected.map(item => item.model.destroy()));\n this.getApp().toast.success(`${selected.length} IP set(s) deleted`);\n this.tableView.collection.fetch();\n }\n}\n\nexport default IPSetTablePage;\n","/**\n * LogTablePage - Log management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { LogList } from '@core/models/Log.js';\nimport LogView from './LogView.js';\n\nclass LogTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_logs',\n pageName: 'Manage Logs',\n router: \"admin/logs\",\n Collection: LogList,\n\n itemViewClass: LogView,\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n // Column definitions\n columns: [\n {\n key: 'created|epoch|datetime',\n label: 'Timestamp',\n sortable: true,\n filter: {\n type: 'daterange',\n }\n },\n {\n key: 'level',\n label: 'Level',\n sortable: true,\n formatter: 'badge',\n filter: {\n type: 'select',\n options: [\n { value: 'info', label: 'Info' },\n { value: 'warning', label: 'Warning' },\n { value: 'error', label: 'Error' }\n ]\n }\n },\n {\n key: 'kind',\n label: 'Kind',\n filter: {\n type: \"text\"\n }\n },\n {\n key: 'method',\n label: 'Method',\n filter: {\n type: \"text\"\n }\n },\n {\n key: 'path',\n label: 'Path',\n filter: {\n type: \"text\"\n }\n },\n {\n key: 'username',\n label: 'User',\n filter: {\n type: \"text\"\n }\n },\n {\n key: 'ip',\n label: 'IP',\n filter: {\n type: \"text\"\n }\n },\n {\n key: 'duid',\n label: 'Browser ID',\n formatter: 'truncate_middle(16)',\n filter: {\n type: \"text\"\n }\n }\n ],\n\n // Default sort by timestamp descending (newest first)\n defaultQuery: {\n sort: '-created'\n },\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: false, // Logs are typically not manually created\n showExport: true,\n\n // Empty state\n emptyMessage: 'No log entries found.',\n\n // Batch actions\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Export\", icon: \"bi bi-download\", action: \"batch-export\" },\n { label: \"Archive\", icon: \"bi bi-archive\", action: \"batch-archive\" },\n { label: \"Delete\", icon: \"bi bi-trash\", action: \"batch-delete\" },\n { label: \"Mark as Reviewed\", icon: \"bi bi-check2\", action: \"batch-reviewed\" }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default LogTablePage;\n","import View from '@core/View.js';\nimport DataView from '@core/views/data/DataView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { MetricsPermission, MetricsForms } from '@core/models/Metrics.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass MetricsPermissionsView extends View {\n constructor(options = {}) {\n super({\n className: 'metrics-permissions-view',\n ...options\n });\n\n this.model = options.model || new MetricsPermission(options.data || {});\n\n 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 `;\n }\n\n async onInit() {\n this.dataView = new DataView({\n containerId: 'data-view',\n model: this.model,\n fields: [\n { name: 'view_permissions', label: 'View Permissions', format: 'list|badge' },\n { name: 'write_permissions', label: 'Write Permissions', format: 'list|badge' },\n ]\n });\n this.addChild(this.dataView);\n\n const contextMenu = new ContextMenu({\n containerId: 'context-menu',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit', action: 'edit', icon: 'bi-pencil' },\n { label: 'Delete', action: 'delete', icon: 'bi-trash', danger: true },\n ]\n }\n });\n this.addChild(contextMenu);\n }\n\n async onActionEdit() {\n const resp = await Modal.modelForm({\n title: `Edit Permissions for ${this.model.get('account')}`,\n model: this.model,\n formConfig: MetricsForms.edit\n });\n if (resp) {\n this.model.set(resp.data.data);\n this.render();\n }\n }\n\n async onActionDelete() {\n const confirmed = await Modal.confirm(`Are you sure you want to delete all permissions for ${this.model.get('account')}?`);\n if (confirmed) {\n await this.model.destroy();\n this.emit('deleted', this.model);\n }\n }\n}\n\nMetricsPermission.VIEW_CLASS = MetricsPermissionsView;\n\nexport default MetricsPermissionsView;\n","import TablePage from '@core/pages/TablePage.js';\nimport { MetricsPermissionList, MetricsForms } from '@core/models/Metrics.js';\nimport MetricsPermissionsView from './MetricsPermissionsView.js';\n\nclass MetricsPermissionsTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_metrics_permissions',\n pageName: 'Metrics Permissions',\n router: \"admin/metrics/permissions\",\n Collection: MetricsPermissionList,\n formEdit: MetricsForms.edit,\n itemViewClass: MetricsPermissionsView,\n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n\n columns: [\n { key: 'account', label: 'Account', sortable: true },\n { key: 'view_permissions', label: 'View Permissions', formatter: 'list|badge' },\n { key: 'write_permissions', label: 'Write Permissions', formatter: 'list|badge' },\n ],\n\n selectable: true,\n searchable: true,\n sortable: true,\n paginated: true,\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n tableOptions: {\n pageSizes: [10, 25, 50],\n defaultPageSize: 25,\n emptyMessage: 'No metrics permissions found.',\n emptyIcon: 'bi-bar-chart-line',\n actions: [\"view\", \"edit\", \"delete\"],\n }\n });\n }\n}\n\nexport default MetricsPermissionsTablePage;\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\nimport { GroupList } from '@core/models/Group.js';\n\n/**\n * Setting - Global or group-scoped configuration key-value pair.\n * Maps to REST endpoints under /api/settings\n *\n * Key properties:\n * - Can be global or scoped to a group (via `group` field)\n * - Supports secret values (`is_secret: true`) — API masks display_value as \"******\"\n * - Secret values are write-only; the raw value is never returned after creation\n *\n * Endpoints:\n * GET /api/settings - List settings (requires manage_settings)\n * POST /api/settings - Create a setting\n * GET /api/settings/<id> - Get one setting\n * POST /api/settings/<id> - Update a setting\n * DELETE /api/settings/<id> - Delete a setting\n */\nclass Setting extends Model {\n constructor(data = {}, options = {}) {\n super(data, {\n endpoint: '/api/settings',\n ...options\n });\n }\n}\n\n/**\n * SettingList - Collection of Setting records.\n * Supports search and sort query params.\n */\nclass SettingList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: Setting,\n endpoint: '/api/settings',\n size: 25,\n ...options\n });\n }\n}\n\n/**\n * Forms configuration for Setting\n */\nconst SettingForms = {\n create: {\n title: 'Create Setting',\n fields: [\n {\n name: 'key',\n type: 'text',\n label: 'Key',\n placeholder: 'WEBHOOK_SECRET',\n required: true,\n columns: 12,\n help: 'A unique configuration key name.'\n },\n {\n name: 'value',\n type: 'textarea',\n label: 'Value',\n required: true,\n columns: 12,\n help: 'The configuration value. For secrets, this will be masked after creation.'\n },\n {\n type: 'collection',\n name: 'parent',\n label: 'Parent Group',\n Collection: GroupList, // Collection class\n labelField: 'name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search groups...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n columns: 12\n },\n {\n name: 'is_secret',\n type: 'switch',\n label: 'Secret',\n columns: 6,\n help: 'Mark as secret to mask the value in API responses.'\n }\n ]\n },\n\n edit: {\n title: 'Edit Setting',\n fields: [\n {\n name: 'key',\n type: 'text',\n label: 'Key',\n columns: 12,\n disabled: true\n },\n {\n name: 'value',\n type: 'textarea',\n label: 'Value',\n columns: 12,\n help: 'Enter a new value to replace the current one.'\n },\n {\n name: 'is_secret',\n type: 'switch',\n label: 'Secret',\n columns: 12,\n help: 'Mark as secret to mask the value in API responses.'\n },\n {\n type: 'collection',\n name: 'parent',\n label: 'Parent Group',\n Collection: GroupList, // Collection class\n labelField: 'name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search groups...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n columns: 12\n }\n ]\n }\n};\n\nexport { Setting, SettingList, SettingForms };\n","/**\n * SettingView - Secure setting detail and management interface\n *\n * Shows setting metadata (key, value, group scope, secret status)\n * and provides actions to edit and delete. Secret values display\n * the masked display_value from the API.\n */\n\nimport View from '@core/View.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { Setting, SettingForms } from '@core/models/Settings.js';\n\nclass SettingView extends View {\n constructor(options = {}) {\n super({\n className: 'setting-view',\n ...options\n });\n\n this.model = options.model || new Setting(options.data || {});\n\n this.template = `\n <div class=\"setting-view-container\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start mb-4\">\n <!-- Left: Icon & Identity -->\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"fs-1 text-primary\">\n <i class=\"bi bi-gear\"></i>\n </div>\n <div>\n <h3 class=\"mb-1 font-monospace\">{{model.key|default('Unnamed Setting')}}</h3>\n <div class=\"text-muted small\">\n ID: {{model.id}}\n <span class=\"mx-2\">|</span>\n Scope: {{model.group.name|default('Global')}}\n </div>\n <div class=\"mt-1\">\n <span class=\"badge {{model.is_secret|boolean('bg-warning text-dark','bg-secondary')}}\">\n {{model.is_secret|boolean('Secret','Plain')}}\n </span>\n </div>\n </div>\n </div>\n\n <!-- Right: Meta & Actions -->\n <div class=\"d-flex align-items-start gap-4\">\n <div class=\"text-end\">\n <div class=\"text-muted small\">Created</div>\n <div>{{model.created|datetime}}</div>\n </div>\n <div data-container=\"setting-context-menu\"></div>\n </div>\n </div>\n\n <!-- Details -->\n <div class=\"list-group mb-3\">\n <div class=\"list-group-item\">\n <h6 class=\"mb-1 text-muted\">Key</h6>\n <p class=\"mb-0 font-monospace\">{{model.key}}</p>\n </div>\n <div class=\"list-group-item\">\n <h6 class=\"mb-1 text-muted\">Value</h6>\n {{#model.is_secret|bool}}\n <p class=\"mb-0 text-muted font-monospace\">{{model.display_value|default('******')}}</p>\n <small class=\"text-muted\">This is a secret value. Enter a new value to replace it.</small>\n {{/model.is_secret|bool}}\n {{^model.is_secret|bool}}\n <p class=\"mb-0 font-monospace\">{{model.value|default(model.display_value)|default('—')}}</p>\n {{/model.is_secret|bool}}\n </div>\n {{#model.group}}\n <div class=\"list-group-item\">\n <h6 class=\"mb-1 text-muted\">Group</h6>\n <p class=\"mb-0\">{{model.group.name|default(model.group)}}</p>\n </div>\n {{/model.group}}\n </div>\n </div>\n `;\n }\n\n async onInit() {\n const settingMenu = new ContextMenu({\n containerId: 'setting-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Edit', action: 'edit-setting', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'Delete Setting', action: 'delete-setting', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(settingMenu);\n }\n\n async onActionEditSetting() {\n const app = this.getApp();\n const resp = await app.showModelForm({\n title: `Edit Setting — ${this.model.get('key')}`,\n model: this.model,\n formConfig: SettingForms.edit,\n });\n if (resp) {\n this.render();\n }\n }\n\n async onActionDeleteSetting() {\n const app = this.getApp();\n const confirmed = await app.confirm({\n title: 'Delete Setting',\n message: `Permanently delete \"${this.model.get('key')}\"? This cannot be undone.`,\n confirmLabel: 'Delete',\n confirmClass: 'btn-danger'\n });\n if (!confirmed) return;\n\n app.showLoading();\n const resp = await this.model.delete();\n app.hideLoading();\n if (resp && resp.success !== false) {\n app.toast.success('Setting deleted');\n this.emit('deleted', { model: this.model });\n } else {\n app.toast.error('Failed to delete setting');\n }\n }\n}\n\nSetting.VIEW_CLASS = SettingView;\nexport default SettingView;\n","/**\n * SettingTablePage - Secure settings management using TablePage component\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { Setting, SettingList, SettingForms } from '@core/models/Settings.js';\nimport SettingView from './SettingView.js';\n\n// Register the add/edit forms on the model class so TableView can find them automatically\nSetting.ADD_FORM = SettingForms.create;\nSetting.EDIT_FORM = SettingForms.edit;\n\nclass SettingTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_settings',\n pageName: 'Settings',\n router: 'admin/settings',\n Collection: SettingList,\n\n itemViewClass: SettingView,\n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'key', label: 'Key', sortable: true },\n { key: 'display_value', label: 'Value', formatter: \"default('—')\" },\n { key: 'group.name', label: 'Group', sortable: true, formatter: \"default('Global')\" },\n {\n key: 'is_secret',\n label: 'Secret',\n formatter: \"boolean('Secret|bg-warning text-dark','Plain|bg-secondary')|badge\",\n width: '100px'\n },\n { key: 'created', label: 'Created', formatter: 'datetime', sortable: true }\n ],\n\n defaultQuery: {\n sort: 'key'\n },\n\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n showRefresh: true,\n showAdd: true,\n showExport: false,\n\n addButtonLabel: 'New Setting',\n\n emptyMessage: 'No settings found.',\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default SettingTablePage;\n","/**\n * FileManagerTablePage - File Manager backend management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { FileManagerList, FileManagerForms } from '@core/models/Files.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass FileManagerTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_file_managers',\n pageName: 'Manage Storage Backends',\n router: \"admin/file-managers\",\n Collection: FileManagerList,\n\n formCreate: FileManagerForms.create,\n formEdit: FileManagerForms.edit,\n\n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n sortable: true,\n class: 'text-muted'\n },\n {\n key: 'name',\n label: 'Name',\n formatter: \"default('Unnamed Backend')\"\n },\n {\n key: 'backend_url',\n label: 'Backend URL',\n sortable: true\n },\n {\n key: 'is_default',\n label: 'Default',\n formatter: \"boolean|badge\"\n },\n {\n key: 'is_active',\n label: 'Active',\n formatter: \"boolean|badge\"\n },\n {\n key: 'is_public',\n label: 'Public',\n formatter: \"boolean|badge\"\n },\n {\n key: 'backend_type',\n label: 'Type',\n formatter: \"default('Unknown')\"\n },\n {\n key: 'created',\n label: 'Created',\n formatter: \"epoch|datetime\"\n }\n ],\n\n contextMenu: [\n { icon: 'bi-pencil', action: 'edit', label: 'Edit Name' },\n { icon: 'bi-shield', action: 'edit-credentials', label: 'Edit Credentials' },\n { icon: 'bi-person', action: 'edit-owners', label: 'Edit Owners' },\n { divider: true },\n { icon: 'bi-copy', action: 'clone', label: 'Clone Manager' },\n { divider: true },\n { icon: 'bi-check', action: 'test-connection', label: 'Test Connection' },\n { icon: 'bi-question-circle', action: 'check-cors', label: 'Check CORS' },\n { icon: 'bi-wrench', action: 'fix-cors', label: 'Fix CORS' },\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No storage backends found. Click \"Add Storage Backend\" to configure your first backend.',\n\n // Batch actions\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Delete\", icon: \"bi bi-trash\", action: \"batch-delete\" },\n { label: \"Export\", icon: \"bi bi-download\", action: \"batch-export\" },\n { label: \"Activate\", icon: \"bi bi-check-circle\", action: \"batch-activate\" },\n { label: \"Deactivate\", icon: \"bi bi-x-circle\", action: \"batch-deactivate\" }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n\n async onActionEditOwners(event, element) {\n const item = this.collection.get(element.dataset.id);\n const result = await Modal.modelForm({\n title: 'Edit Owners',\n model: item,\n fields: FileManagerForms.owners.fields\n });\n if (!result) return true;\n if (result.success) {\n this.getApp().toast.success(\"Owners Updated successfully\");\n } else {\n this.getApp().toast.error(\"Owners update failed\");\n }\n }\n\n async onActionCheckCors(event, element) {\n const item = this.collection.get(element.dataset.id);\n const result = await item.save({check_cors: true});\n if (result.success && result.data.status) {\n // this.getApp().toast.success(result.data.result);\n await Modal.data({\n title: `Audit Report - ${item._.name}`,\n data: result.data,\n size: 'lg'\n });\n } else {\n this.getApp().toast.error(\"Connection test failed\");\n }\n return true;\n }\n\n async onActionTestConnection(event, element) {\n const item = this.collection.get(element.dataset.id);\n const result = await item.save({test_connection: true});\n if (result.success && result.data.status) {\n this.getApp().toast.success(\"Connection test successful\");\n } else {\n this.getApp().toast.error(\"Connection test failed\");\n }\n return true;\n }\n\n async onActionEditCredentials(event, element) {\n const item = this.collection.get(element.dataset.id);\n const result = await Modal.modelForm({\n title: 'Edit Credentials',\n model: item,\n fields: FileManagerForms.credentials.fields\n });\n\n if (!result) return true;\n\n if (result.success && result.data.status) {\n this.getApp().toast.success(\"Credentials updated successfully\");\n } else {\n this.getApp().toast.error(\"Failed to update credentials\");\n }\n return true;\n }\n\n async onActionClone(event, element) {\n\n const confirmed = await Modal.confirm({\n title: 'Clone File Manager',\n message: 'This will create a clone with the same credentials.',\n });\n\n if (!confirmed) {\n return true;\n }\n\n const item = this.collection.get(element.dataset.id);\n const result = await item.save({clone: true});\n if (result.success && result.data.status) {\n this.getApp().toast.success(\"Connection cloned successfully\");\n this.collection.fetch();\n } else {\n this.getApp().toast.error(\"Failed to clone connection\");\n }\n return true;\n }\n}\n\nexport default FileManagerTablePage;\n","/**\n * FileTablePage - File management using TablePage component\n * Clean implementation with file drop support\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { File, FileList, FileForms } from '@core/models/Files.js';\nimport FileView from '@core/views/data/FileView.js';\nimport applyFileDropMixin from '@core/mixins/FileDropMixin.js';\n\nclass FileTablePage extends TablePage {\n constructor(options = {}) {\n super({\n name: 'admin_files',\n pageName: 'Manage Files',\n router: \"admin/files\",\n Collection: FileList,\n\n // Don't use formCreate - we have custom file upload handling\n formEdit: FileForms.edit,\n itemViewClass: FileView,\n \n // Custom add handler for file uploads\n onAdd: async (event) => {\n await this.handleFileUpload(event);\n },\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n sortable: true,\n class: 'text-muted'\n },\n {\n key: 'filename',\n label: 'Filename'\n },\n {\n key: 'content_type',\n label: 'Type',\n formatter: \"default('Unknown')\"\n },\n {\n key: 'file_size',\n label: 'Size',\n formatter: \"filesize\"\n },\n {\n key: 'group.name',\n label: 'Group',\n formatter: \"default('No Group')\"\n },\n {\n key: 'upload_status',\n label: 'Status',\n formatter: \"badge\"\n },\n {\n key: 'created',\n label: 'Uploaded',\n formatter: \"epoch|datetime\"\n }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No files found. Click \"Add File\" to upload your first file.',\n\n // Batch actions\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Download\", icon: \"bi bi-download\", action: \"batch-download\" },\n { label: \"Delete\", icon: \"bi bi-trash\", action: \"batch-delete\" },\n { label: \"Move to Group\", icon: \"bi bi-folder\", action: \"batch-move\" }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n },\n ...options,\n });\n\n // Enable file drop support\n this.enableFileDrop({\n acceptedTypes: ['*/*'],\n maxFileSize: 100 * 1024 * 1024, // 100MB\n multiple: false,\n validateOnDrop: true\n });\n }\n\n /**\n * Handle file upload via file picker\n * Opens native file picker and uses same upload flow as drag-drop\n */\n async handleFileUpload(event) {\n if (event) event.preventDefault();\n\n // Create hidden file input element\n const fileInput = document.createElement('input');\n fileInput.type = 'file';\n fileInput.accept = '*/*';\n fileInput.multiple = false;\n fileInput.style.display = 'none';\n\n // Handle file selection\n fileInput.addEventListener('change', async (e) => {\n const file = e.target.files[0];\n \n if (!file) {\n return;\n }\n\n // Validate file size (same as FileDropMixin config)\n const maxSize = 100 * 1024 * 1024; // 100MB\n if (file.size > maxSize) {\n this.showError(`File size (${this._formatFileSize(file.size)}) exceeds maximum (${this._formatFileSize(maxSize)})`);\n return;\n }\n\n // Use the same upload flow as drag-drop\n try {\n const fileModel = new File();\n let extra = {};\n if (this.options.requiresGroup && this.getApp().activeGroup) {\n extra.group = this.getApp().activeGroup.id;\n }\n\n const upload = fileModel.upload({\n file: file,\n name: file.name,\n description: `File uploaded on ${new Date().toLocaleDateString()}`,\n showToast: true,\n onProgress: (progressInfo) => {\n console.log(`Upload progress: ${progressInfo.percentage}%`);\n },\n onComplete: (result) => {\n console.log('Upload completed:', result);\n this.refresh();\n },\n onError: (error) => {\n console.error('Upload failed:', error);\n this.showError('Upload failed: ' + error.message);\n },\n ...extra\n });\n\n await upload;\n } catch (error) {\n console.error('Error starting file upload:', error);\n this.showError('Failed to start file upload: ' + error.message);\n } finally {\n // Clean up file input\n fileInput.remove();\n }\n });\n\n // Trigger file picker\n document.body.appendChild(fileInput);\n fileInput.click();\n }\n\n /**\n * Format file size for display\n * @param {number} bytes - File size in bytes\n * @returns {string} Formatted file size\n * @private\n */\n _formatFileSize(bytes) {\n if (bytes === 0) return '0 Bytes';\n const k = 1024;\n const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n }\n\n async onFileDrop(files, event, validation) {\n const file = files[0];\n console.log(`File Dropped: ${file.name} (${file.type}) (${file.size} bytes)`);\n\n try {\n // Create new File model instance\n const fileModel = new File();\n let extra = {};\n if (this.options.requiresGroup && this.getApp().activeGroup) {\n extra.group = this.getApp().activeGroup.id;\n }\n\n // Start upload with progress tracking\n const upload = fileModel.upload({\n file: file,\n name: file.name,\n description: `File uploaded via drag & drop on ${new Date().toLocaleDateString()}`,\n showToast: true,\n onProgress: (progressInfo) => {\n console.log(`Upload progress: ${progressInfo.percentage}%`);\n },\n onComplete: (result) => {\n console.log('Upload completed:', result);\n this.refresh();\n },\n onError: (error) => {\n console.error('Upload failed:', error);\n this.showError('Upload failed: ' + error.message);\n },\n ...extra\n });\n\n await upload;\n } catch (error) {\n console.error('Error starting file upload:', error);\n this.showError('Failed to start file upload: ' + error.message);\n }\n }\n}\n\n// Apply file drop mixin\napplyFileDropMixin(FileTablePage);\n\nexport default FileTablePage;\n","/**\n * S3BucketTablePage - S3 Bucket management using TablePage component\n * Clean implementation using TablePage with minimal overrides\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { S3BucketList, S3BucketForms } from '@ext/admin/models/AWS.js';\n\nclass S3BucketTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_s3_buckets',\n pageName: 'Manage S3 Buckets',\n router: \"admin/s3-buckets\",\n Collection: S3BucketList,\n \n formCreate: S3BucketForms.create,\n formEdit: S3BucketForms.edit,\n\n // Column definitions\n columns: [\n {\n key: 'id',\n label: 'ID',\n width: '60px',\n sortable: true,\n class: 'text-muted'\n },\n {\n key: 'name',\n label: 'Bucket Name',\n sortable: true\n },\n {\n key: 'created',\n label: 'Created',\n formatter: \"epoch|datetime\"\n }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n // Toolbar\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n // Empty state\n emptyMessage: 'No S3 buckets found. Click \"Add S3 Bucket\" to create your first bucket.',\n\n // Batch actions\n batchBarLocation: 'top',\n batchActions: [\n { label: \"Delete\", icon: \"bi bi-trash\", action: \"batch-delete\" },\n { label: \"Export\", icon: \"bi bi-download\", action: \"batch-export\" },\n { label: \"Make Public\", icon: \"bi bi-unlock\", action: \"batch-public\" },\n { label: \"Make Private\", icon: \"bi bi-lock\", action: \"batch-private\" },\n { label: \"Empty Bucket\", icon: \"bi bi-bucket\", action: \"batch-empty\" }\n ],\n\n // Table display options\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default S3BucketTablePage;","/**\n * ShortLinkView - Detail view for a ShortLink record\n *\n * Modal-hosted SideNavView with five sections:\n * Details | Preview | Metadata | Click History | Metrics\n *\n * The header surfaces the short URL with copy and open buttons. The\n * context menu offers Copy Short URL, Open Destination, Enable/Disable,\n * Edit, Delete. Mirrors the FileView / IPSetView shape.\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport FormView from '@core/forms/FormView.js';\nimport { MetricsChart } from '@ext/charts/index.js';\nimport {\n ShortLink,\n ShortLinkClickList,\n ShortLinkForms,\n SHORTLINK_SOURCE_OPTIONS,\n TWITTER_CARD_OPTIONS,\n flattenShortLinkMetadata,\n buildShortLinkMetadata,\n extractShortLinkPayload,\n} from '@core/models/ShortLink.js';\n\n// ── Helpers ────────────────────────────────────────────────\n\n/**\n * Compose the full short URL for a ShortLink. Prefers a server-supplied\n * `short_link` field; otherwise builds `{base}/s/<code>` using\n * `app.config.shortlink_base_url` or `window.location.origin`.\n */\nfunction getShortUrl(model, app) {\n const serverUrl = model?.get?.('short_link');\n if (serverUrl) return serverUrl;\n const code = model?.get?.('code');\n if (!code) return '';\n const base =\n app?.config?.shortlink_base_url ||\n (typeof window !== 'undefined' ? window.location.origin : '');\n return `${String(base).replace(/\\/+$/, '')}/s/${code}`;\n}\n\nfunction getDomain(url) {\n if (!url) return '';\n try {\n return new URL(url).hostname;\n } catch (_e) {\n return url;\n }\n}\n\nfunction isShortLinkExpired(model) {\n const raw = model?.get?.('expires_at');\n if (!raw) return false;\n const t = new Date(raw).getTime();\n return Number.isFinite(t) && t < Date.now();\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// PreviewSection — visual mock of the Slack/iMessage card built from\n// `metadata.og:*`. Re-renderable from the parent after edits land.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass ShortLinkPreviewSection extends View {\n constructor(options = {}) {\n super({ className: 'shortlink-preview-section p-3', ...options });\n this._syncFromModel();\n this.template = `\n <p class=\"text-muted small mb-3\">\n Preview of how this link appears when shared in Slack, iMessage, WhatsApp, and other platforms that render OpenGraph metadata.\n </p>\n {{#hasOg|bool}}\n <div class=\"card shadow-sm\" style=\"max-width: 540px; border-left: 4px solid #0d6efd;\">\n {{#ogImage}}<img src=\"{{ogImage}}\" class=\"card-img-top\" alt=\"OG image\" style=\"max-height: 260px; object-fit: cover;\">{{/ogImage}}\n <div class=\"card-body\">\n <div class=\"text-muted small mb-1\">{{domain}}</div>\n {{#ogTitle}}<h5 class=\"card-title mb-1\">{{ogTitle}}</h5>{{/ogTitle}}\n {{#ogDescription}}<p class=\"card-text small text-muted mb-0\">{{ogDescription}}</p>{{/ogDescription}}\n </div>\n </div>\n {{/hasOg|bool}}\n {{^hasOg|bool}}\n <div class=\"alert alert-info mb-0 small d-flex align-items-start gap-2\">\n <i class=\"bi bi-info-circle flex-shrink-0 mt-1\"></i>\n <div>\n No OG metadata set on this link. The server auto-scrapes the destination URL in the background —\n custom values entered in the <strong>Metadata</strong> section take priority over scraped values.\n </div>\n </div>\n {{/hasOg|bool}}\n `;\n }\n\n _syncFromModel() {\n const flat = flattenShortLinkMetadata(this.model?.get('metadata'));\n this.ogTitle = flat.og_title || '';\n this.ogDescription = flat.og_description || '';\n this.ogImage = flat.og_image || '';\n this.hasOg = !!(this.ogTitle || this.ogDescription || this.ogImage);\n this.domain = getDomain(this.model?.get('url'));\n }\n\n async refreshFromModel() {\n this._syncFromModel();\n if (this.isMounted?.()) await this.render();\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// MetricsSection — wraps MetricsChart, or renders an info panel when the\n// link has no metrics stream (track_clicks=false or no owning user).\n// ──────────────────────────────────────────────────────────────────────────\n\nclass ShortLinkMetricsSection extends View {\n constructor(options = {}) {\n super({ className: 'shortlink-metrics-section p-3', ...options });\n\n const trackClicks = !!this.model.get('track_clicks');\n const user = this.model.get('user');\n this.userId = user?.id || user || null;\n this.code = this.model.get('code');\n this.canShowMetrics = trackClicks && this.userId && this.code;\n\n if (this.canShowMetrics) {\n this.template = `<div data-container=\"shortlink-metrics-chart\"></div>`;\n } else {\n const reason = !trackClicks\n ? 'Click tracking is disabled — enable “Track clicks” on this shortlink to collect time-series data.'\n : 'No owning user on this shortlink — per-link metrics are recorded per user account.';\n this.template = `\n <div class=\"alert alert-info mb-0 d-flex align-items-start gap-2\">\n <i class=\"bi bi-info-circle flex-shrink-0 mt-1\"></i>\n <div>${reason}</div>\n </div>\n `;\n }\n }\n\n async onInit() {\n if (!this.canShowMetrics) return;\n this.metricsChart = new MetricsChart({\n containerId: 'shortlink-metrics-chart',\n title: 'Clicks',\n slugs: [`sl:click:${this.code}`],\n account: `user-${this.userId}`,\n granularity: 'days',\n defaultDateRange: '30d',\n yAxis: { label: 'Clicks', beginAtZero: true },\n tooltip: { y: 'number' },\n height: 320,\n });\n this.addChild(this.metricsChart);\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// MetadataSection — editable og_*/twitter_* form. Submit is intercepted\n// so the model.save() body is the nested `metadata` map (not raw fields).\n// ──────────────────────────────────────────────────────────────────────────\n\nclass ShortLinkMetadataSection extends FormView {\n constructor(options = {}) {\n const seed = flattenShortLinkMetadata(options.model?.get('metadata'));\n super({\n className: 'p-3',\n // NOTE: deliberately NOT passing `model` — FormView would otherwise\n // call model.save(rawFormData) which would persist og_title/etc as\n // model attributes. We handle save ourselves in onSubmit.\n data: seed,\n submitButtonText: 'Save Metadata',\n submitButtonClass: 'btn btn-primary',\n formConfig: {\n fields: [\n { name: 'og_title', type: 'text', label: 'og:title', cols: 12 },\n { name: 'og_description', type: 'textarea', label: 'og:description', rows: 3, cols: 12 },\n { name: 'og_image', type: 'url', label: 'og:image', placeholder: 'https://…', cols: 12 },\n { name: 'twitter_card', type: 'select', label: 'twitter:card', options: TWITTER_CARD_OPTIONS, cols: 6 },\n { name: 'twitter_title', type: 'text', label: 'twitter:title', cols: 6 },\n { name: 'twitter_description', type: 'textarea', label: 'twitter:description', rows: 2, cols: 12 },\n { name: 'twitter_image', type: 'url', label: 'twitter:image', cols: 12 },\n ],\n onSubmit: (formData) => this._saveMetadata(formData),\n },\n ...options,\n });\n this._targetModel = options.model;\n }\n\n async _saveMetadata(formData) {\n const metadata = buildShortLinkMetadata(formData);\n try {\n await this._targetModel.save({ metadata });\n this.getApp()?.toast?.success?.('Metadata saved');\n this.emit('metadata:saved', { model: this._targetModel, metadata });\n } catch (err) {\n Modal.showError(err?.data?.error || err?.message || 'Failed to save metadata');\n }\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// ShortLinkView (main component)\n// ──────────────────────────────────────────────────────────────────────────\n\nclass ShortLinkView extends View {\n constructor(options = {}) {\n super({\n className: 'shortlink-view',\n ...options,\n });\n\n this.model = options.model || new ShortLink(options.data || {});\n this.sideNavView = null;\n this.contextMenu = null;\n this.previewSection = null;\n this.metadataSection = null;\n\n this._refreshHeader();\n\n // SideNavView (like FileView) needs a bounded container height to\n // scroll its content panel properly inside a modal.\n this.template = `\n <div class=\"shortlink-view-container d-flex flex-column\" style=\"min-height: 0;\">\n <div class=\"d-flex justify-content-between align-items-start mb-3 flex-shrink-0\">\n <div class=\"d-flex align-items-start gap-3 flex-grow-1\" style=\"min-width: 0;\">\n <div class=\"fs-1 text-primary\"><i class=\"bi bi-link-45deg\"></i></div>\n <div class=\"flex-grow-1\" style=\"min-width: 0;\">\n <div class=\"input-group input-group-lg mb-2\" style=\"max-width: 620px;\">\n <input type=\"text\" class=\"form-control font-monospace\" readonly value=\"{{shortUrl}}\">\n <button class=\"btn btn-outline-primary\" data-action=\"copy-short-url\" title=\"Copy short URL\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n <a class=\"btn btn-outline-secondary\" href=\"{{shortUrl}}\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open short URL in a new tab\">\n <i class=\"bi bi-box-arrow-up-right\"></i>\n </a>\n </div>\n <div class=\"text-muted small text-truncate\" style=\"max-width: 620px;\" title=\"{{model.url}}\">\n <i class=\"bi bi-arrow-right\"></i> {{model.url}}\n </div>\n <div class=\"d-flex align-items-center gap-2 mt-2 flex-wrap\">\n <span class=\"badge {{activeBadge}}\">{{activeLabel}}</span>\n {{#model.source}}<span class=\"badge bg-secondary\">{{model.source}}</span>{{/model.source}}\n {{#hasHits|bool}}<span class=\"badge bg-light text-dark border\">{{model.hit_count}} hits</span>{{/hasHits|bool}}\n {{#isExpired|bool}}<span class=\"badge bg-danger\">Expired</span>{{/isExpired|bool}}\n {{#model.track_clicks}}<span class=\"badge bg-info text-dark\">Tracked</span>{{/model.track_clicks}}\n </div>\n </div>\n </div>\n <div data-container=\"shortlink-context-menu\" class=\"ms-3 flex-shrink-0\"></div>\n </div>\n <div data-container=\"shortlink-sidenav\" class=\"flex-grow-1\" style=\"min-height: 400px; max-height: 70vh;\"></div>\n </div>\n `;\n }\n\n _refreshHeader() {\n const app = this.getApp?.();\n this.shortUrl = getShortUrl(this.model, app);\n this.isActive = !!this.model.get('is_active');\n this.activeLabel = this.isActive ? 'Active' : 'Disabled';\n this.activeBadge = this.isActive ? 'bg-success' : 'bg-secondary';\n this.isExpired = isShortLinkExpired(this.model);\n this.hasHits = (this.model.get('hit_count') || 0) > 0;\n }\n\n async onInit() {\n await this._buildSideNav();\n await this._buildContextMenu();\n }\n\n async _buildSideNav() {\n // ── Details ──\n const sourceVal = this.model.get('source') || '';\n const sourceOpt = SHORTLINK_SOURCE_OPTIONS.find((o) => o.value === sourceVal);\n const sourceLabel = sourceOpt ? sourceOpt.label : (sourceVal || '—');\n\n const detailsView = new DataView({\n model: this.model,\n className: 'p-3',\n columns: 2,\n showEmptyValues: true,\n emptyValueText: '—',\n fields: [\n { name: 'code', label: 'Code', template: `<code>${this.model.get('code') || '—'}</code>`, colSize: 4 },\n { name: 'source', label: 'Source', template: sourceLabel, colSize: 4 },\n { name: 'is_active', label: 'Active', type: 'boolean', colSize: 4 },\n { name: 'url', label: 'Destination', type: 'url', colSize: 12 },\n { name: 'hit_count', label: 'Hits', colSize: 3 },\n { name: 'track_clicks', label: 'Track clicks', type: 'boolean', colSize: 3 },\n { name: 'bot_passthrough', label: 'Bot passthrough', type: 'boolean', colSize: 3 },\n { name: 'is_protected', label: 'Protected', type: 'boolean', colSize: 3 },\n { name: 'expires_at', label: 'Expires', format: 'datetime', colSize: 6 },\n { name: 'created', label: 'Created', format: 'datetime', colSize: 6 },\n { name: 'modified', label: 'Modified', format: 'datetime', colSize: 6 },\n { name: 'user.username', label: 'Owner', colSize: 6 },\n { name: 'group.name', label: 'Group', colSize: 12 },\n ],\n });\n\n // ── Preview ──\n this.previewSection = new ShortLinkPreviewSection({ model: this.model });\n\n // ── Metadata (intercepts submit, saves only `metadata` map) ──\n this.metadataSection = new ShortLinkMetadataSection({ model: this.model });\n this.metadataSection.on('metadata:saved', () => {\n this.previewSection?.refreshFromModel?.();\n });\n\n // ── Click History ──\n const clickCollection = new ShortLinkClickList({\n params: { shortlink: this.model.get('id'), sort: '-created', size: 25 },\n });\n const clicksTable = new TableView({\n collection: clickCollection,\n className: 'p-2',\n hideActivePillNames: ['shortlink'],\n columns: [\n { key: 'created', label: 'Time', width: '180px', formatter: 'datetime', sortable: true },\n { key: 'ip', label: 'IP', width: '140px', template: '<code>{{model.ip}}</code>' },\n {\n key: 'is_bot',\n label: 'Bot',\n width: '80px',\n formatter: 'yesnoicon',\n filter: {\n type: 'select',\n options: [\n { value: 'true', label: 'Bots only' },\n { value: 'false', label: 'Humans only' },\n ],\n },\n },\n { key: 'user_agent', label: 'User-Agent', formatter: 'truncate(60)' },\n { key: 'referer', label: 'Referer', formatter: \"truncate(40)|default('—')\" },\n ],\n searchable: false,\n sortable: true,\n filterable: true,\n paginated: true,\n tableOptions: {\n hover: true,\n size: 'sm',\n emptyMessage: this.model.get('track_clicks')\n ? 'No clicks recorded yet.'\n : 'Click tracking is disabled for this shortlink.',\n emptyIcon: 'bi-cursor',\n actions: [],\n },\n });\n\n // ── Metrics ──\n const metricsSection = new ShortLinkMetricsSection({ model: this.model });\n\n // ── Assemble SideNavView ──\n this.sideNavView = new SideNavView({\n containerId: 'shortlink-sidenav',\n activeSection: 'details',\n navWidth: 200,\n contentPadding: '1.25rem 1.5rem',\n enableResponsive: true,\n minWidth: 600,\n sections: [\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView },\n { key: 'preview', label: 'Preview', icon: 'bi-card-image', view: this.previewSection },\n { key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: this.metadataSection },\n { key: 'clicks', label: 'Click History', icon: 'bi-cursor', view: clicksTable },\n { key: 'metrics', label: 'Metrics', icon: 'bi-bar-chart-line', view: metricsSection },\n ],\n });\n this.addChild(this.sideNavView);\n }\n\n async _buildContextMenu() {\n this.contextMenu = new ContextMenu({\n containerId: 'shortlink-context-menu',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'Copy Short URL', action: 'copy-short-url', icon: 'bi-clipboard' },\n { label: 'Open Destination', action: 'open-destination', icon: 'bi-box-arrow-up-right' },\n { type: 'divider' },\n this.isActive\n ? { label: 'Disable', action: 'disable-shortlink', icon: 'bi-toggle-off' }\n : { label: 'Enable', action: 'enable-shortlink', icon: 'bi-toggle-on' },\n { label: 'Edit Shortlink', action: 'edit-shortlink', icon: 'bi-pencil' },\n { type: 'divider' },\n { label: 'Delete Shortlink', action: 'delete-shortlink', icon: 'bi-trash', danger: true },\n ],\n },\n });\n this.addChild(this.contextMenu);\n }\n\n // Section views are mounted under SideNavView, so model `change` events\n // re-rendering the parent would unmount/remount everything. Sections\n // manage their own reactivity (DataView listens to model directly,\n // PreviewSection has refreshFromModel). Same pattern as FileView.\n _onModelChange() {\n // no-op\n }\n\n // ── Actions ────────────────────────────────────\n\n async onActionCopyShortUrl() {\n if (!this.shortUrl) return;\n try {\n await navigator.clipboard.writeText(this.shortUrl);\n this.getApp()?.toast?.success(`Copied: ${this.shortUrl}`);\n } catch (_e) {\n this.getApp()?.toast?.warning('Copy failed — select the URL manually.');\n }\n }\n\n async onActionOpenDestination() {\n const url = this.model.get('url');\n if (!url || !/^https?:\\/\\//i.test(url)) return;\n window.open(url, '_blank', 'noopener,noreferrer');\n }\n\n async onActionEnableShortlink() {\n try {\n await this.model.save({ is_active: true });\n this.getApp()?.toast?.success('Shortlink enabled');\n this._refreshHeader();\n await this.render();\n } catch (err) {\n Modal.showError(err?.data?.error || err?.message || 'Failed to enable');\n }\n }\n\n async onActionDisableShortlink() {\n const confirmed = await Modal.confirm(\n 'Disable this shortlink? Visitors will be redirected to the fallback URL.',\n 'Disable Shortlink',\n );\n if (!confirmed) return;\n try {\n await this.model.save({ is_active: false });\n this.getApp()?.toast?.success('Shortlink disabled');\n this._refreshHeader();\n await this.render();\n } catch (err) {\n Modal.showError(err?.data?.error || err?.message || 'Failed to disable');\n }\n }\n\n async onActionEditShortlink() {\n const seed = {\n ...this.model.toJSON(),\n ...flattenShortLinkMetadata(this.model.get('metadata')),\n };\n const result = await Modal.form({\n ...ShortLinkForms.edit,\n data: seed,\n });\n if (!result) return;\n const payload = extractShortLinkPayload(result);\n try {\n await this.model.save(payload);\n this.getApp()?.toast?.success('Shortlink updated');\n this._refreshHeader();\n await this.render();\n await this.previewSection?.refreshFromModel?.();\n } catch (err) {\n Modal.showError(err?.data?.error || err?.message || 'Failed to update');\n }\n }\n\n async onActionDeleteShortlink() {\n const confirmed = await Modal.confirm(\n `Delete shortlink \"${this.model.get('code')}\"? This cannot be undone.`,\n 'Delete Shortlink',\n { confirmText: 'Delete', confirmClass: 'btn-danger' },\n );\n if (!confirmed) return;\n\n try {\n await this.model.destroy();\n this.getApp()?.toast?.success('Shortlink deleted');\n this.emit('shortlink:deleted', { model: this.model });\n\n const dialog = this.element?.closest?.('.modal');\n if (dialog) {\n const bsModal = window.bootstrap?.Modal?.getInstance?.(dialog);\n if (bsModal) bsModal.hide();\n }\n } catch (err) {\n Modal.showError(err?.data?.error || err?.message || 'Failed to delete');\n }\n }\n\n async showSection(name) {\n if (this.sideNavView) {\n await this.sideNavView.showSection(name);\n }\n }\n\n getActiveSection() {\n return this.sideNavView ? this.sideNavView.getActiveSection() : null;\n }\n}\n\nShortLink.VIEW_CLASS = ShortLinkView;\n\nexport default ShortLinkView;\nexport { getShortUrl };\n","/**\n * ShortLinkTablePage - Manage application shortlinks (/s/<code>)\n *\n * Lists all shortlinks with CRUD, filtering, and search. The Add/Edit dialogs\n * expose OG metadata fields (og:title, og:description, og:image) as flat form\n * inputs; a custom submit handler collapses them into the REST API's\n * metadata map.\n *\n * Route: system/shortlinks/links\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport {\n ShortLink,\n ShortLinkList,\n ShortLinkForms,\n SHORTLINK_SOURCE_OPTIONS,\n flattenShortLinkMetadata,\n extractShortLinkPayload,\n} from '@core/models/ShortLink.js';\nimport ShortLinkView, { getShortUrl } from './ShortLinkView.js';\n\nclass ShortLinkTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_shortlinks',\n pageName: 'Shortlinks',\n router: 'admin/shortlinks/links',\n Collection: ShortLinkList,\n itemViewClass: ShortLinkView,\n onAdd: () => this._handleAdd(),\n onItemEdit: (model) => this._handleEdit(model),\n\n viewDialogOptions: {\n header: false,\n size: 'xl',\n },\n\n defaultQuery: {\n sort: '-created',\n },\n\n columns: [\n {\n key: 'is_active',\n label: 'Active',\n width: '70px',\n sortable: true,\n formatter: 'yesnoicon',\n filter: {\n type: 'select',\n options: [\n { value: 'true', label: 'Active' },\n { value: 'false', label: 'Disabled' },\n ],\n },\n },\n {\n key: 'code',\n label: 'Code',\n sortable: true,\n template: `\n <div class=\"d-flex align-items-center gap-2\">\n <code>{{model.code}}</code>\n <button class=\"btn btn-sm btn-link p-0 text-muted\"\n data-action=\"copy-code\"\n data-code=\"{{model.code}}\"\n title=\"Copy short URL\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n </div>\n `,\n },\n {\n key: 'url',\n label: 'Destination',\n sortable: true,\n formatter: \"truncate(60)|default('—')\",\n },\n {\n key: 'source',\n label: 'Source',\n width: '110px',\n sortable: true,\n filter: {\n type: 'select',\n options: SHORTLINK_SOURCE_OPTIONS,\n },\n },\n {\n key: 'hit_count',\n label: 'Hits',\n width: '80px',\n sortable: true,\n },\n {\n key: 'track_clicks',\n label: 'Tracked',\n width: '90px',\n formatter: 'yesnoicon',\n filter: {\n type: 'select',\n options: [\n { value: 'true', label: 'Tracked' },\n { value: 'false', label: 'Not tracked' },\n ],\n },\n },\n {\n key: 'expires_at',\n label: 'Expires',\n width: '160px',\n sortable: true,\n formatter: \"datetime|default('Never')\",\n },\n {\n key: 'created',\n label: 'Created',\n width: '160px',\n sortable: true,\n formatter: 'datetime',\n visibility: 'lg',\n },\n ],\n\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n showRefresh: true,\n showAdd: true,\n showExport: true,\n\n emptyMessage: 'No shortlinks yet — create one to share a link with a rich preview card.',\n\n batchBarLocation: 'top',\n batchActions: [\n { label: 'Enable', icon: 'bi bi-toggle-on', action: 'enable' },\n { label: 'Disable', icon: 'bi bi-toggle-off', action: 'disable' },\n { label: 'Delete', icon: 'bi bi-trash', action: 'delete', danger: true },\n ],\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false,\n actions: ['edit', 'delete'],\n pageSizes: [25, 50, 100],\n defaultPageSize: 25,\n emptyIcon: 'bi-link-45deg',\n },\n });\n }\n\n // ── Add/Edit handlers (flatten/unflatten metadata) ──\n\n async _handleAdd() {\n const result = await Modal.form({ ...ShortLinkForms.create });\n if (!result) return;\n\n const payload = extractShortLinkPayload(result);\n try {\n const model = new ShortLink();\n await model.save(payload);\n const shortUrl = getShortUrl(model, this.getApp());\n try {\n await navigator.clipboard?.writeText?.(shortUrl);\n this.getApp()?.toast?.success(`Shortlink created — ${shortUrl} copied to clipboard`);\n } catch (_e) {\n this.getApp()?.toast?.success(`Shortlink created: ${shortUrl}`);\n }\n this.tableView?.collection?.fetch();\n } catch (err) {\n Modal.showError(err?.data?.error || err?.message || 'Failed to create shortlink');\n }\n }\n\n async _handleEdit(model) {\n if (!model) return;\n const seed = {\n ...model.toJSON(),\n ...flattenShortLinkMetadata(model.get('metadata')),\n };\n const result = await Modal.form({\n ...ShortLinkForms.edit,\n data: seed,\n });\n if (!result) return;\n\n const payload = extractShortLinkPayload(result);\n try {\n await model.save(payload);\n this.getApp()?.toast?.success('Shortlink updated');\n this.tableView?.collection?.fetch();\n } catch (err) {\n Modal.showError(err?.data?.error || err?.message || 'Failed to update shortlink');\n }\n }\n\n // ── Row actions ──\n\n async onActionCopyCode(event, element) {\n event?.stopPropagation?.();\n const code = element?.dataset?.code;\n if (!code) return;\n const fakeModel = { get: (k) => (k === 'code' ? code : null) };\n const url = getShortUrl(fakeModel, this.getApp());\n try {\n await navigator.clipboard.writeText(url);\n this.getApp()?.toast?.success(`Copied: ${url}`);\n } catch (_e) {\n this.getApp()?.toast?.warning('Copy failed — select the URL manually.');\n }\n }\n\n // ── Batch actions ──\n\n async onActionBatchEnable() {\n const selected = this.tableView?.getSelectedItems?.() || [];\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Enable ${selected.length} shortlink(s)?`);\n if (!confirmed) return;\n await Promise.all(selected.map((item) => item.model.save({ is_active: true })));\n this.getApp().toast.success(`${selected.length} shortlink(s) enabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDisable() {\n const selected = this.tableView?.getSelectedItems?.() || [];\n if (!selected.length) return;\n const confirmed = await this.getApp().confirm(`Disable ${selected.length} shortlink(s)?`);\n if (!confirmed) return;\n await Promise.all(selected.map((item) => item.model.save({ is_active: false })));\n this.getApp().toast.success(`${selected.length} shortlink(s) disabled`);\n this.tableView.collection.fetch();\n }\n\n async onActionBatchDelete() {\n const selected = this.tableView?.getSelectedItems?.() || [];\n if (!selected.length) return;\n const confirmed = await Modal.confirm(\n `Delete ${selected.length} shortlink(s)? This cannot be undone.`,\n 'Delete Shortlinks',\n { confirmText: 'Delete', confirmClass: 'btn-danger' },\n );\n if (!confirmed) return;\n await Promise.all(selected.map((item) => item.model.destroy()));\n this.getApp().toast.success(`${selected.length} shortlink(s) deleted`);\n this.tableView.collection.fetch();\n }\n}\n\nexport default ShortLinkTablePage;\n","/**\n * ShortLinkClickTablePage - Global shortlink click history\n *\n * Read-only table of every recorded click across all tracked shortlinks.\n * Filterable by shortlink id via URL param (?shortlink=<id>).\n *\n * Route: system/shortlinks/clicks\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { ShortLinkClickList } from '@core/models/ShortLink.js';\n\nclass ShortLinkClickTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_shortlink_clicks',\n pageName: 'Shortlink Click History',\n router: 'admin/shortlinks/clicks',\n Collection: ShortLinkClickList,\n\n defaultQuery: {\n sort: '-created',\n },\n\n columns: [\n {\n key: 'created',\n label: 'Time',\n width: '180px',\n sortable: true,\n formatter: 'datetime',\n },\n {\n key: 'shortlink.code',\n label: 'Code',\n width: '130px',\n template: '<code>{{model.shortlink.code}}</code>',\n },\n {\n key: 'shortlink.url',\n label: 'Destination',\n formatter: \"truncate(50)|default('—')\",\n },\n {\n key: 'ip',\n label: 'IP',\n width: '140px',\n template: '<code>{{model.ip}}</code>',\n },\n {\n key: 'is_bot',\n label: 'Bot',\n width: '80px',\n formatter: 'yesnoicon',\n filter: {\n type: 'select',\n options: [\n { value: 'true', label: 'Bots only' },\n { value: 'false', label: 'Humans only' },\n ],\n },\n },\n {\n key: 'user_agent',\n label: 'User-Agent',\n formatter: 'truncate(50)',\n visibility: 'md',\n },\n {\n key: 'referer',\n label: 'Referer',\n formatter: \"truncate(40)|default('—')\",\n visibility: 'lg',\n },\n ],\n\n searchable: false,\n sortable: true,\n filterable: true,\n paginated: true,\n selectable: false,\n showRefresh: true,\n showAdd: false,\n showExport: true,\n\n emptyMessage: 'No click history recorded. Clicks are only captured on shortlinks created with track_clicks=true.',\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false,\n actions: [],\n pageSizes: [25, 50, 100],\n defaultPageSize: 50,\n emptyIcon: 'bi-cursor',\n },\n });\n }\n}\n\nexport default ShortLinkClickTablePage;\n","/**\n * UserDeviceLocationView - Clean, modern session location detail view\n *\n * Shows a device-location record with:\n * - Header: device icon, browser/device summary, location, IP, threat badges\n * - SideNavView: Location, Device, Network & Risk, Map (if coords), Events\n *\n * Opened when clicking a location row in admin UserView or DeviceView.\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport { UserDeviceLocation } from '@core/models/User.js';\nimport { IncidentEventList } from '@ext/admin/models/Incident.js';\nimport { LogList } from '@core/models/Log.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass UserDeviceLocationView extends View {\n constructor(options = {}) {\n super({\n className: 'udl-view',\n ...options\n });\n\n this.model = options.model || new UserDeviceLocation(options.data || {});\n\n // Extract nested data\n this._ud = this.model.get('user_device') || {};\n this._di = this._ud.device_info || {};\n this._geo = this.model.get('geolocation') || {};\n\n // Computed for template\n this.deviceIcon = this._getDeviceIcon();\n this.browserFull = this._getBrowser();\n this.osFull = this._getOS();\n this.deviceFull = this._getDevice();\n this.locationSummary = this._getLocationSummary();\n this.countryFlag = this._geo.country_code || '';\n this.threatLevel = this._geo.threat_level || 'unknown';\n this.threatColor = this._getThreatColor();\n this.hasCoordinates = !!(this._geo.latitude && this._geo.longitude);\n\n this.template = `\n <style>\n .udl-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; }\n .udl-identity { display: flex; align-items: center; gap: 1rem; }\n .udl-icon-wrap { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.4rem; flex-shrink: 0; }\n .udl-title { font-size: 1.15rem; font-weight: 600; margin: 0; line-height: 1.3; }\n .udl-subtitle { font-size: 0.8rem; color: #6c757d; margin-top: 0.15rem; }\n .udl-right { display: flex; align-items: flex-start; gap: 0.75rem; }\n .udl-threat-label { font-size: 0.7rem; color: #adb5bd; text-transform: uppercase; letter-spacing: 0.04em; }\n .udl-threat-value { font-size: 1rem; font-weight: 600; }\n .udl-threat-flags { display: flex; gap: 0.35rem; margin-top: 0.35rem; }\n .udl-flag { font-size: 0.65rem; padding: 0.15em 0.45em; border-radius: 3px; font-weight: 600; }\n </style>\n\n <div class=\"udl-header\">\n <div class=\"udl-identity\">\n <div class=\"udl-icon-wrap bg-primary bg-opacity-10 text-primary\">\n <i class=\"bi {{deviceIcon}}\"></i>\n </div>\n <div>\n <h4 class=\"udl-title\">{{locationSummary}}</h4>\n <div class=\"udl-subtitle\">\n {{browserFull}} <span class=\"text-muted\">on</span> {{deviceFull}}\n <span class=\"text-muted mx-1\">·</span>\n {{model.ip_address}}\n </div>\n </div>\n </div>\n <div class=\"udl-right\">\n <div class=\"text-end\">\n <div class=\"udl-threat-label\">Threat Level</div>\n <div class=\"udl-threat-value {{threatColor}}\">{{threatLevel|capitalize}}</div>\n <div class=\"udl-threat-flags\">\n {{#_geo.is_vpn}}<span class=\"udl-flag bg-warning text-dark\">VPN</span>{{/_geo.is_vpn}}\n {{#_geo.is_tor}}<span class=\"udl-flag bg-danger text-white\">Tor</span>{{/_geo.is_tor}}\n {{#_geo.is_proxy}}<span class=\"udl-flag bg-warning text-dark\">Proxy</span>{{/_geo.is_proxy}}\n {{#_geo.is_datacenter}}<span class=\"udl-flag bg-secondary text-white\">DC</span>{{/_geo.is_datacenter}}\n {{#_geo.is_cloud}}<span class=\"udl-flag bg-info text-white\">Cloud</span>{{/_geo.is_cloud}}\n </div>\n </div>\n <div data-container=\"udl-context-menu\"></div>\n </div>\n </div>\n\n <div data-container=\"udl-sidenav\" style=\"min-height: 300px;\"></div>\n `;\n }\n\n // ── Computed helpers ─────────────────────────\n\n _getDeviceIcon() {\n const browser = this._di?.user_agent?.family?.toLowerCase() || '';\n const os = this._di?.os?.family?.toLowerCase() || '';\n const device = this._di?.device?.family?.toLowerCase() || '';\n if (browser.includes('chrome')) return 'bi-browser-chrome';\n if (browser.includes('firefox')) return 'bi-browser-firefox';\n if (browser.includes('safari')) return 'bi-browser-safari';\n if (browser.includes('edge')) return 'bi-browser-edge';\n if (os.includes('mac') || os.includes('ios')) return 'bi-apple';\n if (os.includes('windows')) return 'bi-windows';\n if (os.includes('android')) return 'bi-android2';\n if (device.includes('iphone')) return 'bi-phone';\n if (device.includes('ipad')) return 'bi-tablet';\n return 'bi-geo-alt';\n }\n\n _getBrowser() {\n const ua = this._di?.user_agent || {};\n return ua.family ? `${ua.family} ${ua.major || ''}`.trim() : 'Unknown Browser';\n }\n\n _getOS() {\n const os = this._di?.os || {};\n const ver = [os.major, os.minor].filter(Boolean).join('.');\n return os.family ? `${os.family} ${ver}`.trim() : 'Unknown OS';\n }\n\n _getDevice() {\n const dev = this._di?.device || {};\n const parts = [dev.brand, dev.family].filter(Boolean);\n return parts.length ? parts.join(' ') : 'Unknown Device';\n }\n\n _getLocationSummary() {\n const parts = [this._geo.city, this._geo.region, this._geo.country_name].filter(Boolean);\n return parts.length ? parts.join(', ') : 'Unknown Location';\n }\n\n _getThreatColor() {\n const level = (this._geo.threat_level || '').toLowerCase();\n if (level === 'high' || this._geo.is_threat) return 'text-danger';\n if (level === 'medium' || this._geo.is_suspicious) return 'text-warning';\n if (level === 'low') return 'text-success';\n return 'text-muted';\n }\n\n async onInit() {\n const geo = this._geo;\n const di = this._di;\n\n // ── Location section ────────────────────────\n const locationView = new View({\n model: this.model,\n template: `\n <style>\n .udl-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .udl-section-label:first-child { margin-top: 0; }\n .udl-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .udl-field-row:last-child { border-bottom: none; }\n .udl-field-label { width: 130px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .udl-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n </style>\n\n <div class=\"udl-section-label\">Geography</div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">City</div>\n <div class=\"udl-field-value\">${geo.city || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Region</div>\n <div class=\"udl-field-value\">${geo.region || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Country</div>\n <div class=\"udl-field-value\">${geo.country_name || '—'} ${geo.country_code ? `<span class=\"text-muted\">(${geo.country_code})</span>` : ''}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Postal Code</div>\n <div class=\"udl-field-value\">${geo.postal_code || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Timezone</div>\n <div class=\"udl-field-value\">${geo.timezone || '—'}</div>\n </div>\n ${geo.latitude ? `\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Coordinates</div>\n <div class=\"udl-field-value\">${geo.latitude}, ${geo.longitude}</div>\n </div>` : ''}\n\n <div class=\"udl-section-label\">Network</div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">IP Address</div>\n <div class=\"udl-field-value\" style=\"font-family: ui-monospace, monospace; font-size: 0.82rem;\">{{model.ip_address}}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">ISP</div>\n <div class=\"udl-field-value\">${geo.isp || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">ASN</div>\n <div class=\"udl-field-value\">${geo.asn || '—'} ${geo.asn_org ? `<span class=\"text-muted small\">(${geo.asn_org})</span>` : ''}</div>\n </div>\n\n <div class=\"udl-section-label\">Timestamps</div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">First Seen</div>\n <div class=\"udl-field-value\">{{model.first_seen|epoch|datetime|default('—')}}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Last Seen</div>\n <div class=\"udl-field-value\">{{model.last_seen|epoch|datetime|default('—')}}</div>\n </div>\n `\n });\n\n // ── Device section ──────────────────────────\n const deviceView = new View({\n model: this.model,\n template: `\n <style>\n .udl-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .udl-section-label:first-child { margin-top: 0; }\n .udl-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .udl-field-row:last-child { border-bottom: none; }\n .udl-field-label { width: 130px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .udl-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .udl-ua-string { font-family: ui-monospace, monospace; font-size: 0.73rem; color: #6c757d; word-break: break-all; line-height: 1.5; padding: 0.5rem 0.75rem; background: #f8f9fa; border-radius: 6px; margin-top: 0.25rem; }\n </style>\n\n <div class=\"udl-section-label\">Browser</div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Name</div>\n <div class=\"udl-field-value\">${di?.user_agent?.family || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Version</div>\n <div class=\"udl-field-value\">${[di?.user_agent?.major, di?.user_agent?.minor, di?.user_agent?.patch].filter(Boolean).join('.') || '—'}</div>\n </div>\n\n <div class=\"udl-section-label\">Operating System</div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Name</div>\n <div class=\"udl-field-value\">${di?.os?.family || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Version</div>\n <div class=\"udl-field-value\">${[di?.os?.major, di?.os?.minor, di?.os?.patch].filter(Boolean).join('.') || '—'}</div>\n </div>\n\n <div class=\"udl-section-label\">Hardware</div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Brand</div>\n <div class=\"udl-field-value\">${di?.device?.brand || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Family</div>\n <div class=\"udl-field-value\">${di?.device?.family || '—'}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Model</div>\n <div class=\"udl-field-value\">${di?.device?.model || '—'}</div>\n </div>\n\n ${di?.string ? `\n <div class=\"udl-section-label\">User Agent String</div>\n <div class=\"udl-ua-string\">${di.string}</div>` : ''}\n `\n });\n\n // ── Risk & Reputation section ───────────────\n const riskView = new View({\n model: this.model,\n template: `\n <style>\n .udl-section-label { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #adb5bd; margin-bottom: 0.5rem; margin-top: 1.5rem; }\n .udl-section-label:first-child { margin-top: 0; }\n .udl-field-row { display: flex; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n .udl-field-row:last-child { border-bottom: none; }\n .udl-field-label { width: 130px; font-size: 0.78rem; color: #6c757d; flex-shrink: 0; }\n .udl-field-value { flex: 1; font-size: 0.88rem; color: #212529; }\n .udl-risk-icon { font-size: 0.85rem; margin-right: 0.35rem; }\n .udl-risk-yes { color: #dc3545; }\n .udl-risk-no { color: #adb5bd; }\n </style>\n\n <div class=\"udl-section-label\">Threat Assessment</div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Threat Level</div>\n <div class=\"udl-field-value ${this.threatColor}\" style=\"font-weight: 600;\">${(geo.threat_level || 'Unknown')}</div>\n </div>\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\">Risk Score</div>\n <div class=\"udl-field-value\">${geo.risk_score != null ? geo.risk_score : '—'}</div>\n </div>\n\n <div class=\"udl-section-label\">Detection Flags</div>\n ${this._riskRow('VPN', 'bi-shield', geo.is_vpn)}\n ${this._riskRow('Tor Exit Node', 'bi-shield-lock', geo.is_tor)}\n ${this._riskRow('Proxy', 'bi-diagram-3', geo.is_proxy)}\n ${this._riskRow('Cloud Provider', 'bi-cloud', geo.is_cloud)}\n ${this._riskRow('Datacenter', 'bi-hdd-stack', geo.is_datacenter)}\n ${this._riskRow('Mobile', 'bi-phone', geo.is_mobile)}\n\n <div class=\"udl-section-label\">Reputation</div>\n ${this._riskRow('Known Attacker', 'bi-exclamation-triangle', geo.is_known_attacker)}\n ${this._riskRow('Known Abuser', 'bi-flag', geo.is_known_abuser)}\n ${this._riskRow('Threat', 'bi-shield-exclamation', geo.is_threat)}\n ${this._riskRow('Suspicious', 'bi-question-circle', geo.is_suspicious)}\n `\n });\n\n // ── Build sections array ────────────────────\n const sections = [\n { key: 'location', label: 'Location', icon: 'bi-geo-alt', view: locationView },\n { key: 'device', label: 'Device', icon: 'bi-laptop', view: deviceView },\n { key: 'risk', label: 'Risk', icon: 'bi-shield-exclamation', view: riskView }\n ];\n\n // Map (if coordinates)\n if (this.hasCoordinates) {\n try {\n const MapView = (await import('@ext/map/MapView.js')).default;\n const mapView = new MapView({\n markers: [{\n lat: this._geo.latitude,\n lng: this._geo.longitude,\n popup: `<strong>${this.model.get('ip_address')}</strong><br>${this.locationSummary}`\n }],\n tileLayer: 'light',\n zoom: 6,\n height: 400\n });\n sections.push({ key: 'map', label: 'Map', icon: 'bi-map', view: mapView });\n } catch (e) {\n // MapView extension not available — skip\n }\n }\n\n // Events (by IP)\n const ip = this.model.get('ip_address');\n if (ip) {\n const eventsView = new TableView({\n collection: new IncidentEventList({\n params: { size: 10, source_ip: ip }\n }),\n hideActivePillNames: ['source_ip'],\n columns: [\n { key: 'created', label: 'Date', formatter: 'datetime', sortable: true, width: '150px' },\n { key: 'category|badge', label: 'Category' },\n { key: 'title', label: 'Title' }\n ]\n });\n sections.push({ type: 'divider', label: 'Activity' });\n sections.push({ key: 'events', label: 'Events', icon: 'bi-calendar-event', view: eventsView });\n\n const logsView = new TableView({\n collection: new LogList({\n params: { size: 10, ip }\n }),\n permissions: 'view_logs',\n hideActivePillNames: ['ip'],\n columns: [\n { key: 'created', label: 'Timestamp', sortable: true, formatter: 'epoch|datetime' },\n { key: 'level', label: 'Level', sortable: true },\n { key: 'kind', label: 'Kind' },\n { name: 'log', label: 'Log' }\n ]\n });\n sections.push({ key: 'logs', label: 'Logs', icon: 'bi-journal-text', view: logsView, permissions: 'view_logs' });\n }\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'udl-sidenav',\n activeSection: this.hasCoordinates ? 'map' : 'location',\n navWidth: 160,\n contentPadding: '1rem 1.5rem',\n enableResponsive: true,\n minWidth: 450,\n sections\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n const menu = new ContextMenu({\n containerId: 'udl-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n ...(this._ud?.user ? [{ label: 'View User', action: 'view-user', icon: 'bi-person' }] : []),\n ...(this._ud?.id ? [{ label: 'View Device', action: 'view-device', icon: 'bi-laptop' }] : []),\n ...(this.hasCoordinates ? [{\n label: 'Open in Maps',\n action: 'open-in-maps',\n icon: 'bi-box-arrow-up-right'\n }] : []),\n { type: 'divider' },\n { label: 'Delete Record', action: 'delete-record', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(menu);\n }\n\n // ── Helpers ──────────────────────────────────\n\n _riskRow(label, icon, value) {\n const cls = value ? 'udl-risk-yes' : 'udl-risk-no';\n const indicator = value\n ? '<i class=\"bi bi-check-circle-fill udl-risk-icon udl-risk-yes\"></i>Yes'\n : '<i class=\"bi bi-dash-circle udl-risk-icon udl-risk-no\"></i>No';\n return `\n <div class=\"udl-field-row\">\n <div class=\"udl-field-label\"><i class=\"bi ${icon} me-1 ${cls}\"></i>${label}</div>\n <div class=\"udl-field-value\">${indicator}</div>\n </div>`;\n }\n\n // ── Actions ──────────────────────────────────\n\n async onActionViewUser() {\n const userId = this._ud?.user?.id || this._ud?.user;\n if (userId) this.emit('view-user', { userId });\n }\n\n async onActionViewDevice() {\n const deviceId = this._ud?.id;\n if (deviceId) this.emit('view-device', { deviceId });\n }\n\n async onActionOpenInMaps() {\n if (this.hasCoordinates) {\n window.open(`https://www.google.com/maps/search/?api=1&query=${this._geo.latitude},${this._geo.longitude}`, '_blank');\n }\n }\n\n async onActionDeleteRecord() {\n const confirmed = await Modal.confirm(\n 'Are you sure you want to delete this location record?',\n 'Delete Location Record'\n );\n if (!confirmed) return true;\n\n const resp = await this.model.destroy();\n if (resp.success) {\n this.emit('location:deleted', { model: this.model });\n }\n return true;\n }\n\n static async show(id) {\n const model = new UserDeviceLocation({ id });\n await model.fetch();\n if (model.id) {\n return Modal.dialog({\n title: false,\n size: 'lg',\n body: new UserDeviceLocationView({ model }),\n buttons: [{ text: 'Close', class: 'btn-secondary', dismiss: true }]\n });\n }\n Modal.alert({ message: `Could not find location record: ${id}`, type: 'warning' });\n return null;\n }\n}\n\nUserDeviceLocation.VIEW_CLASS = UserDeviceLocationView;\nexport default UserDeviceLocationView;\n","/**\n * CloudWatchResourceView - Detailed metric view for a single AWS resource\n *\n * Shows all available metric categories for an EC2, RDS, or Redis resource\n * using CloudWatchChart (MetricsChart) instances in a responsive grid.\n * Each chart has its own granularity/date controls.\n * Can be opened in a Dialog via the static show() method.\n */\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport CloudWatchChart from './CloudWatchChart.js';\n\nconst METRIC_CATEGORIES = {\n ec2: [\n { key: 'cpu', label: 'CPU Utilization', unit: '%' },\n { key: 'memory', label: 'Memory Usage', unit: '%' },\n { key: 'disk', label: 'Disk Usage', unit: '%' },\n { key: 'net_in', label: 'Network In', unit: 'bytes' },\n { key: 'net_out', label: 'Network Out', unit: 'bytes' },\n { key: 'disk_read', label: 'Disk Read Ops', unit: 'ops' },\n { key: 'disk_write', label: 'Disk Write Ops', unit: 'ops' },\n { key: 'status_check', label: 'Status Check', unit: '' }\n ],\n rds: [\n { key: 'cpu', label: 'CPU Utilization', unit: '%' },\n { key: 'conns', label: 'Active Connections', unit: '' },\n { key: 'free_storage', label: 'Free Storage', unit: 'bytes' },\n { key: 'free_memory', label: 'Freeable Memory', unit: 'bytes' },\n { key: 'read_iops', label: 'Read IOPS', unit: 'ops/s' },\n { key: 'write_iops', label: 'Write IOPS', unit: 'ops/s' },\n { key: 'read_latency', label: 'Read Latency', unit: 's' },\n { key: 'write_latency', label: 'Write Latency', unit: 's' },\n { key: 'net_in', label: 'Network In', unit: 'bytes' },\n { key: 'net_out', label: 'Network Out', unit: 'bytes' }\n ],\n redis: [\n { key: 'cpu', label: 'CPU Utilization', unit: '%' },\n { key: 'conns', label: 'Current Connections', unit: '' },\n { key: 'cache_memory', label: 'Cache Memory Used', unit: 'bytes' },\n { key: 'cache_hits', label: 'Cache Hits', unit: '' },\n { key: 'cache_misses', label: 'Cache Misses', unit: '' },\n { key: 'replication_lag', label: 'Replication Lag', unit: 's' },\n { key: 'net_in', label: 'Network In', unit: 'bytes' },\n { key: 'net_out', label: 'Network Out', unit: 'bytes' }\n ]\n};\n\nconst TYPE_ICONS = { ec2: 'bi-pc-display', rds: 'bi-database', redis: 'bi-lightning-charge' };\nconst TYPE_LABELS = { ec2: 'EC2 Instance', rds: 'RDS Database', redis: 'ElastiCache Redis' };\n\nfunction yAxisForUnit(unit) {\n if (unit === '%') return { label: '%', beginAtZero: true, max: 100 };\n if (unit === 'bytes') return { label: 'Bytes', beginAtZero: true };\n if (unit === 's') return { label: 'Seconds', beginAtZero: true };\n return { beginAtZero: true };\n}\n\nexport default class CloudWatchResourceView extends View {\n constructor(options = {}) {\n super({\n className: 'cloudwatch-resource-view',\n ...options\n });\n\n this.resourceType = options.resourceType || 'ec2';\n this.slug = options.slug || '';\n this.resource = options.resource || {};\n }\n\n async getTemplate() {\n const categories = METRIC_CATEGORIES[this.resourceType] || [];\n const icon = TYPE_ICONS[this.resourceType] || 'bi-cloud';\n const typeLabel = TYPE_LABELS[this.resourceType] || 'Resource';\n\n const state = this.resource.state || this.resource.status || 'unknown';\n const metaItems = this._buildMetaItems();\n const metaHtml = metaItems.map(m => `<span class=\"me-3\" style=\"font-size: 0.78rem; color: #6c757d;\">${m}</span>`).join('');\n\n return `\n <style>\n .cwrv-header { padding: 1rem 0; border-bottom: 1px solid #e9ecef; margin-bottom: 1rem; }\n .cwrv-name { font-size: 1.15rem; font-weight: 700; }\n .cwrv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }\n @media (max-width: 768px) { .cwrv-grid { grid-template-columns: 1fr; } }\n </style>\n\n <div class=\"cwrv-header\">\n <div class=\"cwrv-name\">\n <i class=\"bi ${icon} me-2\"></i>${this.slug}\n <span class=\"badge bg-secondary ms-2\" style=\"font-size: 0.7rem;\">${typeLabel}</span>\n </div>\n <div class=\"mt-1\">${metaHtml}</div>\n </div>\n\n <div class=\"cwrv-grid\">\n ${categories.map((_, i) => `<div id=\"cwrv-chart-${i}\"></div>`).join('')}\n </div>\n `;\n }\n\n async onInit() {\n const categories = METRIC_CATEGORIES[this.resourceType] || [];\n\n for (let i = 0; i < categories.length; i++) {\n const cat = categories[i];\n const chart = new CloudWatchChart({\n containerId: `cwrv-chart-${i}`,\n account: this.resourceType,\n category: cat.key,\n slug: this.slug,\n title: cat.label,\n height: 200,\n yAxis: yAxisForUnit(cat.unit),\n showGranularity: true,\n showDateRange: false,\n defaultDateRange: '24h',\n granularity: 'hours'\n });\n this.addChild(chart);\n }\n }\n\n _buildMetaItems() {\n const r = this.resource;\n switch (this.resourceType) {\n case 'ec2':\n return [r.instance_type, r.private_ip, r.public_ip].filter(Boolean)\n .map((v, i) => {\n const icons = ['bi-cpu', 'bi-hdd-network', 'bi-globe'];\n return `<i class=\"bi ${icons[i]} me-1\"></i>${v}`;\n });\n case 'rds':\n return [r.engine, r.instance_class].filter(Boolean)\n .map((v, i) => {\n const icons = ['bi-database', 'bi-cpu'];\n return `<i class=\"bi ${icons[i]} me-1\"></i>${v}`;\n });\n case 'redis':\n return [r.engine, r.node_type, r.num_nodes ? `${r.num_nodes} node${r.num_nodes > 1 ? 's' : ''}` : ''].filter(Boolean)\n .map((v, i) => {\n const icons = ['bi-lightning', 'bi-cpu', 'bi-diagram-3'];\n return `<i class=\"bi ${icons[i]} me-1\"></i>${v}`;\n });\n default:\n return [];\n }\n }\n\n static async show(resourceType, slug, resource = {}, options = {}) {\n const view = new CloudWatchResourceView({\n resourceType, slug, resource\n });\n\n const icon = TYPE_ICONS[resourceType] || 'bi-cloud';\n const typeLabel = TYPE_LABELS[resourceType] || 'Resource';\n\n await Modal.dialog(view, {\n header: `<i class=\"bi ${icon} me-2\"></i>${slug} <small class=\"text-muted\">— ${typeLabel}</small>`,\n size: 'xl',\n scrollable: true\n });\n }\n}\n","/**\n * JobHealthView - System health overview\n */\n\nimport View from '@core/View.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { Job } from '@ext/admin/models/Job.js';\n\nexport default class JobHealthView extends View {\n constructor(options = {}) {\n super({\n className: 'job-health-section',\n ...options\n });\n\n this.health = {\n overall_status: 'unknown',\n runners: { active: 0, total: 0 },\n channels: []\n };\n this.healthStatusClass = 'text-muted';\n this.schedulerStatusText = 'Unknown';\n this.schedulerStatusClass = 'text-muted';\n this.schedulerIcon = 'bi-question-circle-fill';\n\n this.template = `\n <div class=\"job-health-header mb-4\">\n <div class=\"card border-0 shadow-sm\">\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\">Service 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 `;\n }\n\n _onModelChange() {\n this.loadHealth();\n if (this.isMounted()) {\n this.render();\n }\n }\n\n async loadHealth() {\n if (!this.model._.totals) return;\n const data = this.model.attributes\n // Determine overall health status\n let overall_status = 'healthy';\n if (data.totals.runners_active === 0) {\n overall_status = 'critical';\n } else if (!data.scheduler.active) {\n overall_status = 'warning';\n }\n this.health = {\n overall_status,\n channels: data.channels,\n runners: {\n active: data.totals.runners_active,\n total: data.runners.length\n },\n totals: data.totals,\n scheduler: data.scheduler\n };\n\n this.healthStatusClass = this.getHealthStatusClass(this.health.overall_status);\n this.setupChannelDisplay();\n this.setupSchedulerDisplay();\n\n }\n\n setupChannelDisplay() {\n if (!this.health.channels) return;\n\n this.health.channelsArray = Object.values(this.health.channels).map(channel => {\n let channelStatus = 'healthy';\n\n // Determine status based on queue backlog and runner availability\n const totalJobs = (channel.queued_count || 0) + (channel.inflight_count || 0);\n if (totalJobs > 50) {\n channelStatus = 'warning';\n }\n if (totalJobs > 100 || channel.runners === 0) {\n channelStatus = 'critical';\n }\n\n return {\n ...channel,\n status: channelStatus,\n statusBadgeClass: this.getChannelBadgeClass(channelStatus),\n statusIcon: this.getChannelIcon(channelStatus),\n queued: channel.queued_count || 0,\n inflight: channel.inflight_count || 0,\n // For backward compatibility, map to old names\n pending: channel.queued_count || 0,\n running: channel.inflight_count || 0\n };\n });\n }\n\n setupSchedulerDisplay() {\n if (!this.health.scheduler) {\n this.schedulerStatusText = 'Unknown';\n this.schedulerStatusClass = 'text-muted';\n this.schedulerIcon = 'bi-question-circle-fill';\n return;\n }\n\n if (this.health.scheduler.active) {\n this.schedulerStatusText = 'Running';\n this.schedulerStatusClass = 'text-success';\n this.schedulerIcon = 'bi-check-circle-fill';\n } else {\n this.schedulerStatusText = 'Stopped';\n this.schedulerStatusClass = 'text-danger';\n this.schedulerIcon = 'bi-x-octagon-fill';\n }\n }\n\n getHealthStatusClass(status) {\n const classes = {\n 'healthy': 'text-success',\n 'warning': 'text-warning',\n 'critical': 'text-danger',\n 'unknown': 'text-muted'\n };\n return classes[status] || 'text-muted';\n }\n\n getChannelBadgeClass(status) {\n const classes = {\n 'healthy': 'bg-success',\n 'warning': 'bg-warning',\n 'critical': 'bg-danger'\n };\n return classes[status] || 'bg-secondary';\n }\n\n getChannelIcon(status) {\n const icons = {\n 'healthy': 'bi-check-circle-fill',\n 'warning': 'bi-exclamation-triangle-fill',\n 'critical': 'bi-x-octagon-fill'\n };\n return icons[status] || 'bi-dash-circle-fill';\n }\n\n async onActionRefreshHealth(event, element) {\n try {\n element.disabled = true;\n await this.model.fetch();\n } catch (error) {\n console.error('Failed to refresh health:', error);\n } finally {\n element.disabled = false;\n }\n }\n\n async onActionSystemSettings() {\n await Modal.alert({\n title: 'System Settings',\n message: 'System settings interface coming soon!',\n type: 'info'\n });\n }\n}\n","/**\n * AssistantSkillView - Detail view for a single assistant skill\n *\n * Shows skill metadata, trigger phrases, and ordered step definitions.\n */\n\nimport View from '@core/View.js';\nimport { AssistantSkill } from '@ext/admin/models/Assistant.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nconst TIER_BADGE = {\n global: 'bg-primary',\n user: 'bg-info',\n group: 'bg-warning text-dark'\n};\n\nclass AssistantSkillView extends View {\n constructor(options = {}) {\n super({\n className: 'assistant-skill-view',\n ...options\n });\n\n this.model = options.model || new AssistantSkill(options.data || {});\n\n this.template = `\n <div class=\"d-flex flex-column h-100\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start mb-3\">\n <div class=\"flex-grow-1\" style=\"min-width: 0;\">\n <div class=\"d-flex align-items-center gap-2 mb-1\">\n {{{tierBadge}}}\n {{{statusBadge}}}\n <span class=\"text-muted small\">Skill #{{model.id}}</span>\n </div>\n <h4 class=\"mb-1\">{{model.name}}</h4>\n <p class=\"text-muted mb-2\">{{model.description}}</p>\n <div class=\"text-muted small d-flex align-items-center gap-3 flex-wrap\">\n {{#model.auto_execute|bool}}\n <span><i class=\"bi bi-lightning-fill text-warning me-1\"></i>Auto-execute enabled</span>\n {{/model.auto_execute|bool}}\n {{#model.created}}\n <span><i class=\"bi bi-clock me-1\"></i>{{model.created|relative}}</span>\n {{/model.created}}\n {{#model.modified}}\n <span><i class=\"bi bi-pencil me-1\"></i>{{model.modified|relative}}</span>\n {{/model.modified}}\n </div>\n </div>\n <div class=\"d-flex align-items-center gap-2 flex-shrink-0\">\n <button class=\"btn btn-outline-danger btn-sm\" data-action=\"delete-skill\">\n <i class=\"bi bi-trash me-1\"></i>Delete\n </button>\n </div>\n </div>\n\n <!-- Auto-Execute Info -->\n {{#model.auto_execute|bool}}\n <div class=\"alert alert-warning small py-2 mb-3\">\n <i class=\"bi bi-lightning-fill me-1\"></i>\n <strong>Auto-execute:</strong> The assistant will run this skill without asking for confirmation when triggered.\n </div>\n {{/model.auto_execute|bool}}\n\n <!-- Triggers -->\n {{#hasTriggers|bool}}\n <div class=\"mb-3\">\n <h6 class=\"text-muted mb-2\"><i class=\"bi bi-chat-quote me-1\"></i>Trigger Phrases</h6>\n <div class=\"d-flex flex-wrap gap-2\">\n {{#triggers}}\n <span class=\"badge bg-light text-dark border px-2 py-1\">{{.}}</span>\n {{/triggers}}\n </div>\n </div>\n {{/hasTriggers|bool}}\n\n <!-- Steps -->\n {{#hasSteps|bool}}\n <div class=\"mb-3\">\n <h6 class=\"text-muted mb-2\"><i class=\"bi bi-list-ol me-1\"></i>Steps ({{stepCount}})</h6>\n <div class=\"skill-steps-list\">\n {{{stepsHtml}}}\n </div>\n </div>\n {{/hasSteps|bool}}\n\n <!-- Metadata -->\n {{#hasMetadata|bool}}\n <div class=\"mb-3\">\n <h6 class=\"text-muted mb-2\"><i class=\"bi bi-braces me-1\"></i>Metadata</h6>\n <pre class=\"bg-light p-3 rounded small mb-0\"><code>{{metadataJson}}</code></pre>\n </div>\n {{/hasMetadata|bool}}\n </div>\n `;\n }\n\n async onInit() {\n // Fetch detail graph if steps not loaded\n if (!this.model.get('steps')) {\n try {\n await this.model.fetch({ params: { graph: 'detail' } });\n } catch (_e) {\n // Use whatever data we have\n }\n }\n }\n\n async onBeforeRender() {\n const tier = this.model.get('tier') || 'user';\n const tierCls = TIER_BADGE[tier] || 'bg-secondary';\n this.tierBadge = `<span class=\"badge ${tierCls}\">${tier}</span>`;\n\n const isActive = this.model.get('is_active');\n this.statusBadge = isActive\n ? '<span class=\"badge bg-success\">Active</span>'\n : '<span class=\"badge bg-secondary\">Inactive</span>';\n\n this.triggers = this.model.get('triggers') || [];\n this.hasTriggers = this.triggers.length > 0;\n\n const steps = this.model.get('steps') || [];\n this.hasSteps = steps.length > 0;\n this.stepCount = steps.length;\n this.stepsHtml = this._buildStepsHtml(steps);\n\n const metadata = this.model.get('metadata');\n this.hasMetadata = metadata && Object.keys(metadata).length > 0;\n this.metadataJson = this.hasMetadata ? JSON.stringify(metadata, null, 2) : '';\n }\n\n _buildStepsHtml(steps) {\n return steps.map((step, index) => {\n const esc = this._escapeHtml;\n const paramsJson = step.params ? JSON.stringify(step.params, null, 2) : null;\n const collapseId = `step-params-${this.model.get('id')}-${index}`;\n\n let html = `\n <div class=\"skill-step-item d-flex gap-3 py-2 ${index < steps.length - 1 ? 'border-bottom' : ''}\">\n <div class=\"skill-step-number flex-shrink-0\">\n <span class=\"badge bg-dark rounded-pill\">${index + 1}</span>\n </div>\n <div class=\"flex-grow-1\" style=\"min-width: 0;\">\n <div class=\"d-flex align-items-center gap-2 mb-1\">\n <code class=\"small\">${esc(step.tool || 'unknown')}</code>\n ${step.description ? `<span class=\"text-muted small\">— ${esc(step.description)}</span>` : ''}\n </div>`;\n\n if (step.condition) {\n html += `\n <div class=\"small text-warning\">\n <i class=\"bi bi-funnel me-1\"></i>Condition: <code>${esc(step.condition)}</code>\n </div>`;\n }\n\n if (paramsJson) {\n html += `\n <div class=\"mt-1\">\n <a class=\"small text-muted\" data-bs-toggle=\"collapse\" href=\"#${collapseId}\" role=\"button\" aria-expanded=\"false\">\n <i class=\"bi bi-chevron-right me-1\"></i>Parameters\n </a>\n <div class=\"collapse\" id=\"${collapseId}\">\n <pre class=\"bg-light p-2 rounded small mt-1 mb-0\"><code>${esc(paramsJson)}</code></pre>\n </div>\n </div>`;\n }\n\n html += `\n </div>\n </div>`;\n return html;\n }).join('');\n }\n\n _escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n async onActionDeleteSkill() {\n const confirmed = await Modal.confirm(\n `Delete skill \"${this.model.get('name')}\"? This cannot be undone.`,\n 'Delete Skill',\n { confirmText: 'Delete', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n try {\n await this.model.destroy();\n this.getApp()?.toast?.success('Skill deleted');\n this.emit('item:deleted', { id: this.model.get('id') });\n } catch (_e) {\n this.getApp()?.toast?.error('Failed to delete skill');\n }\n }\n}\n\nexport default AssistantSkillView;\n","/**\n * AssistantSkillTablePage - Skill management using TablePage component\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { AssistantSkillList } from '@ext/admin/models/Assistant.js';\nimport AssistantSkillView from './AssistantSkillView.js';\n\nconst TIER_BADGE = {\n global: 'bg-primary',\n user: 'bg-info',\n group: 'bg-warning text-dark'\n};\n\nclass AssistantSkillTablePage extends TablePage {\n constructor(options = {}) {\n super({\n name: 'assistant_skills',\n pageName: 'Assistant Skills',\n router: 'admin/assistant/skills',\n Collection: AssistantSkillList,\n itemViewClass: AssistantSkillView,\n\n viewDialogOptions: {\n header: false\n },\n\n defaultQuery: {\n sort: '-modified'\n },\n\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'name', label: 'Name', sortable: true },\n { key: 'description', label: 'Description', sortable: false, formatter: \"truncate:60\" },\n {\n key: 'tier', label: 'Tier', sortable: true, width: '100px',\n formatter: (value) => {\n const cls = TIER_BADGE[value] || 'bg-secondary';\n return `<span class=\"badge ${cls}\">${value || 'unknown'}</span>`;\n },\n filter: {\n type: 'multiselect',\n placeHolder: 'Select Tier',\n options: ['global', 'user', 'group']\n }\n },\n {\n key: 'steps', label: 'Steps', width: '80px', sortable: false,\n formatter: (value) => {\n const count = Array.isArray(value) ? value.length : 0;\n return `<span class=\"badge bg-secondary\">${count}</span>`;\n }\n },\n {\n key: 'auto_execute', label: 'Auto', width: '80px', sortable: true,\n formatter: (value) => value\n ? '<i class=\"bi bi-check-circle-fill text-success\"></i>'\n : '<i class=\"bi bi-circle text-muted\"></i>'\n },\n {\n key: 'is_active', label: 'Active', width: '80px', sortable: true,\n formatter: (value) => value\n ? '<span class=\"badge bg-success\">Active</span>'\n : '<span class=\"badge bg-secondary\">Inactive</span>'\n },\n { key: 'created', label: 'Created', sortable: true, formatter: 'datetime' }\n ],\n\n // Table features\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: false,\n\n // Toolbar — skills are created via chat, not UI\n showRefresh: true,\n showAdd: false,\n showExport: false,\n\n // Empty state\n emptyMessage: 'No skills found. Skills are created through the assistant chat.',\n\n // Table display\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n },\n ...options,\n });\n }\n}\n\nexport default AssistantSkillTablePage;\n","/**\n * AssistantConversationView - Detail view for a conversation's message history\n *\n * Displays the full message thread using ChatView with AssistantMessageView\n * for rendering structured blocks (tables, charts, stats, etc.).\n */\n\nimport View from '@core/View.js';\nimport ChatView from '@core/views/chat/ChatView.js';\nimport { AssistantConversation } from '@ext/admin/models/Assistant.js';\nimport AssistantMessageView from './AssistantMessageView.js';\nimport AssistantView from './AssistantView.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass AssistantConversationView extends View {\n constructor(options = {}) {\n super({\n className: 'assistant-conversation-view',\n ...options\n });\n\n this.model = options.model || new AssistantConversation(options.data || {});\n\n this.template = `\n <div class=\"d-flex flex-column h-100\">\n <!-- Header -->\n <div class=\"d-flex justify-content-between align-items-start mb-3 flex-shrink-0\">\n <div class=\"flex-grow-1\" style=\"min-width: 0;\">\n <h4 class=\"mb-1 text-truncate\">{{model.title}}</h4>\n <div class=\"text-muted small d-flex align-items-center gap-3 flex-wrap\">\n <span><i class=\"bi bi-hash me-1\"></i>Conversation #{{model.id}}</span>\n {{#model.created}}\n <span><i class=\"bi bi-clock me-1\"></i>{{model.created|relative}}</span>\n {{/model.created}}\n {{#model.modified}}\n <span><i class=\"bi bi-pencil me-1\"></i>Last active {{model.modified|relative}}</span>\n {{/model.modified}}\n {{#messageCount}}\n <span><i class=\"bi bi-chat-left-text me-1\"></i>{{messageCount}} messages</span>\n {{/messageCount}}\n </div>\n </div>\n <div class=\"d-flex align-items-center gap-2 flex-shrink-0\">\n <button class=\"btn btn-outline-danger btn-sm\" data-action=\"delete-conversation\">\n <i class=\"bi bi-trash me-1\"></i>Delete\n </button>\n </div>\n </div>\n\n <!-- Messages -->\n <div class=\"flex-grow-1 border rounded\" style=\"min-height: 200px;\" data-container=\"chat-view\"></div>\n </div>\n `;\n }\n\n async onInit() {\n // Fetch full message history\n try {\n await this.model.fetch({ params: { graph: 'detail' } });\n } catch (_e) {\n // Use whatever data we have\n }\n\n const messages = this.model.get('messages') || [];\n this.messageCount = messages.length;\n\n // Use the conversation's user ID, not the admin's\n const conversationUser = this.model.get('user');\n const currentUserId = conversationUser?.id;\n const transformedMessages = AssistantView._collapseMessages(\n messages\n .filter(msg => msg.role !== 'tool_result')\n .map(msg => this._transformMessage(msg, conversationUser))\n );\n\n this.chatView = new ChatView({\n containerId: 'chat-view',\n theme: 'compact',\n messageViewClass: AssistantMessageView,\n currentUserId,\n showInput: false,\n showFileInput: false,\n adapter: {\n fetch: async () => transformedMessages,\n addNote: async () => ({ success: false })\n }\n });\n this.addChild(this.chatView);\n }\n\n _transformMessage(msg, conversationUser) {\n let content = msg.content || '';\n let blocks = msg.blocks || [];\n let toolCalls = msg.tool_calls || [];\n\n // Extract text from tool_call entries\n if (toolCalls.length > 0) {\n const textParts = toolCalls\n .filter(tc => tc.type === 'text' && tc.text)\n .map(tc => tc.text);\n if (!content && textParts.length > 0) {\n content = textParts.join('\\n\\n');\n }\n toolCalls = toolCalls.filter(tc => tc.type === 'tool_use');\n }\n\n // Parse blocks from content if API didn't return them (legacy format)\n if (blocks.length === 0 && content.includes('assistant_block')) {\n const parsed = AssistantView._parseBlocks(content);\n content = parsed.content;\n blocks = parsed.blocks;\n }\n\n // Build author from message data or conversation-level user\n let author;\n if (msg.role === 'assistant') {\n author = { name: 'Assistant' };\n } else if (msg.author) {\n author = msg.author;\n } else {\n const user = msg.user || conversationUser;\n const avatarUrl = user?.avatar?.thumbnail || user?.avatar?.url || '';\n author = {\n name: user?.display_name || 'Unknown',\n id: user?.id,\n ...(avatarUrl ? { avatarUrl } : {})\n };\n }\n\n return {\n id: msg.id,\n role: msg.role || 'user',\n author,\n content,\n timestamp: msg.created || msg.timestamp,\n blocks,\n tool_calls: toolCalls,\n _conversationId: this.model.get('id')\n };\n }\n\n async onActionDeleteConversation() {\n const confirmed = await Modal.confirm(\n `Delete this conversation? This cannot be undone.`,\n 'Delete Conversation',\n { confirmText: 'Delete', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n try {\n await this.model.destroy();\n this.getApp()?.toast?.success('Conversation deleted');\n this.emit('item:deleted', { id: this.model.get('id') });\n } catch (_e) {\n this.getApp()?.toast?.error('Failed to delete conversation');\n }\n }\n}\n\nexport default AssistantConversationView;\n","/**\n * AssistantConversationTablePage - Conversation history using TablePage\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { AssistantConversationList } from '@ext/admin/models/Assistant.js';\nimport AssistantConversationView from './AssistantConversationView.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nclass AssistantConversationTablePage extends TablePage {\n constructor(options = {}) {\n super({\n name: 'assistant_conversations',\n pageName: 'Conversation History',\n router: 'admin/assistant/conversations',\n Collection: AssistantConversationList,\n itemViewClass: AssistantConversationView,\n\n viewDialogOptions: {\n header: false,\n size: 'xl'\n },\n\n defaultQuery: {\n sort: '-modified'\n },\n\n columns: [\n { key: 'id', label: 'ID', width: '70px', sortable: true, class: 'text-muted' },\n { key: 'user.username', label: 'User', sortable: true },\n { key: 'title', label: 'Title', sortable: true, formatter: \"truncate:80\" },\n { key: 'created', label: 'Created', sortable: true, formatter: 'datetime' },\n { key: 'modified', label: 'Last Active', sortable: true, formatter: 'relative' }\n ],\n\n // Table features\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: false,\n paginated: true,\n\n // Toolbar — conversations are created via chat\n showRefresh: true,\n showAdd: false,\n showExport: false,\n\n // Batch actions\n batchActions: [\n { label: 'Delete', action: 'delete', icon: 'bi bi-trash', class: 'text-danger' }\n ],\n\n // Empty state\n emptyMessage: 'No conversations yet. Start a conversation with the assistant.',\n\n // Table display\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n },\n ...options,\n });\n }\n\n async onActionBatchDelete() {\n const selected = this.tableView.getSelectedItems();\n if (!selected || selected.length === 0) return;\n\n const confirmed = await Modal.confirm(\n `Delete ${selected.length} conversation${selected.length > 1 ? 's' : ''}? This cannot be undone.`,\n 'Delete Conversations',\n { confirmText: 'Delete', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n let deleted = 0;\n for (const item of selected) {\n try {\n await item.destroy();\n deleted++;\n } catch (_e) {\n // continue\n }\n }\n\n this.getApp()?.toast?.success(`Deleted ${deleted} conversation${deleted !== 1 ? 's' : ''}`);\n await this.tableView.refresh();\n }\n}\n\nexport default AssistantConversationTablePage;\n","/**\n * AssistantMemoryPage - View and manage assistant memory entries\n *\n * Displays memories grouped by tier (global, personal, group) as\n * key-value lists with delete capability.\n */\n\nimport Page from '@core/Page.js';\nimport rest from '@core/Rest.js';\nimport Modal from '@core/views/feedback/Modal.js';\n\nconst TIER_CONFIG = {\n global: { label: 'Global', icon: 'bi-globe', badge: 'bg-primary', description: 'Visible to all assistant users' },\n user: { label: 'Personal', icon: 'bi-person', badge: 'bg-info', description: 'Private to you' },\n group: { label: 'Group', icon: 'bi-people', badge: 'bg-warning text-dark', description: 'Shared with your group' }\n};\n\nclass AssistantMemoryPage extends Page {\n constructor(options = {}) {\n super({\n pageName: 'Assistant Memory',\n className: 'mojo-page assistant-memory-page',\n ...options\n });\n\n this._memories = {};\n this._activeTier = 'user';\n this._loading = true;\n\n this.template = `\n <div class=\"container-fluid py-3\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div>\n <h4 class=\"mb-0\"><i class=\"bi bi-lightbulb me-2\"></i>Assistant Memory</h4>\n <p class=\"text-muted small mb-0\">Facts and preferences the assistant has learned during conversations.</p>\n </div>\n <button class=\"btn btn-outline-secondary btn-sm\" data-action=\"refresh\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Refresh\n </button>\n </div>\n\n <!-- Tier Tabs -->\n <ul class=\"nav nav-tabs mb-3\">\n {{#tierTabs}}\n <li class=\"nav-item\">\n <a class=\"nav-link {{#active}}active{{/active}}\" href=\"#\"\n data-action=\"switch-tier\" data-tier=\"{{key}}\">\n <i class=\"bi {{icon}} me-1\"></i>{{label}}\n {{#count}}<span class=\"badge bg-secondary ms-1\">{{count}}</span>{{/count}}\n </a>\n </li>\n {{/tierTabs}}\n </ul>\n\n <!-- Content -->\n {{#loading|bool}}\n <div class=\"text-center py-5\">\n <div class=\"spinner-border spinner-border-sm text-muted\" role=\"status\"></div>\n <div class=\"text-muted small mt-2\">Loading memories...</div>\n </div>\n {{/loading|bool}}\n\n {{^loading|bool}}\n {{{tierContent}}}\n {{/loading|bool}}\n </div>\n `;\n }\n\n async onEnter() {\n await this._loadMemories();\n }\n\n async onBeforeRender() {\n this.loading = this._loading;\n\n // Build tier tabs\n const tiers = ['user', 'global', 'group'];\n this.tierTabs = tiers\n .filter(key => key !== 'group' || this._memories[key])\n .map(key => {\n const cfg = TIER_CONFIG[key];\n const entries = this._memories[key] || {};\n return {\n key,\n label: cfg.label,\n icon: cfg.icon,\n count: Object.keys(entries).length || 0,\n active: key === this._activeTier\n };\n });\n\n // Build content for the active tier\n this.tierContent = this._buildTierContent(this._activeTier);\n }\n\n async _loadMemories() {\n this._loading = true;\n this.render();\n\n try {\n const resp = await rest.get('/api/assistant/memory');\n const data = resp?.data?.data || resp?.data || {};\n this._memories = {\n global: data.global || {},\n user: data.user || {},\n group: data.group || {}\n };\n } catch (_e) {\n this._memories = { global: {}, user: {}, group: {} };\n this.getApp()?.toast?.error('Failed to load memories');\n }\n\n this._loading = false;\n this.render();\n }\n\n _buildTierContent(tier) {\n const entries = this._memories[tier] || {};\n const keys = Object.keys(entries);\n const cfg = TIER_CONFIG[tier];\n\n if (keys.length === 0) {\n return `\n <div class=\"text-center py-5\">\n <i class=\"bi ${cfg.icon} fs-1 text-muted\"></i>\n <p class=\"text-muted mt-2 mb-0\">No memories stored.</p>\n <p class=\"text-muted small\">The assistant learns and stores memories during conversations.</p>\n </div>\n `;\n }\n\n const rows = keys.map(key => {\n const escapedKey = this._escapeHtml(key);\n const escapedValue = this._escapeHtml(String(entries[key]));\n return `\n <tr>\n <td class=\"align-middle\" style=\"width: 30%; min-width: 150px;\">\n <code class=\"small\">${escapedKey}</code>\n </td>\n <td class=\"align-middle\">${escapedValue}</td>\n <td class=\"align-middle text-end\" style=\"width: 60px;\">\n <button class=\"btn btn-outline-danger btn-sm\"\n data-action=\"delete-memory\"\n data-tier=\"${tier}\"\n data-key=\"${escapedKey}\"\n title=\"Delete this memory\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </td>\n </tr>\n `;\n }).join('');\n\n return `\n <div class=\"small text-muted mb-2\">\n <i class=\"bi bi-info-circle me-1\"></i>${cfg.description}\n </div>\n <div class=\"table-responsive\">\n <table class=\"table table-striped table-hover mb-0\">\n <thead>\n <tr>\n <th>Key</th>\n <th>Value</th>\n <th></th>\n </tr>\n </thead>\n <tbody>${rows}</tbody>\n </table>\n </div>\n `;\n }\n\n _escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n // ── Actions ──\n\n onActionSwitchTier(_event, element) {\n const tier = element.dataset.tier || element.closest('[data-tier]')?.dataset.tier;\n if (!tier || tier === this._activeTier) return;\n _event.preventDefault();\n this._activeTier = tier;\n this.render();\n }\n\n async onActionRefresh() {\n await this._loadMemories();\n }\n\n async onActionDeleteMemory(_event, element) {\n const tier = element.dataset.tier || element.closest('[data-tier]')?.dataset.tier;\n const key = element.dataset.key || element.closest('[data-key]')?.dataset.key;\n if (!tier || !key) return;\n\n const confirmed = await Modal.confirm(\n `Delete memory \"${key}\" from ${TIER_CONFIG[tier]?.label || tier}?`,\n 'Delete Memory',\n { confirmText: 'Delete', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n try {\n await rest.delete('/api/assistant/memory', { tier, key });\n delete this._memories[tier]?.[key];\n this.getApp()?.toast?.success('Memory deleted');\n this.render();\n } catch (_e) {\n this.getApp()?.toast?.error('Failed to delete memory');\n }\n }\n}\n\nexport default AssistantMemoryPage;\n","/**\n * MOJO Admin Extension - Entry (2.1.0)\n */\n\n// Bundle admin CSS\nimport '@ext/admin/css/admin.css';\n\n// Admin Pages\nexport { default as AdminDashboardPage } from '@ext/admin/account/AdminDashboardPage.js';\nexport { default as UserTablePage } from '@ext/admin/account/users/UserTablePage.js';\nexport { default as MemberTablePage } from '@ext/admin/account/users/MemberTablePage.js';\nexport { default as GroupTablePage } from '@ext/admin/account/groups/GroupTablePage.js';\nexport { default as UserDeviceTablePage } from '@ext/admin/account/devices/UserDeviceTablePage.js';\nexport { default as UserDeviceLocationTablePage } from '@ext/admin/account/devices/UserDeviceLocationTablePage.js';\nexport { default as GeoLocatedIPTablePage } from '@ext/admin/account/devices/GeoLocatedIPTablePage.js';\nexport { default as ApiKeyTablePage } from '@ext/admin/account/api_keys/ApiKeyTablePage.js';\n\nexport { default as CloudWatchDashboardPage } from '@ext/admin/aws/CloudWatchDashboardPage.js';\nexport { default as CloudWatchChart } from '@ext/admin/aws/CloudWatchChart.js';\n\nexport { default as IncidentDashboardPage } from '@ext/admin/incidents/dashboard/SecurityDashboardPage.js';\nexport { default as IncidentTablePage } from '@ext/admin/incidents/IncidentTablePage.js';\nexport { default as EventTablePage } from '@ext/admin/incidents/EventTablePage.js';\nexport { default as TicketTablePage } from '@ext/admin/incidents/TicketTablePage.js';\nexport { default as RuleSetTablePage } from '@ext/admin/incidents/RuleSetTablePage.js';\n\nexport { default as EmailDomainTablePage } from '@ext/admin/messaging/email/EmailDomainTablePage.js';\nexport { default as EmailMailboxTablePage } from '@ext/admin/messaging/email/EmailMailboxTablePage.js';\nexport { default as EmailTemplateTablePage } from '@ext/admin/messaging/email/EmailTemplateTablePage.js';\nexport { default as SentMessageTablePage } from '@ext/admin/messaging/email/SentMessageTablePage.js';\nexport { default as PublicMessageTablePage } from '@ext/admin/messaging/PublicMessageTablePage.js';\nexport { default as PhoneNumberTablePage } from '@ext/admin/messaging/sms/PhoneNumberTablePage.js';\nexport { default as SMSTablePage } from '@ext/admin/messaging/sms/SMSTablePage.js';\nexport { default as PushDashboardPage } from '@ext/admin/messaging/push/PushDashboardPage.js';\nexport { default as PushConfigTablePage } from '@ext/admin/messaging/push/PushConfigTablePage.js';\nexport { default as PushTemplateTablePage } from '@ext/admin/messaging/push/PushTemplateTablePage.js';\nexport { default as PushDeliveryTablePage } from '@ext/admin/messaging/push/PushDeliveryTablePage.js';\nexport { default as PushDeviceTablePage } from '@ext/admin/messaging/push/PushDeviceTablePage.js';\n\nexport { default as JobDashboardPage } from '@ext/admin/jobs/JobDashboardPage.js';\nexport { default as JobRunnersPage } from '@ext/admin/jobs/JobRunnersPage.js';\nexport { default as JobsTablePage } from '@ext/admin/jobs/JobsTablePage.js';\nexport { default as RunnerDetailsView } from '@ext/admin/jobs/RunnerDetailsView.js';\n\n// Security Pages\nexport { default as BlockedIPsTablePage } from '@ext/admin/security/BlockedIPsTablePage.js';\nexport { default as FirewallLogTablePage } from '@ext/admin/security/FirewallLogTablePage.js';\nexport { default as BouncerSignalTablePage } from '@ext/admin/security/BouncerSignalTablePage.js';\nexport { default as BouncerDeviceTablePage } from '@ext/admin/security/BouncerDeviceTablePage.js';\nexport { default as BotSignatureTablePage } from '@ext/admin/security/BotSignatureTablePage.js';\nexport { default as IPSetTablePage } from '@ext/admin/security/IPSetTablePage.js';\n\n// Security Views\nexport { default as BouncerSignalView } from '@ext/admin/security/BouncerSignalView.js';\nexport { default as BouncerDeviceView } from '@ext/admin/security/BouncerDeviceView.js';\nexport { default as HandlerBuilderView } from '@ext/admin/security/HandlerBuilderView.js';\nexport { default as IPSetView } from '@ext/admin/security/IPSetView.js';\n\nexport { default as LogTablePage } from '@ext/admin/monitoring/LogTablePage.js';\nexport { default as MetricsPermissionsTablePage } from '@ext/admin/monitoring/MetricsPermissionsTablePage.js';\n\nexport { default as SettingTablePage } from '@ext/admin/settings/SettingTablePage.js';\n\nexport { default as FileManagerTablePage } from '@ext/admin/storage/FileManagerTablePage.js';\nexport { default as FileTablePage } from '@ext/admin/storage/FileTablePage.js';\nexport { default as S3BucketTablePage } from '@ext/admin/storage/S3BucketTablePage.js';\n\nexport { default as ShortLinkTablePage } from '@ext/admin/shortlinks/ShortLinkTablePage.js';\nexport { default as ShortLinkClickTablePage } from '@ext/admin/shortlinks/ShortLinkClickTablePage.js';\nexport { default as ShortLinkView } from '@ext/admin/shortlinks/ShortLinkView.js';\n\n// Admin Views\nexport { default as DeviceView } from '@ext/admin/account/devices/DeviceView.js';\nexport { default as GeoIPView } from '@ext/admin/account/devices/GeoIPView.js';\nexport { default as UserDeviceLocationView } from '@ext/admin/account/devices/UserDeviceLocationView.js';\nexport { default as GroupView } from '@ext/admin/account/groups/GroupView.js';\nexport { default as ApiKeyView } from '@ext/admin/account/api_keys/ApiKeyView.js';\nexport { default as CloudWatchResourceView } from '@ext/admin/aws/CloudWatchResourceView.js';\nexport { default as MemberView } from '@ext/admin/account/users/MemberView.js';\nexport { default as UserView } from '@ext/admin/account/users/UserView.js';\n\nexport { default as IncidentView } from '@ext/admin/incidents/IncidentView.js';\nexport { default as EventView } from '@ext/admin/incidents/EventView.js';\nexport { default as TicketView } from '@ext/admin/incidents/TicketView.js';\nexport { default as RuleSetView } from '@ext/admin/incidents/RuleSetView.js';\n\nexport { default as EmailTemplateView } from '@ext/admin/messaging/email/EmailTemplateView.js';\nexport { default as EmailView } from '@ext/admin/messaging/email/EmailView.js';\nexport { default as PublicMessageView } from '@ext/admin/messaging/PublicMessageView.js';\nexport { default as PhoneNumberView } from '@ext/admin/messaging/sms/PhoneNumberView.js';\nexport { default as PushDeliveryView } from '@ext/admin/messaging/push/PushDeliveryView.js';\nexport { default as PushDeviceView } from '@ext/admin/messaging/push/PushDeviceView.js';\n\nexport { default as JobDetailsView } from '@ext/admin/jobs/JobDetailsView.js';\nexport { default as JobHealthView } from '@ext/admin/jobs/JobHealthView.js';\nexport { default as JobStatsView } from '@ext/admin/jobs/JobStatsView.js';\n\nexport { default as LogView } from '@ext/admin/monitoring/LogView.js';\nexport { default as MetricsPermissionsView } from '@ext/admin/monitoring/MetricsPermissionsView.js';\n\nexport { default as SettingView } from '@ext/admin/settings/SettingView.js';\n\nexport { default as FileView } from '@core/views/data/FileView.js';\n\n// Assistant\nexport { default as AssistantView } from '@ext/admin/assistant/AssistantView.js';\nexport { default as AssistantSkillTablePage } from '@ext/admin/assistant/AssistantSkillTablePage.js';\nexport { default as AssistantSkillView } from '@ext/admin/assistant/AssistantSkillView.js';\nexport { default as AssistantConversationTablePage } from '@ext/admin/assistant/AssistantConversationTablePage.js';\nexport { default as AssistantConversationView } from '@ext/admin/assistant/AssistantConversationView.js';\nexport { default as AssistantMemoryPage } from '@ext/admin/assistant/AssistantMemoryPage.js';\n\n// Admin Components\n// Convenience\nexport { default as WebApp } from '@core/WebApp.js';\n\n// Version info passthrough\nexport {\n VERSION_INFO,\n VERSION,\n VERSION_MAJOR,\n VERSION_MINOR,\n VERSION_REVISION,\n BUILD_TIME\n} from './version.js';\n\n\n\n// Import all admin page classes for the register function\nimport AdminDashboardPageClass from '@ext/admin/account/AdminDashboardPage.js';\nimport UserTablePageClass from '@ext/admin/account/users/UserTablePage.js';\nimport MemberTablePageClass from '@ext/admin/account/users/MemberTablePage.js';\nimport GroupTablePageClass from '@ext/admin/account/groups/GroupTablePage.js';\nimport UserDeviceTablePageClass from '@ext/admin/account/devices/UserDeviceTablePage.js';\nimport UserDeviceLocationTablePageClass from '@ext/admin/account/devices/UserDeviceLocationTablePage.js';\nimport GeoLocatedIPTablePageClass from '@ext/admin/account/devices/GeoLocatedIPTablePage.js';\nimport ApiKeyTablePageClass from '@ext/admin/account/api_keys/ApiKeyTablePage.js';\nimport CloudWatchDashboardPageClass from '@ext/admin/aws/CloudWatchDashboardPage.js';\n\nimport IncidentDashboardPageClass from '@ext/admin/incidents/dashboard/SecurityDashboardPage.js';\nimport IncidentTablePageClass from '@ext/admin/incidents/IncidentTablePage.js';\nimport EventTablePageClass from '@ext/admin/incidents/EventTablePage.js';\nimport TicketTablePageClass from '@ext/admin/incidents/TicketTablePage.js';\nimport RuleSetTablePageClass from '@ext/admin/incidents/RuleSetTablePage.js';\n\nimport EmailDomainTablePageClass from '@ext/admin/messaging/email/EmailDomainTablePage.js';\nimport EmailMailboxTablePageClass from '@ext/admin/messaging/email/EmailMailboxTablePage.js';\nimport EmailTemplateTablePageClass from '@ext/admin/messaging/email/EmailTemplateTablePage.js';\nimport SentMessageTablePageClass from '@ext/admin/messaging/email/SentMessageTablePage.js';\nimport PublicMessageTablePageClass from '@ext/admin/messaging/PublicMessageTablePage.js';\nimport PhoneNumberTablePageClass from '@ext/admin/messaging/sms/PhoneNumberTablePage.js';\nimport SMSTablePageClass from '@ext/admin/messaging/sms/SMSTablePage.js';\nimport PushDashboardPageClass from '@ext/admin/messaging/push/PushDashboardPage.js';\nimport PushConfigTablePageClass from '@ext/admin/messaging/push/PushConfigTablePage.js';\nimport PushTemplateTablePageClass from '@ext/admin/messaging/push/PushTemplateTablePage.js';\nimport PushDeliveryTablePageClass from '@ext/admin/messaging/push/PushDeliveryTablePage.js';\nimport PushDeviceTablePageClass from '@ext/admin/messaging/push/PushDeviceTablePage.js';\n\nimport JobDashboardPageClass from '@ext/admin/jobs/JobDashboardPage.js';\nimport JobRunnersPageClass from '@ext/admin/jobs/JobRunnersPage.js';\nimport JobsTablePageClass from '@ext/admin/jobs/JobsTablePage.js';\nimport ScheduledTaskTablePageClass from '@ext/admin/jobs/ScheduledTaskTablePage.js';\n\nimport BlockedIPsTablePageClass from '@ext/admin/security/BlockedIPsTablePage.js';\nimport FirewallLogTablePageClass from '@ext/admin/security/FirewallLogTablePage.js';\nimport BouncerSignalTablePageClass from '@ext/admin/security/BouncerSignalTablePage.js';\nimport BouncerDeviceTablePageClass from '@ext/admin/security/BouncerDeviceTablePage.js';\nimport BotSignatureTablePageClass from '@ext/admin/security/BotSignatureTablePage.js';\nimport IPSetTablePageClass from '@ext/admin/security/IPSetTablePage.js';\n\nimport LogTablePageClass from '@ext/admin/monitoring/LogTablePage.js';\nimport MetricsPermissionsTablePageClass from '@ext/admin/monitoring/MetricsPermissionsTablePage.js';\n\nimport SettingTablePageClass from '@ext/admin/settings/SettingTablePage.js';\n\nimport FileManagerTablePageClass from '@ext/admin/storage/FileManagerTablePage.js';\nimport FileTablePageClass from '@ext/admin/storage/FileTablePage.js';\nimport S3BucketTablePageClass from '@ext/admin/storage/S3BucketTablePage.js';\n\nimport ShortLinkTablePageClass from '@ext/admin/shortlinks/ShortLinkTablePage.js';\nimport ShortLinkClickTablePageClass from '@ext/admin/shortlinks/ShortLinkClickTablePage.js';\n\nimport AssistantSkillTablePageClass from '@ext/admin/assistant/AssistantSkillTablePage.js';\nimport AssistantConversationTablePageClass from '@ext/admin/assistant/AssistantConversationTablePage.js';\nimport AssistantMemoryPageClass from '@ext/admin/assistant/AssistantMemoryPage.js';\n\n/**\n * Register all admin pages to a WebApp instance\n * @param {WebApp} app - The WebApp instance to register pages to\n * @returns {void}\n */\nexport function registerSystemPages(app, addToMenu = true) {\n // Register all admin pages with consistent naming\n // Permissions align with django-mojo backend: category perms (security, users, groups, etc.)\n // and fine-grained perms (view_security, manage_users, etc.)\n app.registerPage('system/dashboard', AdminDashboardPageClass, {permissions: [\"security\"]});\n app.registerPage('system/jobs/dashboard', JobDashboardPageClass, {permissions: [\"view_jobs\", \"manage_jobs\"]});\n app.registerPage('system/jobs/runners', JobRunnersPageClass, {permissions: [\"view_jobs\"]});\n app.registerPage('system/jobs/list', JobsTablePageClass, {permissions: [\"view_jobs\"]});\n app.registerPage('system/users', UserTablePageClass, {permissions: [\"view_users\", \"manage_users\"]});\n app.registerPage('system/groups', GroupTablePageClass, {permissions: [\"view_groups\", \"manage_groups\"]});\n app.registerPage('system/members', MemberTablePageClass, {permissions: [\"view_members\", \"manage_groups\"]});\n app.registerPage('system/s3buckets', S3BucketTablePageClass, {permissions: [\"manage_aws\"]});\n app.registerPage('system/filemanagers', FileManagerTablePageClass, {permissions: [\"view_fileman\", \"manage_files\"]});\n app.registerPage('system/files', FileTablePageClass, {permissions: [\"manage_files\"]});\n app.registerPage('system/shortlinks/links', ShortLinkTablePageClass, {permissions: [\"manage_shortlinks\"]});\n app.registerPage('system/shortlinks/clicks', ShortLinkClickTablePageClass, {permissions: [\"manage_shortlinks\"]});\n app.registerPage('system/incidents', IncidentTablePageClass, {permissions: [\"view_security\"]});\n app.registerPage('system/events', EventTablePageClass, {permissions: [\"view_security\"]});\n app.registerPage('system/logs', LogTablePageClass, {permissions: [\"view_logs\"]});\n app.registerPage('system/user/devices', UserDeviceTablePageClass, {permissions: [\"manage_users\"]});\n app.registerPage('system/user/device-locations', UserDeviceLocationTablePageClass, {permissions: [\"manage_users\"]});\n app.registerPage('system/system/geoip', GeoLocatedIPTablePageClass, {permissions: [\"view_security\", \"manage_users\"]});\n app.registerPage('system/email/mailboxes', EmailMailboxTablePageClass, {permissions: [\"manage_aws\"]});\n app.registerPage('system/email/domains', EmailDomainTablePageClass, {permissions: [\"manage_aws\"]});\n app.registerPage('system/email/sent', SentMessageTablePageClass, {permissions: [\"manage_aws\"]});\n app.registerPage('system/email/templates', EmailTemplateTablePageClass, {permissions: [\"manage_aws\"]});\n app.registerPage('system/messaging/public-messages', PublicMessageTablePageClass, {permissions: [\"view_support\", \"support\", \"security\"]});\n app.registerPage('system/incident-dashboard', IncidentDashboardPageClass, { permissions: [\"view_security\"] });\n app.registerPage('system/rulesets', RuleSetTablePageClass, { permissions: [\"manage_security\"] });\n app.registerPage('system/tickets', TicketTablePageClass, { permissions: [\"manage_security\"] });\n app.registerPage('system/metrics/permissions', MetricsPermissionsTablePageClass, { permissions: [\"manage_metrics\"] });\n app.registerPage('system/push/dashboard', PushDashboardPageClass, { permissions: [\"manage_notifications\"] });\n app.registerPage('system/push/configs', PushConfigTablePageClass, { permissions: [\"manage_push_config\"] });\n app.registerPage('system/push/templates', PushTemplateTablePageClass, { permissions: [\"manage_notifications\"] });\n app.registerPage('system/push/deliveries', PushDeliveryTablePageClass, { permissions: [\"view_notifications\", \"manage_notifications\"] });\n app.registerPage('system/push/devices', PushDeviceTablePageClass, { permissions: [\"view_devices\", \"manage_devices\"] });\n app.registerPage('system/phonehub/numbers', PhoneNumberTablePageClass, { permissions: [\"view_phone_numbers\", \"manage_phone_numbers\"] });\n app.registerPage('system/phonehub/sms', SMSTablePageClass, { permissions: [\"view_sms\", \"manage_sms\"] });\n app.registerPage('system/api-keys', ApiKeyTablePageClass, { permissions: [\"manage_groups\", \"manage_group\"] });\n app.registerPage('system/settings', SettingTablePageClass, { permissions: [\"manage_settings\"] });\n app.registerPage('system/cloudwatch', CloudWatchDashboardPageClass, { permissions: [\"manage_aws\"] });\n\n // Security pages\n app.registerPage('system/security/blocked-ips', BlockedIPsTablePageClass, { permissions: [\"view_security\"] });\n app.registerPage('system/security/firewall-log', FirewallLogTablePageClass, { permissions: [\"view_security\"] });\n app.registerPage('system/security/bouncer-signals', BouncerSignalTablePageClass, { permissions: [\"view_security\"] });\n app.registerPage('system/security/bouncer-devices', BouncerDeviceTablePageClass, { permissions: [\"view_security\"] });\n app.registerPage('system/security/bot-signatures', BotSignatureTablePageClass, { permissions: [\"manage_security\"] });\n app.registerPage('system/security/ipsets', IPSetTablePageClass, { permissions: [\"view_security\"] });\n\n // Assistant management pages\n app.registerPage('system/assistant/skills', AssistantSkillTablePageClass, { permissions: [\"view_admin\", \"assistant\"] });\n app.registerPage('system/assistant/conversations', AssistantConversationTablePageClass, { permissions: [\"view_admin\", \"assistant\"] });\n app.registerPage('system/assistant/memory', AssistantMemoryPageClass, { permissions: [\"view_admin\", \"assistant\"] });\n\n // Check if sidebar exists and has an admin menu config\n if (addToMenu && app.sidebar && app.sidebar.getMenuConfig) {\n const adminMenuConfig = app.sidebar.getMenuConfig('system');\n if (adminMenuConfig && adminMenuConfig.items) {\n // Add admin pages to sidebar menu\n const adminMenuItems = [\n // ── Top-level (daily-use pages) ──\n {\n text: 'Dashboard',\n route: '?page=system/dashboard',\n icon: 'bi-speedometer2',\n permissions: [\"security\"]\n },\n {\n text: 'Security Dashboard',\n route: '?page=system/incident-dashboard',\n icon: 'bi-shield-check',\n permissions: [\"view_security\"]\n },\n {\n text: 'Users',\n route: '?page=system/users',\n icon: 'bi-people',\n permissions: [\"view_users\", \"manage_users\"]\n },\n {\n text: 'Groups',\n route: '?page=system/groups',\n icon: 'bi-diagram-3',\n permissions: [\"view_groups\", \"manage_groups\"]\n },\n {\n text: 'Job Engine',\n route: null,\n icon: 'bi-gear-wide-connected',\n permissions: [\"view_jobs\", \"manage_jobs\"],\n children: [\n { text: 'Dashboard', route: '?page=system/jobs/dashboard', icon: 'bi-bar-chart-line', permissions: [\"jobs\", \"view_jobs\", \"manage_jobs\"] },\n { text: 'Runners', route: '?page=system/jobs/runners', icon: 'bi-cpu', permissions: [\"jobs\", \"view_jobs\", \"manage_jobs\"] },\n { text: 'Jobs', route: '?page=system/jobs/list', icon: 'bi-list-task', permissions: [\"jobs\", \"view_jobs\", \"manage_jobs\"] }\n ]\n },\n\n // ── System Security (incident pipeline) ──\n {\n text: 'System Security',\n route: null,\n icon: 'bi-shield-exclamation',\n permissions: [\"view_security\"],\n children: [\n { text: 'Tickets', route: '?page=system/tickets', icon: 'bi-ticket-detailed', permissions: [\"manage_security\"] },\n { text: 'Incidents', route: '?page=system/incidents', icon: 'bi-exclamation-triangle', permissions: [\"view_security\"] },\n { text: 'Events', route: '?page=system/events', icon: 'bi-bell', permissions: [\"view_security\"] },\n { text: 'Rules', route: '?page=system/rulesets', icon: 'bi-funnel', permissions: [\"manage_security\"] },\n ]\n },\n\n // ── Network Security (IP filtering / firewall) ──\n {\n text: 'Network Security',\n route: null,\n icon: 'bi-hdd-network',\n permissions: [\"view_security\"],\n children: [\n { text: 'IPs', route: '?page=system/system/geoip', icon: 'bi-globe', permissions: [\"view_security\"] },\n { text: 'IP Sets', route: '?page=system/security/ipsets', icon: 'bi-shield-shaded', permissions: [\"view_security\"] },\n { text: 'Blocked', route: '?page=system/security/blocked-ips', icon: 'bi-slash-circle', permissions: [\"view_security\"] },\n { text: 'Firewall Log', route: '?page=system/security/firewall-log', icon: 'bi-journal-code', permissions: [\"view_security\"] },\n ]\n },\n\n // ── Bouncer (signal / device / bot detection) ──\n {\n text: 'Bouncer',\n route: null,\n icon: 'bi-fingerprint',\n permissions: [\"view_security\"],\n children: [\n { text: 'Signals', route: '?page=system/security/bouncer-signals', icon: 'bi-activity', permissions: [\"view_security\"] },\n { text: 'Devices', route: '?page=system/security/bouncer-devices', icon: 'bi-fingerprint', permissions: [\"view_security\"] },\n { text: 'Bots', route: '?page=system/security/bot-signatures', icon: 'bi-robot', permissions: [\"manage_security\"] },\n ]\n },\n\n // ── Messaging ──\n {\n text: 'Messaging',\n route: null,\n icon: 'bi-envelope',\n permissions: [\"manage_aws\", \"view_support\", \"support\"],\n children: [\n { text: 'Domains', route: '?page=system/email/domains', icon: 'bi-globe', permissions: [\"manage_aws\"] },\n { text: 'Mailboxes', route: '?page=system/email/mailboxes', icon: 'bi-inbox', permissions: [\"manage_aws\"] },\n { text: 'Sent', route: '?page=system/email/sent', icon: 'bi-send-check', permissions: [\"manage_aws\"] },\n { text: 'Templates', route: '?page=system/email/templates', icon: 'bi-file-text', permissions: [\"manage_aws\"] },\n { text: 'Contact Messages', route: '?page=system/messaging/public-messages', icon: 'bi-chat-square-text', permissions: [\"view_support\", \"support\"] },\n ]\n },\n\n // ── Push Notifications ──\n {\n text: 'Push Notifications',\n route: null,\n icon: 'bi-broadcast',\n permissions: [\"manage_notifications\", \"manage_push_config\"],\n children: [\n { text: 'Dashboard', route: '?page=system/push/dashboard', icon: 'bi-bar-chart-line', permissions: [\"manage_notifications\"] },\n { text: 'Configurations', route: '?page=system/push/configs', icon: 'bi-gear', permissions: [\"manage_push_config\"] },\n { text: 'Templates', route: '?page=system/push/templates', icon: 'bi-file-earmark-text', permissions: [\"manage_notifications\"] },\n { text: 'Deliveries', route: '?page=system/push/deliveries', icon: 'bi-send', permissions: [\"view_notifications\", \"manage_notifications\"] },\n { text: 'Devices', route: '?page=system/push/devices', icon: 'bi-phone', permissions: [\"view_devices\", \"manage_devices\"] },\n ]\n },\n\n // ── Shortlinks ──\n {\n text: 'Shortlinks',\n route: null,\n icon: 'bi-link-45deg',\n permissions: [\"manage_shortlinks\"],\n children: [\n { text: 'Links', route: '?page=system/shortlinks/links', icon: 'bi-link', permissions: [\"manage_shortlinks\"] },\n { text: 'Click History', route: '?page=system/shortlinks/clicks', icon: 'bi-cursor', permissions: [\"manage_shortlinks\"] },\n ]\n },\n\n // ── Phone Hub ──\n {\n text: 'Phone Hub',\n route: null,\n icon: 'bi-telephone',\n permissions: [\"view_phone_numbers\", \"manage_phone_numbers\"],\n children: [\n { text: 'Numbers', route: '?page=system/phonehub/numbers', icon: 'bi-collection', permissions: [\"view_phone_numbers\", \"manage_phone_numbers\"] },\n { text: 'SMS', route: '?page=system/phonehub/sms', icon: 'bi-chat-dots', permissions: [\"view_sms\", \"manage_sms\"] },\n ]\n },\n\n // ── Storage ──\n {\n text: 'Storage',\n route: null,\n icon: 'bi-folder',\n permissions: [\"manage_files\", \"manage_aws\"],\n children: [\n { text: 'S3 Buckets', route: '?page=system/s3buckets', icon: 'bi-bucket', permissions: [\"manage_aws\"] },\n { text: 'Storage Backends', route: '?page=system/filemanagers', icon: 'bi-hdd-stack', permissions: [\"view_fileman\", \"manage_files\"] },\n { text: 'Files', route: '?page=system/files', icon: 'bi-file-earmark', permissions: [\"manage_files\"] },\n ]\n },\n\n // ── AI Assistant ──\n {\n text: 'AI Assistant',\n route: null,\n icon: 'bi-robot',\n permissions: [\"view_admin\", \"assistant\"],\n children: [\n { text: 'Skills', route: '?page=system/assistant/skills', icon: 'bi-lightning', permissions: [\"view_admin\", \"assistant\"] },\n { text: 'Memory', route: '?page=system/assistant/memory', icon: 'bi-lightbulb', permissions: [\"view_admin\", \"assistant\"] },\n { text: 'Conversations', route: '?page=system/assistant/conversations', icon: 'bi-chat-left-text', permissions: [\"view_admin\", \"assistant\"] },\n ]\n },\n\n // ── System (infrastructure & ops) ──\n {\n text: 'System',\n route: null,\n icon: 'bi-wrench-adjustable',\n permissions: [\"view_logs\", \"manage_settings\", \"manage_groups\"],\n children: [\n { text: 'Logs', route: '?page=system/logs', icon: 'bi-journal-text', permissions: [\"view_logs\"] },\n { text: 'API Keys', route: '?page=system/api-keys', icon: 'bi-key', permissions: [\"manage_groups\", \"manage_group\"] },\n { text: 'User Devices', route: '?page=system/user/devices', icon: 'bi-phone', permissions: [\"manage_users\"] },\n { text: 'Device Locations', route: '?page=system/user/device-locations', icon: 'bi-geo-alt', permissions: [\"manage_users\"] },\n { text: 'Metrics Permissions', route: '?page=system/metrics/permissions', icon: 'bi-bar-chart-line', permissions: [\"manage_metrics\"] },\n { text: 'Settings', route: '?page=system/settings', icon: 'bi-gear', permissions: [\"manage_settings\"] },\n { text: 'CloudWatch', route: '?page=system/cloudwatch', icon: 'bi-cloud', permissions: [\"manage_aws\"] },\n ]\n },\n ];\n\n // Add items to existing admin menu\n adminMenuConfig.items.unshift(...adminMenuItems);\n }\n }\n\n}\n\nexport { registerSystemPages as registerAdminPages };\n\n/**\n * Register the Admin Assistant topbar button\n * Auto-selects display mode based on viewport width:\n * >= 1000px → right sidebar panel (reflow layout)\n * < 1000px → fullscreen modal\n * Requires `view_admin` permission.\n *\n * @param {WebApp} app - The WebApp/PortalApp instance\n */\nexport function registerAssistant(app) {\n\n // ── Sidebar panel helpers ────────────────────────────────\n\n function closeSidebarPanel() {\n if (!app._assistantPanel) return;\n const layout = document.querySelector('.portal-layout');\n if (layout) layout.classList.remove('assistant-panel-open');\n app._assistantPanel.destroy();\n app._assistantPanel = null;\n const panelEl = document.getElementById('assistant-panel');\n if (panelEl) panelEl.remove();\n }\n\n async function openSidebarPanel() {\n // If already open, just focus\n if (app._assistantPanel && app._assistantPanel.isMounted()) {\n app._assistantPanel.focusInput();\n return;\n }\n\n const { default: AssistantPanelView } = await import('@ext/admin/assistant/AssistantPanelView.js');\n\n // Create mount point inside .portal-layout\n const layout = document.querySelector('.portal-layout');\n if (!layout) return openFullscreenModal(); // fallback\n\n let panelEl = document.getElementById('assistant-panel');\n if (!panelEl) {\n panelEl = document.createElement('div');\n panelEl.id = 'assistant-panel';\n layout.appendChild(panelEl);\n }\n\n const view = new AssistantPanelView({ app });\n view.on('panel:close', () => closeSidebarPanel());\n view.on('panel:fullscreen', () => openFullscreenModal());\n view.on('panel:popout', (data) => openPopupWindow(data?.conversationId));\n await view.render(true, panelEl);\n app._assistantPanel = view;\n\n // Restore saved width before triggering open transition\n const savedWidth = localStorage.getItem('mojo:assistant_panel_width');\n if (savedWidth) {\n const w = parseInt(savedWidth, 10);\n if (w >= 300 && w <= 700) panelEl.style.width = '0px'; // start from 0 for transition\n }\n\n // Trigger reflow transition\n requestAnimationFrame(() => {\n layout.classList.add('assistant-panel-open');\n if (savedWidth) {\n const w = parseInt(savedWidth, 10);\n if (w >= 300 && w <= 700) panelEl.style.width = w + 'px';\n }\n });\n }\n\n async function openPopupWindow(conversationId) {\n // Close sidebar first\n closeSidebarPanel();\n\n const popup = window.open('', 'mojo-assistant',\n 'width=480,height=700,toolbar=no,menubar=no,status=no,location=no,resizable=yes');\n\n if (!popup) {\n // Popup blocked — fall back to sidebar or fullscreen\n if (app.toast) app.toast.warning('Popup blocked — opening sidebar instead');\n if (window.innerWidth >= 1000) {\n await openSidebarPanel();\n } else {\n await openFullscreenModal();\n }\n return;\n }\n\n // Write minimal document into the popup\n const styles = document.querySelectorAll('link[rel=\"stylesheet\"], style');\n let styleHTML = '';\n styles.forEach(s => {\n if (s.tagName === 'LINK') {\n styleHTML += `<link rel=\"stylesheet\" href=\"${s.href}\">`;\n } else {\n styleHTML += s.outerHTML;\n }\n });\n\n popup.document.write(`<!DOCTYPE html>\n<html><head><title>AI Assistant</title>${styleHTML}\n<style>\n body { margin: 0; height: 100vh; overflow: hidden; }\n #assistant-popup-root { height: 100vh; }\n .assistant-panel-view { height: 100%; }\n .assistant-panel-resize-handle { display: none; }\n</style>\n</head><body class=\"assistant-popup\">\n<div id=\"assistant-popup-root\"></div>\n</body></html>`);\n popup.document.close();\n\n const { default: AssistantPanelView } = await import('@ext/admin/assistant/AssistantPanelView.js');\n\n const view = new AssistantPanelView({\n app,\n conversationId: conversationId || app._assistantConversationId || null\n });\n\n // Pop-out from popup doesn't make sense — close panel just closes popup\n view.on('panel:close', () => popup.close());\n view.on('panel:popout', () => {}); // no-op in popup\n\n const root = popup.document.getElementById('assistant-popup-root');\n await view.render(true, root);\n\n app._assistantPopup = popup;\n app._assistantPopupView = view;\n\n // Cleanup when popup is closed externally\n popup.addEventListener('beforeunload', () => {\n if (app._assistantPopupView) {\n app._assistantPopupView.destroy();\n app._assistantPopupView = null;\n }\n app._assistantPopup = null;\n });\n }\n\n async function openFullscreenModal() {\n // Close sidebar if open, preserving conversation ID\n closeSidebarPanel();\n\n const { default: AssistantViewClass } = await import('@ext/admin/assistant/AssistantView.js');\n const { default: Modal } = await import('@core/views/feedback/Modal.js');\n const view = new AssistantViewClass({ app });\n Modal.show(view, {\n size: 'fullscreen',\n noBodyPadding: true,\n title: ' ',\n buttons: []\n });\n }\n\n // ── Resize listener (debounced) ──────────────────────────\n\n let resizeTimeout = null;\n function onResize() {\n if (resizeTimeout) clearTimeout(resizeTimeout);\n resizeTimeout = setTimeout(() => {\n // If sidebar is open but viewport is now too narrow, switch to fullscreen\n if (app._assistantPanel && window.innerWidth < 1000) {\n openFullscreenModal();\n }\n }, 250);\n }\n window.addEventListener('resize', onResize);\n\n // ── Topbar button ────────────────────────────────────────\n\n const assistantItem = {\n id: 'assistant',\n icon: 'bi-robot',\n action: 'open-assistant',\n isButton: true,\n buttonClass: 'btn btn-link nav-link',\n tooltip: 'Admin Assistant',\n permissions: ['view_admin'],\n handler: async () => {\n // Toggle sidebar if already open\n if (app._assistantPanel && app._assistantPanel.isMounted()) {\n closeSidebarPanel();\n return;\n }\n\n if (window.innerWidth >= 1000) {\n await openSidebarPanel();\n } else {\n await openFullscreenModal();\n }\n }\n };\n\n // If topbar is already created, add to its config directly\n if (app.topbar && app.topbar.config) {\n app.topbar.config.rightItems.unshift(assistantItem);\n if (app.topbar.isMounted()) {\n app.topbar.render();\n }\n } else if (app.topbarConfig) {\n // Before app.start() — add to the topbar config that PortalApp reads\n if (!app.topbarConfig.rightItems) app.topbarConfig.rightItems = [];\n app.topbarConfig.rightItems.unshift(assistantItem);\n }\n}\n","/**\n * ScheduledTaskTablePage - Admin page for managing user-defined scheduled tasks\n */\n\nimport TablePage from '@core/pages/TablePage.js';\nimport { ScheduledTask, ScheduledTaskList, ScheduledTaskForms, DAY_LABELS } from '@ext/admin/models/ScheduledTask.js';\nimport ScheduledTaskView from './ScheduledTaskView.js';\n\nScheduledTask.ADD_FORM = ScheduledTaskForms.create;\nScheduledTask.EDIT_FORM = ScheduledTaskForms.edit;\n\nclass ScheduledTaskTablePage extends TablePage {\n constructor(options = {}) {\n super({\n ...options,\n name: 'admin_scheduled_tasks',\n pageName: 'Scheduled Tasks',\n router: 'admin/scheduled-tasks',\n Collection: ScheduledTaskList,\n\n itemViewClass: ScheduledTaskView,\n viewDialogOptions: {\n header: false,\n size: 'lg'\n },\n\n columns: [\n { key: 'name', label: 'Name', sortable: true },\n {\n key: 'task_type',\n label: 'Type',\n width: '90px',\n formatter: \"uppercase|badge\"\n },\n {\n key: 'enabled',\n label: 'Status',\n width: '100px',\n formatter: \"boolean('Enabled|bg-success','Disabled|bg-secondary')|badge\"\n },\n {\n key: 'run_times',\n label: 'Schedule',\n render(value, model) {\n const times = value || [];\n const days = model.get('run_days') || [];\n const timeStr = times.join(', ') || '—';\n const dayStr = days.length === 0 || days.length === 7\n ? 'Every day'\n : days.map(d => DAY_LABELS[d] || d).join(', ');\n return `${timeStr} · ${dayStr}`;\n }\n },\n { key: 'run_count', label: 'Runs', width: '70px', sortable: true },\n { key: 'last_run', label: 'Last Run', formatter: 'relative', sortable: true },\n { key: 'created', label: 'Created', formatter: 'relative', sortable: true }\n ],\n\n selectable: true,\n searchable: true,\n sortable: true,\n filterable: true,\n paginated: true,\n\n showRefresh: true,\n showAdd: true,\n showExport: false,\n\n addButtonLabel: 'New Task',\n\n filters: [\n {\n key: 'enabled',\n label: 'Status',\n type: 'select',\n options: [\n { value: '', label: 'All' },\n { value: 'true', label: 'Enabled' },\n { value: 'false', label: 'Disabled' }\n ]\n },\n {\n key: 'task_type',\n label: 'Type',\n type: 'select',\n options: [\n { value: '', label: 'All' },\n { value: 'llm', label: 'LLM' },\n { value: 'job', label: 'Job' },\n { value: 'webhook', label: 'Webhook' }\n ]\n }\n ],\n\n emptyMessage: 'No scheduled tasks found.',\n\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false\n }\n });\n }\n}\n\nexport default ScheduledTaskTablePage;\n"],"names":["KPITile","View","constructor","options","super","tagName","className","severity","trim","this","slug","label","value","delta","deltaPct","tone","sparklineValues","Array","isArray","sparkline","slice","sparklineColor","sparklineHeight","formatter","element","setAttribute","onInit","MiniChart","containerId","chartType","data","color","_defaultSparkColor","fillColor","_fillColorFor","fill","smoothing","height","width","showTooltip","showCrosshair","showXAxis","animate","padding","addChild","getTemplate","valueDisplay","_formatValue","deltaHtml","_renderDelta","escapeHtml","onAfterRender","_clickBound","addEventListener","emit","tile","setData","isMounted","render","critical","high","warn","info","good","stroke","m","String","match","v","toLocaleString","sign","Number","isFinite","rounded","Math","abs","round","cls","KPIStrip","tiles","account","granularity","sparklineDays","sparklineGranularity","seriesEndpoint","fetchEndpoint","includeSparkline","tileHeight","_tileViews","i","length","spec","key","on","push","getViewData","tilesHtml","map","_","join","renderTemplate","refresh","rest","getApp","metricSpecs","filter","t","restSpecs","sparkSlugs","from","Set","sparklineSlug","Boolean","promises","seriesPromise","params","slugs","with_delta","Date","now","GET","catch","err","console","sparkPromise","drStart","sparkParams","with_labels","dr_start","floor","getTime","restPromises","endpoint","Promise","allSettled","seriesResp","sparkResp","seriesData","_unwrap","sparkInner","tileIdx","indexOf","deltaInfo","deltas","delta_pct","slugSeries","fallbackSeries","default","sparkSeries","restIdx","resp","setPayload","_readRestCount","fallback","response","success","body","count","AdminHeaderView","title","headerActions","icon","action","buttonClass","stats","user_activity_day","total_users","group_activity_day","total_groups","api_calls","apiChange","incidents","incidentsChange","prepareStatsForTemplate","userActivity","MetricsMiniChartWidget","subtitle","background","textColor","trendRange","trendOffset","chartWidth","showTrending","showSettings","showDateRange","groupActivity","apiActivity","incidentActivity","onBeforeRender","loadValues","status","Object","assign","header","error","loadStats","AdminDashboardPage","Page","pageTitle","pageSubtitle","lastUpdated","headerView","apiMetricsChart","MetricsChart","yAxis","beginAtZero","tooltip","y","onActionRefreshAll","event","button","currentTarget","querySelector","classList","add","disabled","eventBus","events","page","timestamp","alert","innerHTML","setTimeout","remove","onActionExportMetrics","chart","exportChartPng","charts","onActionViewAlerts","router","navigateTo","onActionViewSystemStatus","refreshDashboard","getCharts","apiMetrics","getStats","LoginLocationMapView","userId","mapStyle","drEnd","_drillCountry","_refreshing","mapView","_mapAvailable","MapLibreView","import","then","n","d","style","zoom","center","pitch","bearing","showNavigationControl","autoFitBounds","e","_setStatus","_fetchSummary","_applyMarkers","countryCode","Error","url","dr_end","user_id","country_code","region","entries","updateMarkers","maxCount","max","markers","latitude","longitude","entry","intensity","markerSize","isRegion","newCount","new_region_count","new_country_count","newLabel","popup","lng","lat","size","_getMarkerColor","_countryCode","_isRegion","_attachMarkerClicks","mapMarkers","forEach","glMarker","el","getElement","stopPropagation","drillDown","end","mix","s","_showDrillBar","fitBounds","onActionResetDrillDown","_hideDrillBar","bar","replace","textContent","onTabActivated","resize","message","display","AdminProfileSection","template","hasPhone","model","get","roleLabel","onActionForceVerifyEmail","Modal","confirm","_saveField","is_email_verified","onActionUnverifyEmail","onActionForceVerifyPhone","is_phone_verified","onActionUnverifyPhone","onActionChangeEmail","email","prompt","defaultValue","onActionChangePhone","phone","phone_number","onActionSetPhone","placeholder","onActionRemovePhone","save","set","toast","onActionEditUsername","username","fields","successMsg","AdminPersonalSection","hasDob","dobFormatted","dob","year","month","day","split","timezoneDisplay","timezone","hasAddress","meta","street","city","state","zip","country","addressSummary","onActionForceVerifyDob","is_dob_verified","onActionUnverifyDob","onActionEditDisplayName","name","display_name","onActionEditFirstName","first_name","onActionEditLastName","last_name","onActionEditDob","form","type","cols","onActionEditTimezone","text","metadata","onActionEditAddress","updatedMeta","toLowerCase","AdminSecuritySection","onActionSendPasswordReset","app","POST","onActionSendEmailVerification","onActionSendMagicLink","onActionSetPassword","required","help","password","onActionToggleMfa","currentMfa","requires_mfa","onActionDisableTotp","DELETE","id","onActionManagePasskeys","collection","PasskeyList","user","fetch","items","models","view","passkeys","p","toJSON","onActionEditPasskey","async","dataset","passkey","find","modelForm","PasskeyForms","edit","onActionDeletePasskey","destroy","dialog","buttons","class","dismiss","onActionViewRecoveryCodes","dataOnly","remaining","codes","onActionRevokeAllSessions","PROVIDER_ICONS","google","github","microsoft","apple","facebook","twitter","linkedin","AdminConnectedSection","connections","results","c","provider","onActionUnlink","connection","CHANNEL_LABELS","in_app","CHANNELS","AdminNotificationsSection","preferences","channels","ch","hasPreferences","keys","preferenceRows","sort","kind","kindLabel","toUpperCase","toggles","channel","checked","onActionTogglePref","AdminApiKeysSection","apiKeys","generatedToken","_loadKeys","_renderKeysList","container","rows","created","toLocaleDateString","expires","lastUsed","last_used","ips","allowed_ips","is_active","token_prefix","onActionGenerateKey","uid","expire_days","parseInt","ipsStr","ip","token","resultEl","tokenEl","onActionRevokeKey","onActionCopyToken","navigator","clipboard","writeText","AdminMetadataSection","_renderEntries","_getMetadata","val","JSON","stringify","_escapeHtml","str","div","document","createElement","onActionAddEntry","parse","onActionEditEntry","currentValue","displayValue","updated","onActionRemoveEntry","DeviceRow","TableRow","deviceIcon","dev","device","os","some","family","includes","deviceName","brand","deviceModel","browserName","ua","user_agent","major","osName","deviceMeta","UserView","User","sideNavView","setModel","profileView","personalView","securityView","connectedView","notificationsView","apiKeysView","metadataView","permsView","FormView","CATEGORY_PERMISSION_FIELDS","autosaveModelField","advPermsView","GRANULAR_PERMISSION_FIELDS","membersCollection","MemberList","groupsView","TableView","hideActivePillNames","columns","sortable","eventsCollection","IncidentEventList","model_name","model_id","eventsTable","eventsView","devicesView","UserDeviceList","clickAction","itemClass","loginMapView","loginEventsTable","LoginEventList","locationsView","TabView","tabs","Map","Logins","activeTab","pushDevices","PushDeviceList","pushDevicesView","logsCollection","LogList","logsTable","permissions","startName","endName","fieldName","format","displayFormat","separator","logsView","activityCollection","activityTable","activityView","SideNavView","activeSection","navWidth","contentPadding","enableResponsive","minWidth","sections","userMenu","ContextMenu","context","config","onActionEditUser","modelName","formConfig","UserForms","onActionClearAvatar","avatar","onActionToggleActive","onActionDeactivateUser","onActionActivateUser","showSection","sectionName","getActiveSection","showTab","tabName","getActiveTab","_onModelChange","create","VIEW_CLASS","UserTablePage","TablePage","pageName","Collection","UserList","viewDialogOptions","defaultQuery","visibility","filters","selectable","searchable","filterable","paginated","showRefresh","showAdd","showExport","emptyMessage","contextMenu","tableOptions","striped","bordered","hover","responsive","onActionEditPermissions","preventDefault","item","onActionChangePassword","attributes","autocomplete","readonly","tabindex","passwordUsage","showToggle","new_password","MOJOUtils","checkPasswordStrength","score","onPasswordChange","onActionSendInvite","send_invite","MemberView","Member","detailsView","permissionsView","PERMISSION_FIELDS","memberMenu","danger","onActionEditMembership","MemberForms","onActionViewUser","k","showModelById","onActionViewGroup","groupId","Group","j","onActionDeactivateMember","onActionActivateMember","onActionRemoveMember","MemberTablePage","formEdit","itemViewClass","batchBarLocation","batchActions","ApiKey","Model","ApiKeyList","ModelClass","ApiKeyForms","GroupView","membersView","group","addButtonLabel","onAdd","onInviteClick","childrenView","GroupList","parent","onActionAddChildGroup","addFormConfig","defaults","groupMenu","onActionEditGroup","GroupForms","detailed","onActionInviteMember","Event","f","newGroup","onActionDeactivateGroup","onActionActivateGroup","onActionViewParent","parentId","GroupTablePage","formCreate","GroupKindOptions","onActionMakeActive","setActiveGroup","DeviceLocationRow","locationText","geo","country_name","countryName","ispName","isp","asn_org","threatFlags","flags","is_vpn","is_tor","is_proxy","hasThreatFlags","DeviceView","UserDevice","deviceInfo","_getIcon","browserFull","_getBrowser","osFull","_getOS","deviceFull","_getDevice","isMobile","_isMobile","parts","ver","minor","browser","UserDeviceLocationList","user_device","deviceMenu","onActionDeleteDevice","show","duid","getByDuid","UserDeviceTablePage","UserDeviceLocationTablePage","route","tabView","GeoIPView","GeoLocatedIP","hasCoordinates","DataView","showEmptyValues","emptyValueText","networkView","riskView","blockView","source_ip","trafficCollection","trafficView","locationStr","MapView","tileLayer","menuItems","geoIPMenu","window","bootstrap","Tooltip","querySelectorAll","existing","getInstance","dispose","onActionEditLocation","EDIT_LOCATION_FORM","onActionEditSecurity","EDIT_SECURITY_FORM","onActionEditNetwork","EDIT_NETWORK_FORM","onActionRefreshGeoip","onActionBlockIp","block","reason","ttl","onActionUnblockIp","unblock","onActionWhitelistIp","whitelist","onActionUnwhitelistIp","unwhitelist","onActionThreatAnalysis","threat_analysis","onActionViewOnMap","open","onActionDeleteGeoip","confirmClass","confirmText","lookup","GeoLocatedIPTablePage","GeoLocatedIPList","itemView","tableViewOptions","evt","onLookup","showForm","tableView","_onRowView","ApiKeyView","isActive","apiKeyMenu","onActionEditKey","showModelForm","onActionDeactivateKey","confirmLabel","showLoading","hideLoading","onActionActivateKey","onActionDeleteKey","delete","ADD_FORM","EDIT_FORM","ApiKeyTablePage","onActionAdd","result","showError","showAlert","CloudWatchChart","resourceType","category","defaultDateRange","stat","buildApiParams","setStat","fetchData","DASHBOARD_CHARTS","unit","yAxisForUnit","CloudWatchDashboardPage","def","showGranularity","lazyMount","onEnter","scheduleRefresh","_refreshMounted","tier","_lazyMount","_lazyTriggered","runScheduledRefreshes","StatusStripPanel","strip","_mode","is_blocked","_onTileClick","_openHistoryDrawer","showPage","compactHeader","showTypeSwitch","showLegend","drawer","eyebrow","_humanizeSlug","charAt","StackTraceView","stackTrace","formattedStackTrace","formatStackTrace","lines","html","line","index","funcName","filePath","lineNum","colNum","startsWith","updateStackTrace","newStackTrace","HANDLER_TYPES","description","build","opts","preview","target","priority","func","HandlerBuilderView","handlerString","_parseExisting","h","selectedType","fieldValues","handlerDef","URLSearchParams","field","select","_renderFields","_updatePreview","onActionChangeType","_emitChange","o","onActionFieldChange","getValue","setValue","RuleSetView","RuleSet","matchByValue","matchByOption","MatchByOptions","opt","matchByLabel","bundleByValue","bundleByOption","BundleByOptions","bundleByLabel","triggerCount","triggerWindow","retriggerEvery","configView","rulesCollection","RuleList","rulesView","actions","divider","addFormDefaults","Configuration","Rules","onActionEditHandler","builder","scrollable","handler","onActionEditRuleset","onActionDisableRuleset","newStatus","onActionDeleteRuleset","closest","bsModal","hide","IncidentHistoryAdapter","incidentId","IncidentHistoryList","messages","transform","all","msg","content","_renderMarkdown","author","avatarUrl","attachments","addNote","history","IncidentHistory","note","media","files","markdown","post","_e","AssistantMessageView","ChatMessageView","_blockViews","_needsMarkdown","role","_setupCollapsibleMessage","blocks","blocksContainer","wrapper","appendChild","_renderTableBlock","_renderChartBlock","_renderStatBlock","_renderActionBlock","_renderListBlock","_renderAlertBlock","_renderProgressBlock","_renderFileBlock","errEl","_createCollapsibleCard","collapseId","_blockCounter","esc","bind","card","collapseEl","showCallbacks","cb","once","onShow","rowCount","colCount","col","columnKeys","row","idx","ci","preloaded","chart_type","seriesCount","series","pointCount","labels","isPie","pie","area","chartContainer","FORWARD_SERIES","stacked","grouped","crosshair_tracking","colors","show_legend","legend_position","FORWARD_PIE","cutout","show_labels","show_percentages","forward","reduce","acc","snake","camel","chartData","datasets","values","PieChart","chartView","legendPosition","SeriesChart","btnRow","btn","ws","isConnected","send","conversation_id","_conversationId","action_id","level","icons","warning","steps","doneCount","pct","plan_id","planId","stepsHtml","statusIcons","pending","in_progress","done","skipped","step","summary","filename","parsed","URL","location","href","protocol","csv","xlsx","pdf","json","download","rel","_formatBytes","row_count","expires_in","bytes","toFixed","updateProgressStep","stepId","summaryEl","allSteps","counter","textEl","requestAnimationFrame","scrollHeight","setProperty","MAX_HEIGHT","toggle","collapsed","parentNode","insertBefore","nextSibling","raw","markdownToHtml","_m","_lang","code","AssistantConversationListView","activeId","_searchTimeout","_renderItems","searchInput","_onSearchInput","input","clearTimeout","query","search","start","groups","_groupByDate","modified","timeStr","_relativeTime","userName","thumbnail","_initials","setActive","hasMore","loadMore","onActionLoadMore","nextPage","today","getFullYear","getMonth","getDate","yesterday","setDate","dataFormatter","normalizeEpoch","createdDate","onActionNewConversation","onActionDeleteConversation","dateStr","date","isNaN","diff","seconds","minutes","hours","days","w","AssistantView","conversationId","_wsHandlers","_messageIdCounter","_hasMessages","_activePlans","_requestStartTime","activeUser","conversations","AssistantConversationList","conversationListView","chatView","ChatView","theme","messageViewClass","currentUserId","showFileInput","showInput","adapter","_createAdapter","origAddMessage","addMessage","scroll","hideThinking","_setInputEnabled","_onConversationSelect","_onNewConversation","_onConversationDeleted","_subscribeWS","textarea","_autoResize","_handleKeydown","focus","_updateConnectionStatus","min","shiftKey","_sendMessage","onActionUseSuggestion","_event","onActionSend","_showChatArea","welcome","chatArea","_showWelcome","enabled","sendBtn","stopBtn","_setInputStatus","_responseTimeout","_onResponseTimeout","_hasDismiss","onActionStop","_showSystemMessage","conversation","AssistantConversation","graph","transformed","_transformMessage","_collapseMessages","_err","userMsg","toISOString","showThinking","respData","_handleAPIError","thinking","_onThinking","_onText","tool_call","_onToolCall","_onResponse","_onError","plan","_onPlan","plan_update","_onPlanUpdate","envelope","_dispatchWSMessage","connected","disconnected","reconnecting","_unsubscribeWS","off","inner","_isMyConversation","_adoptConversationId","_resetResponseTimeout","message_id","tool_calls","tool","tool_calls_made","errorText","step_id","msgView","messageViews","clearMessages","toolCalls","tc","textParts","INTERNAL_TOOLS","has","_parseBlocks","hasContent","hasTools","hasBlocks","prev","BLOCK_RE","VALID_TYPES","exec","dot","isReconnecting","onBeforeDestroy","AssistantContextAdapter","pk","_onConversationCreated","_fetchRetried","AssistantContextChat","statusEl","ta","openAssistantChat","assistant_conversation_id","newConversationId","_inferValueType","isInteger","num","STATUS_CONFIG","new","badge","investigating","paused","resolved","closed","ignored","getStatusConfig","getPriorityConfig","bg","getHeaderIcon","GeoIPSummaryCard","sourceIP","ipInfo","geoData","threatBadgeClass","threatLevel","isBlocked","isWhitelisted","blockedReason","geoModel","threat_level","_getThreatBadgeClass","is_whitelisted","blocked_reason","l","onActionViewGeoip","QuickActionsBar","incident","isResolved","isProtected","do_not_delete","onActionQuickResolve","onActionQuickPause","onActionQuickIgnore","onActionQuickReopen","onActionQuickCreateTicket","onActionQuickEscalate","current","newPriority","onActionQuickAnalyzeLlm","onActionQuickAskAi","onActionQuickProtect","onActionQuickUnprotect","LLMAnalysisResultsView","analysis","summaryHtml","hasProposedRule","proposed_ruleset_id","proposedRulesetId","mergedCount","merged_incidents","mergedIds","onActionReAnalyze","onActionViewProposedRule","ruleset","IncidentOverviewSection","quickActions","_showLlmAnalysisIfAvailable","statusCfg","priorityCfg","statusHelp","priorityHelp","details","dataView","serverParts","server","serverInfo","_resolveSourceIP","geoipCard","llmAnalysis","llm_analysis","llmResultsView","refreshAnalysis","removeChild","ip_address","firstEvent","first","HttpRequestSection","metaModel","IPIntelligenceSection","ipModel","threatView","flagsView","RuleEngineSection","ruleSet","rulesetId","rulesetModel","hasRuleset","autoDeleteEnabled","incidentProtected","delete_on_resolution","rulesetDataView","rulesTable","onActionEditLinkedRuleset","RuleSetForms","onActionViewLinkedRuleset","onActionCreateRuleFromIncident","scope","bundle_by","bundle_minutes","match_by","saveResp","rule_set","rulesCreated","_showMetadataRulePicker","EXCLUDE_FIELDS","pickerResp","submitText","cancelText","selected","Rule","field_name","comparator","value_type","IncidentTicketsSection","ticketsCollection","TicketList","ticketsTable","onActionCreateTicket","TicketForms","ticket","Ticket","RelatedIncidentsSection","id__not","relatedCollection","IncidentList","relatedTable","IncidentView","Incident","incidentIcon","_sourceIP","overviewSection","_handleIncidentUpdated","_handleCreateTicket","_handleAnalyzeLlm","_handleAskAi","eventsSection","ruleEngineSection","ticketsSection","http_method","http_path","httpSection","ipIntelSection","stack_trace","traceback","hasStackTrace","hasMetadata","stackTraceSection","metadataSection","_buildMetadataSection","sideNav","_buildContextMenu","knownFields","knownKeys","hasStructuredFields","structuredView","incidentMenu","onActionSetStatusOpen","onActionSetStatusInvestigating","onActionSetStatusPaused","onActionSetStatusResolved","onActionSetStatusIgnored","onActionProtectIncident","onActionRemoveProtection","onActionChangePriority","onActionEditIncident","IncidentForms","onActionBlockSourceIp","onActionViewSourceGeoip","onActionMergeIncidents","mergeIds","merge_ids","merge","onActionAskAi","onActionAnalyzeLlm","analyze","errorMsg","_pollAnalysisProgress","attempt","poll","analysis_in_progress","onActionDeleteIncident","PriorityQueueView","allowActions","priority__gte","status__in","isLoading","hasError","isEmpty","showActions","_fetchSafely","_rowFor","sevClass","sevLabel","severityClass","severityLabel","ageLabel","sourceIp","eventCount","event_count","onActionOpenIncident","onActionResolveIncident","onActionPauseIncident","TicketNoteAdapter","ticketId","TicketNoteList","TicketNote","qa","getStatusBadge","cfg","TicketDescriptionSection","descriptionHtml","renderMarkdown","LinkedIncidentCard","incidentTitle","incidentStatus","incidentPriority","incidentCategory","incidentScope","statusBadge","TicketView","assignee","assigneeName","hasDescription","linkedIncident","descriptionView","getCurrentUserId","inputPlaceholder","inputButtonText","ticketMenu","currentUser","onActionRefreshNotes","onActionEditTicket","onActionChangeStatus","onActionSetPriority","onActionAssignUser","labelField","valueField","onActionCloseTicket","TicketsQueueView","assigneeLabel","onActionOpenTicket","onActionResolveTicket","onActionPauseTicket","SLUGS","ThreatCompositionChart","range","_reflectRange","is7d","is30d","is90d","_openDayDrawer","onActionSetRange","setQuickRange","x","breakdown","_esc","FAMILIES","GeographyPanel","activeFamily","inlineMap","_reflectFamilies","leaderboard","leaderboardEmpty","families","active","inlineMapHtml","_leaderboardHtml","MetricsCountryMapView","_currentCategory","maxCountries","metricLabel","_currentLabel","mapOptions","interactive","_fetchLeaderboard","onActionShowMap","metrics","_buildLeaderboard","cc","pop","total","a","b","onActionSetFamily","onActionOpenCountry","openCountryDrawer","safeCc","encodeURIComponent","STATUS_COLORS","PRIORITY_BUCKETS","DistributionStrip","_statusData","priorityRows","priorityEmpty","funnelRows","funnelEmpty","statusDonut","centerLabel","centerSubLabel","_openStatusDrawer","_fetch","statusRows","_fetchTopByField","totalIncidents","r","_buildStatusSlices","_bucketByPriority","sums","_buildFunnel","every","_field","_size","buckets","bucket","percent","safeLabel","onActionOpenPriority","lo","hi","escHtml","DEFAULT_CATEGORY_EXCLUDES","TopSourcesPanel","allowBlock","excludeCategories","cats","ipsEmpty","catsEmpty","_withRank","rank","WINDOW_DAYS","catFilters","category__not_in","ipsRaw","catsRaw","_fetchTop","extraFilters","onActionOpenIp","safeIp","onActionOpenCategory","cat","safeCat","SUB_TILE_CATEGORIES","AuthFailuresPanel","_fetchSubTiles","next","onActionOpenSubTile","HealthStrip","dots","empty","_fetchedOnce","_reflectState","_normalize","_buildSummary","when","last_seen","incident_id","hasIncident","crit","SecurityDashboardPage","canManageSecurity","hasPermission","statusStrip","priorityQueue","ticketsQueue","composition","geography","distributions","topSources","authFailures","healthStrip","_refreshLazyMounted","lazy","IncidentTablePage","onActionBatchResolve","getSelectedItems","onActionBatchOpen","onActionBatchPause","onActionBatchIgnore","onActionBatchMerge","_element","parentModel","onActionBatchProtect","MODEL_REGISTRY","userdevice","userdevicelocation","UserDeviceLocation","geolocatedip","member","incidentevent","IncidentEvent","job","Job","log","Log","apikey","EventView","eventIcon","getIconForEvent","overviewView","Overview","stackTraceView","eventMenu","onActionViewIncident","onActionViewModel","objectId","onActionDeleteEvent","EventTablePage","IncidentEventForms","category__not","TicketTablePage","editable","editableOptions","placeHolder","TicketCategories","RuleSetTablePage","RuleSetList","onActionBatchEnable","onActionBatchDisable","onActionBatchDelete","EmailDomainTablePage","EmailDomainList","EmailDomainForms","onActionEditAwsCreds","credentials","onActionOnboard","EmailDomain","formData","onboard","onActionAudit","audit","onActionReconcile","reconcile","EmailMailboxTablePage","MailboxList","MailboxForms","onActionSendEmail","from_email","Mailbox","sendEmail","onActionSendTemplateEmail","EmailHtmlPreviewView","renderHtmlInIframe","iframe","htmlContent","iframeDoc","contentDocument","contentWindow","write","close","onActionRefreshPreview","EmailTemplateView","EmailTemplate","hasHtml","hasText","EmailTemplateTablePage","EmailTemplateList","EmailTemplateForms","formDialogConfig","EmailView","SentMessage","hasContext","SentMessageTablePage","SentMessageList","STATUS_BADGE","humanizeKey","PublicMessageView","PublicMessage","kindVal","kindOpt","PublicMessageKindOptions","statusLabel","statusBadgeClass","toggleBtnClass","toggleBtnIcon","toggleBtnLabel","subject","replySubject","safeMailtoEmail","submitterView","colSize","metadataKeys","detailFields","PublicMessageMetadataLabels","onActionToggleStatus","PublicMessageTablePage","PublicMessageList","PublicMessageStatusOptions","onActionBatchMarkClosed","failed","succeeded","PhoneNumberView","PhoneNumber","carrierView","addressView","Carrier","Address","Metadata","ctxMenu","onActionRefreshLookup","number","force_refresh","onActionDeletePhone","MODEL_CLASS","PhoneNumberTablePage","PhoneNumberList","addButtonIcon","SMSView","SMS","messageView","deliveryView","Message","Delivery","onActionRefreshSms","onActionDeleteSms","SMSTablePage","SMSList","PushDashboardPage","deliveriesChart","statusChart","recentDeliveries","PushDeliveryList","_sort","_limit","failedDeliveries","PushConfigTablePage","PushConfigList","PushConfigForms","pageSizes","defaultPageSize","emptyIcon","PushTemplateTablePage","PushTemplateList","PushTemplateForms","PushDeliveryView","PushDeliveryTablePage","PushDeviceView","PushDeviceTablePage","JobStatsView","running","completed","scheduled","totals","JobsRunnersStrip","summaryClass","list","anyDown","alive","processed","jobs_processed","jobs_failed","runner_id","dotClass","badgeClass","channelsLabel","processedLabel","last_heartbeat","JobOverviewSection","jobsPublishedChart","jobsFailedChart","JobOperationsSection","onActionRunSimpleJob","executeJobAction","test","onActionRunTestJobs","tests","onActionClearStuck","channelOptions","getChannels","clearStuck","onActionClearChannel","clearChannel","onActionPurgeJobs","purgeJobs","days_old","onActionCleanupConsumers","cleanConsumers","onActionRunnerBroadcast","JobRunnerForms","broadcast","broadcastResult","JobRunner","command","timeout","actionFn","successMessage","JobDashboardPage","jobStats","JobsEngineStats","runnersStrip","jobStatsView","operationsSection","health","formatBytes","resourceBadgeClass","progressBarClass","RunnerOverviewTab","aliveBadgeClass","aliveIcon","aliveText","started","startedText","sec","uptimeText","formatUptime","ageSec","isoString","heartbeatText","heartbeatAgeText","heartbeatClass","jobsProcessed","jobsFailed","errorRate","RunnerSysinfoTab","sysinfo","sysinfoError","loading","loaded","loadSysinfo","payload","enrichSysinfo","memory","totalFmt","usedFmt","used","availableFmt","available","barClass","disk","freeFmt","free","network","bytesRecvFmt","bytes_recv","bytesSentFmt","bytes_sent","errClass","errin","errout","dropClass","dropin","dropout","cpuPct","cpu_load","cpuLoadBarClass","cpuLoadBadgeClass","cpu","freq","freqText","cpus_load","cpuCores","bootDatetime","boot_time","collectedText","datetime","onActionRefreshSysinfo","RunnerJobsTab","jobs","loadJobs","durationText","started_at","toLocaleTimeString","attemptBadgeClass","onActionRefreshJobs","onActionViewJob","jobId","runner","onActionCancelJob","cancel_request","showSuccess","RunnerLogsTab","logs","filteredLogs","logFilter","filterAllClass","filterDebugClass","filterInfoClass","filterWarnClass","filterErrorClass","loadLogs","jobsResp","jobIds","concat","levelBadgeClass","getLogLevelClass","kindDisplay","createdText","debug","onActionFilterLogs","onActionRefreshLogs","RunnerActionsTab","pingResult","onActionPing","onActionShutdown","graceful","onActionBroadcast","commandEl","timeoutEl","parseFloat","showBusy","hideBusy","onActionExport","exportData","exported_at","blob","Blob","createObjectURL","click","revokeObjectURL","RunnerDetailsView","Logs","Actions","JobRunnersSection","runnersTable","JobRunnerList","isAlive","heartbeatTime","diffMs","diffSeconds","JobRunnersPage","runnersSection","JobDetailsView","payloadView","JobEventList","JobLogList","job_id","similarView","JobList","getStatusBadgeClass","getStatusIcon","canCancel","canRetry","jobMenu","_prepareViewData","isScheduled","statusIcon","statusBgSubtle","statusTextClass","formattedDuration","getFormattedDuration","runAt","ms","runAtAbs","runAtRel","_relativeFuture","timestampMs","diffSec","past","onActionRefreshJob","cancel","onActionRetryJob","retryData","JobForms","retry","delay","newJobId","COLUMNS","future","ALL_FILTERS","CANCEL_BATCH_ACTIONS","JobTableSection","extraParams","viewOptions","columnConfig","sectionTitle","batchActionConfig","collectionParams","isAllView","tableConfig","hideActivePills","JobsTablePage","jobTableSection","BlockedIPsTablePage","onActionBatchUnblock","onActionBatchWhitelist","LogView","logIcon","getIconForLog","lvl","logContent","formattedLog","logContentView","onActionCopyLog","Details","logMenu","onActionViewIp","onActionViewDevice","onActionDeleteLog","FirewallLogTablePage","kind__startswith","BouncerSignalView","decisionBadge","allow","monitor","rawSignalsView","serverSignalsView","onActionRefresh","BouncerSignalTablePage","BouncerSignalList","BouncerDeviceView","riskBadge","blocked","medium","low","unknown","signalsTable","muid","incidentsTable","category__startswith","Signals","Incidents","BouncerDeviceTablePage","BouncerDeviceList","BotSignatureTablePage","BouncerSignatureList","BouncerSignatureForms","IPSetView","IPSet","IPSetKindBadgeOptions","isEnabled","enabledLabel","enabledBadge","sourceVal","sourceOpt","IPSetSourceOptions","sourceLabel","lastSynced","syncError","configFields","cidrView","ipsetModel","cidrData","cidrLoading","onActionCopyCidrs","onActionRefreshSource","refresh_source","onActionSyncFleet","sync","onActionEnableIpset","enable","onActionDisableIpset","disable","onActionEditIpset","IPSetForms","onActionDeleteIpset","IPSetTablePage","IPSetList","_handleAdd","countryOpt","CommonBlockCountries","source","onActionBatchSync","onActionBatchRefresh","LogTablePage","MetricsPermissionsView","MetricsPermission","onActionEdit","MetricsForms","onActionDelete","MetricsPermissionsTablePage","MetricsPermissionList","Setting","SettingList","SettingForms","maxItems","emptyFetch","debounceMs","SettingView","settingMenu","onActionEditSetting","onActionDeleteSetting","SettingTablePage","FileManagerTablePage","FileManagerList","FileManagerForms","onActionEditOwners","owners","onActionCheckCors","check_cors","onActionTestConnection","test_connection","onActionEditCredentials","onActionClone","clone","FileTablePage","FileList","FileForms","FileView","handleFileUpload","enableFileDrop","acceptedTypes","maxFileSize","multiple","validateOnDrop","fileInput","accept","file","maxSize","_formatFileSize","fileModel","File","extra","requiresGroup","activeGroup","upload","showToast","onProgress","progressInfo","percentage","onComplete","onError","pow","onFileDrop","validation","applyFileDropMixin","S3BucketTablePage","S3BucketList","S3BucketForms","getShortUrl","serverUrl","base","shortlink_base_url","origin","ShortLinkPreviewSection","_syncFromModel","flat","flattenShortLinkMetadata","ogTitle","og_title","ogDescription","og_description","ogImage","og_image","hasOg","domain","hostname","getDomain","refreshFromModel","ShortLinkMetricsSection","trackClicks","canShowMetrics","metricsChart","ShortLinkMetadataSection","submitButtonText","submitButtonClass","TWITTER_CARD_OPTIONS","onSubmit","_saveMetadata","_targetModel","buildShortLinkMetadata","ShortLinkView","ShortLink","previewSection","_refreshHeader","shortUrl","activeLabel","activeBadge","isExpired","isShortLinkExpired","hasHits","_buildSideNav","SHORTLINK_SOURCE_OPTIONS","clickCollection","ShortLinkClickList","shortlink","clicksTable","metricsSection","onActionCopyShortUrl","onActionOpenDestination","onActionEnableShortlink","onActionDisableShortlink","onActionEditShortlink","seed","ShortLinkForms","extractShortLinkPayload","onActionDeleteShortlink","ShortLinkTablePage","ShortLinkList","onItemEdit","_handleEdit","onActionCopyCode","ShortLinkClickTablePage","UserDeviceLocationView","_ud","_di","device_info","_geo","_getDeviceIcon","locationSummary","_getLocationSummary","countryFlag","threatColor","_getThreatColor","is_threat","is_suspicious","di","postal_code","asn","patch","string","risk_score","_riskRow","is_cloud","is_datacenter","is_mobile","is_known_attacker","is_known_abuser","menu","deviceId","onActionOpenInMaps","onActionDeleteRecord","METRIC_CATEGORIES","ec2","rds","redis","TYPE_ICONS","TYPE_LABELS","CloudWatchResourceView","resource","categories","typeLabel","metaHtml","_buildMetaItems","instance_type","private_ip","public_ip","engine","instance_class","node_type","num_nodes","JobHealthView","overall_status","runners","healthStatusClass","schedulerStatusText","schedulerStatusClass","schedulerIcon","loadHealth","runners_active","scheduler","getHealthStatusClass","setupChannelDisplay","setupSchedulerDisplay","channelsArray","channelStatus","totalJobs","queued_count","inflight_count","getChannelBadgeClass","getChannelIcon","queued","inflight","healthy","onActionRefreshHealth","onActionSystemSettings","TIER_BADGE","global","AssistantSkillView","AssistantSkill","tierCls","tierBadge","triggers","hasTriggers","hasSteps","stepCount","_buildStepsHtml","metadataJson","paramsJson","condition","onActionDeleteSkill","AssistantSkillTablePage","AssistantSkillList","AssistantConversationView","messageCount","conversationUser","transformedMessages","AssistantConversationTablePage","deleted","TIER_CONFIG","AssistantMemoryPage","_memories","_activeTier","_loading","_loadMemories","tierTabs","tierContent","_buildTierContent","escapedKey","onActionSwitchTier","onActionDeleteMemory","registerSystemPages","addToMenu","registerPage","AdminDashboardPageClass","JobDashboardPageClass","JobRunnersPageClass","JobsTablePageClass","UserTablePageClass","GroupTablePageClass","MemberTablePageClass","S3BucketTablePageClass","FileManagerTablePageClass","FileTablePageClass","ShortLinkTablePageClass","ShortLinkClickTablePageClass","IncidentTablePageClass","EventTablePageClass","LogTablePageClass","UserDeviceTablePageClass","UserDeviceLocationTablePageClass","GeoLocatedIPTablePageClass","EmailMailboxTablePageClass","EmailDomainTablePageClass","SentMessageTablePageClass","EmailTemplateTablePageClass","PublicMessageTablePageClass","IncidentDashboardPageClass","RuleSetTablePageClass","TicketTablePageClass","MetricsPermissionsTablePageClass","PushDashboardPageClass","PushConfigTablePageClass","PushTemplateTablePageClass","PushDeliveryTablePageClass","PushDeviceTablePageClass","PhoneNumberTablePageClass","SMSTablePageClass","ApiKeyTablePageClass","SettingTablePageClass","CloudWatchDashboardPageClass","BlockedIPsTablePageClass","FirewallLogTablePageClass","BouncerSignalTablePageClass","BouncerDeviceTablePageClass","BotSignatureTablePageClass","IPSetTablePageClass","AssistantSkillTablePageClass","AssistantConversationTablePageClass","AssistantMemoryPageClass","sidebar","getMenuConfig","adminMenuConfig","adminMenuItems","children","unshift","registerAssistant","closeSidebarPanel","_assistantPanel","layout","panelEl","getElementById","openSidebarPanel","focusInput","AssistantPanelView","openFullscreenModal","innerWidth","styles","styleHTML","outerHTML","_assistantConversationId","root","_assistantPopup","_assistantPopupView","openPopupWindow","savedWidth","localStorage","getItem","AssistantViewClass","resolve","AssistantView$1","noBodyPadding","resizeTimeout","assistantItem","isButton","topbar","rightItems","topbarConfig","ScheduledTask","ScheduledTaskForms"],"mappings":"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"}
|