web-mojo 2.5.8 → 2.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/admin-models.es.js +1 -1
  3. package/dist/admin.cjs.js +1 -1
  4. package/dist/admin.cjs.js.map +1 -1
  5. package/dist/admin.es.js +1 -1
  6. package/dist/admin.es.js.map +1 -1
  7. package/dist/auth.cjs.js +1 -1
  8. package/dist/auth.es.js +1 -1
  9. package/dist/charts.cjs.js +1 -1
  10. package/dist/charts.es.js +1 -1
  11. package/dist/chunks/{ChatView-C27ckVwL.js → ChatView-CPaBPG5C.js} +2 -2
  12. package/dist/chunks/{ChatView-C27ckVwL.js.map → ChatView-CPaBPG5C.js.map} +1 -1
  13. package/dist/chunks/{ContextMenu-xdLpFeau.js → ContextMenu-Dch7L988.js} +2 -2
  14. package/dist/chunks/{ContextMenu-xdLpFeau.js.map → ContextMenu-Dch7L988.js.map} +1 -1
  15. package/dist/chunks/{DataView-DBQUt_vV.js → DataView-DhhTCP0d.js} +2 -2
  16. package/dist/chunks/{DataView-DBQUt_vV.js.map → DataView-DhhTCP0d.js.map} +1 -1
  17. package/dist/chunks/{FormView-DaZFbDWr.js → FormView-C6XZFZ6s.js} +2 -2
  18. package/dist/chunks/{FormView-DaZFbDWr.js.map → FormView-C6XZFZ6s.js.map} +1 -1
  19. package/dist/chunks/{ListView-C-jiqALE.js → ListView-McEedPG8.js} +2 -2
  20. package/dist/chunks/{ListView-C-jiqALE.js.map → ListView-McEedPG8.js.map} +1 -1
  21. package/dist/chunks/{MetricsCountryMapView-5r0SeCQw.js → MetricsCountryMapView-B9hNrGIG.js} +2 -2
  22. package/dist/chunks/{MetricsCountryMapView-5r0SeCQw.js.map → MetricsCountryMapView-B9hNrGIG.js.map} +1 -1
  23. package/dist/chunks/{Modal-Dgtnpj85.js → Modal-DfAzkbgB.js} +3 -3
  24. package/dist/chunks/{Modal-Dgtnpj85.js.map → Modal-DfAzkbgB.js.map} +1 -1
  25. package/dist/chunks/{Passkeys-B4bndv5b.js → Passkeys-C7YPKCKQ.js} +2 -2
  26. package/dist/chunks/{Passkeys-B4bndv5b.js.map → Passkeys-C7YPKCKQ.js.map} +1 -1
  27. package/dist/chunks/{TokenManager-DR5zQikX.js → TokenManager-CIGfr9LQ.js} +2 -2
  28. package/dist/chunks/{TokenManager-DR5zQikX.js.map → TokenManager-CIGfr9LQ.js.map} +1 -1
  29. package/dist/chunks/{User-DNifTiFu.js → User-DgB81g6W.js} +2 -2
  30. package/dist/chunks/{User-DNifTiFu.js.map → User-DgB81g6W.js.map} +1 -1
  31. package/dist/chunks/{UserProfileView-DugtA_qG.js → UserProfileView-Cn4x_As0.js} +2 -2
  32. package/dist/chunks/{UserProfileView-DugtA_qG.js.map → UserProfileView-Cn4x_As0.js.map} +1 -1
  33. package/dist/chunks/{WebApp-CNhEZYYG.js → WebApp-BLO494WW.js} +2 -2
  34. package/dist/chunks/{WebApp-CNhEZYYG.js.map → WebApp-BLO494WW.js.map} +1 -1
  35. package/dist/chunks/{admin-models-CdOCWMEj.js → admin-models-C8VPjEPG.js} +2 -2
  36. package/dist/chunks/{admin-models-CdOCWMEj.js.map → admin-models-C8VPjEPG.js.map} +1 -1
  37. package/dist/chunks/{exportChart-QDe89jLV.js → exportChart-9xYMybEK.js} +2 -2
  38. package/dist/chunks/{exportChart-QDe89jLV.js.map → exportChart-9xYMybEK.js.map} +1 -1
  39. package/dist/chunks/{index-D-gO-M9M.js → index-D09iCOVq.js} +2 -2
  40. package/dist/chunks/{index-D-gO-M9M.js.map → index-D09iCOVq.js.map} +1 -1
  41. package/dist/chunks/version-Bs_8ymHO.js +2 -0
  42. package/dist/chunks/version-Bs_8ymHO.js.map +1 -0
  43. package/dist/chunks/version-DlzdKqY9.js +2 -0
  44. package/dist/chunks/version-DlzdKqY9.js.map +1 -0
  45. package/dist/docit.cjs.js +1 -1
  46. package/dist/docit.es.js +1 -1
  47. package/dist/index.cjs.js +1 -1
  48. package/dist/index.es.js +1 -1
  49. package/dist/lightbox.cjs.js +1 -1
  50. package/dist/lightbox.es.js +1 -1
  51. package/dist/map.es.js +1 -1
  52. package/dist/timeline.es.js +1 -1
  53. package/dist/user-profile.es.js +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunks/version-CBPFfIng.js +0 -2
  56. package/dist/chunks/version-CBPFfIng.js.map +0 -1
  57. package/dist/chunks/version-D8Oyq5Pb.js +0 -2
  58. package/dist/chunks/version-D8Oyq5Pb.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ChatView-C27ckVwL.js","sources":["../../src/core/models/Metrics.js","../../src/core/models/System.js","../../src/core/pages/TablePage.js","../../src/core/views/list/grouping.js","../../src/core/views/navigation/SideNavView.js","../../src/core/views/data/DetailView.js","../../src/core/views/data/MetricCard.js","../../src/core/views/data/StatusPanel.js","../../src/core/views/data/Timeline.js","../../src/core/views/data/KnownFieldsCard.js","../../src/core/views/data/FilePreviewView.js","../../src/core/models/ShortLink.js","../../src/core/views/data/FileView.js","../../src/core/views/chat/ChatMessageView.js","../../src/core/views/chat/ChatInputView.js","../../src/core/views/chat/ChatView.js"],"sourcesContent":["import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\nclass MetricsPermission extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/metrics/permissions',\n id_key: 'account'\n });\n }\n}\n\nclass MetricsPermissionList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: MetricsPermission,\n endpoint: '/api/metrics/permissions',\n ...options,\n });\n }\n\n}\n\nconst MetricsForms = {\n edit: {\n title: 'Edit Metrics Permissions',\n fields: [\n { name: 'account', type: 'text', label: 'Account', columns:12 },\n { name: 'view_permissions', type: 'tags', label: 'View Permissions', help: 'Enter permissions or \"public\"', columns:12 },\n { name: 'write_permissions', type: 'tags', label: 'Write Permissions', help: 'Enter permissions', columns:12 },\n ]\n }\n};\n\nexport { MetricsPermission, MetricsPermissionList, MetricsForms };\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/* =========================\n * GeoLocatedIP\n * ========================= */\n\nconst GeoIPForms = {\n editLocation: {\n title: 'Edit Location',\n size: 'lg',\n fields: [\n { name: 'ip_address', label: 'IP Address', type: 'text', required: true, readonly: true, cols: 6 },\n { name: 'subnet', label: 'Subnet', type: 'text', cols: 6 },\n { name: 'country_name', label: 'Country', type: 'text', cols: 6 },\n { name: 'country_code', label: 'Country Code', type: 'text', cols: 6 },\n { name: 'region', label: 'Region', type: 'text', cols: 6 },\n { name: 'city', label: 'City', type: 'text', cols: 6 },\n { name: 'postal_code', label: 'Postal Code', type: 'text', cols: 6 },\n { name: 'timezone', label: 'Timezone', type: 'text', cols: 6 },\n { name: 'latitude', label: 'Latitude', type: 'number', step: 'any', cols: 6 },\n { name: 'longitude', label: 'Longitude', type: 'number', step: 'any', cols: 6 },\n ]\n },\n editSecurity: {\n title: 'Edit Security',\n size: 'md',\n fields: [\n { \n name: 'threat_level', \n label: 'Threat Level', \n type: 'select', \n cols: 12,\n options: [\n { value: '', label: 'None' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'critical', label: 'Critical' }\n ]\n },\n { name: 'is_threat', label: 'Threat', type: 'switch', cols: 6 },\n { name: 'is_suspicious', label: 'Suspicious', type: 'switch', cols: 6 },\n { name: 'is_known_attacker', label: 'Known Attacker', type: 'switch', cols: 6 },\n { name: 'is_known_abuser', label: 'Known Abuser', type: 'switch', cols: 6 },\n { name: 'risk_score', label: 'Risk Score', type: 'number', cols: 6 },\n { name: 'is_tor', label: 'TOR Exit Node', type: 'switch', cols: 6 },\n { name: 'is_vpn', label: 'VPN', type: 'switch', cols: 6 },\n { name: 'is_proxy', label: 'Proxy', type: 'switch', cols: 6 },\n { name: 'is_cloud', label: 'Cloud Provider', type: 'switch', cols: 6 },\n { name: 'is_datacenter', label: 'Datacenter', type: 'switch', cols: 6 }\n ]\n },\n editNetwork: {\n title: 'Edit Network',\n size: 'md',\n fields: [\n { name: 'asn', label: 'ASN', type: 'text', cols: 6 },\n { name: 'asn_org', label: 'ASN Organization', type: 'text', cols: 6 },\n { name: 'isp', label: 'ISP', type: 'text', cols: 12 },\n { name: 'connection_type', label: 'Connection Type', type: 'text', cols: 6 },\n { name: 'provider', label: 'Provider', type: 'text', cols: 6 },\n { name: 'is_mobile', label: 'Mobile Connection', type: 'switch', cols: 6 },\n { name: 'mobile_carrier', label: 'Mobile Carrier', type: 'text', cols: 6 },\n { name: 'last_seen', label: 'Last Seen', type: 'datetime', cols: 12 }\n ]\n }\n};\n\nclass GeoLocatedIP extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/system/geoip',\n });\n }\n\n static async lookup(ip) {\n const model = new GeoLocatedIP();\n const resp = await model.rest.GET('/api/system/geoip/lookup', { ip });\n if (resp.success && resp.data && resp.data.data) {\n return new GeoLocatedIP(resp.data.data);\n }\n return null;\n }\n}\n\n// Attach forms to model (use Location as default EDIT_FORM for TableView)\nGeoLocatedIP.EDIT_FORM = GeoIPForms.editLocation;\nGeoLocatedIP.EDIT_LOCATION_FORM = GeoIPForms.editLocation;\nGeoLocatedIP.EDIT_SECURITY_FORM = GeoIPForms.editSecurity;\nGeoLocatedIP.EDIT_NETWORK_FORM = GeoIPForms.editNetwork;\n\nclass GeoLocatedIPList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: GeoLocatedIP,\n endpoint: '/api/system/geoip',\n ...options,\n });\n }\n}\n\nexport { GeoLocatedIP, GeoLocatedIPList };\n","/**\n * TablePage - Page component that manages a TableView with URL parameter synchronization\n *\n * A clean, simplified implementation using the new TableView component.\n * Automatically syncs pagination, sorting, and filtering with URL parameters.\n *\n * @example\n * const usersPage = new TablePage({\n * pageName: 'users',\n * title: 'User Management',\n * collection: userCollection,\n * columns: [\n * { key: 'name', label: 'Name', sortable: true },\n * { key: 'email', label: 'Email' },\n * { key: 'role', label: 'Role', type: 'badge' }\n * ],\n * actions: ['view', 'edit', 'delete']\n * });\n */\n\nimport Page from '@core/Page.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport TableView from '@core/views/table/TableView.js';\nimport Collection from '@core/Collection.js';\nimport { parseFilterKey } from '@core/utils/DjangoLookups.js';\n\nclass TablePage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n pageName: options.pageName || options.name || 'table'\n });\n\n // Page configuration\n this.title = options.title || this.pageName;\n this.description = options.description || '';\n\n // Collection setup\n this.Collection = options.Collection || null;\n this.collection = options.collection || null;\n\n // our default collection query\n this.defaultQuery = options.defaultQuery || {};\n\n // Group field configuration - defaults to \"group\"\n this.groupField = options.groupField || 'group';\n\n // Store configuration for TableView\n // Map legacy property names to new ones\n this.tableViewConfig = {\n // Core table properties\n columns: options.columns || [],\n actions: options.actions || null,\n contextMenu: options.contextMenu || null,\n batchActions: options.batchActions || null,\n batchBarLocation: options.batchBarLocation || 'top',\n clickAction: options.clickAction || 'view',\n // Map legacy form properties to new names\n addForm: options.addForm || options.formFields || options.formCreate,\n editForm: options.editForm || options.formEdit || options.formFields,\n\n // Model operation configurations\n itemView: options.itemView || options.itemViewClass,\n deleteTemplate: options.deleteTemplate,\n formDialogConfig: options.formDialogConfig,\n viewDialogOptions: options.viewDialogOptions,\n\n // Features\n searchable: options.searchable !== false,\n sortable: options.sortable !== false,\n filterable: options.filterable !== false,\n paginated: options.paginated !== false,\n\n // Selection mode\n selectionMode: options.selectionMode || (options.selectable ? 'multiple' : 'none'),\n\n // Filter configuration\n filters: options.filters || options.additionalFilters || [],\n hideActivePills: options.hideActivePills || false,\n hideActivePillNames: options.hideActivePillNames || [],\n searchPlacement: options.searchPlacement || 'toolbar',\n\n // Toolbar day-range segment + chronological grouping. ListView /\n // TableView own the implementation; TablePage just forwards.\n dayRangeFilter: options.dayRangeFilter,\n groupBy: options.groupBy,\n groupHeaderTemplate: options.groupHeaderTemplate,\n groupHeaderLabel: options.groupHeaderLabel,\n groupHeaderStyle: options.groupHeaderStyle,\n\n // Row stripe — same forwarding pattern.\n rowStripe: options.rowStripe,\n\n // Display options for the HTML table element\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false,\n ...options.tableOptions\n },\n\n // Additional options\n emptyMessage: options.emptyMessage || 'No data available',\n searchPlaceholder: options.searchPlaceholder || 'Search...',\n showAdd: options.showAdd !== false,\n showExport: options.showExport !== false,\n\n // Custom handlers\n onItemView: options.onItemView,\n onItemEdit: options.onItemEdit,\n onItemDelete: options.onItemDelete,\n onAdd: options.onAdd,\n onExport: options.onExport,\n\n // Override with tableViewOptions if provided\n ...options.tableViewOptions\n };\n\n // URL synchronization\n this.urlSyncEnabled = options.urlSyncEnabled !== false;\n\n // Status tracking\n this.lastUpdated = null;\n this.isLoading = false;\n\n // Set up template\n this.template = options.template || this.buildTemplate();\n }\n\n /**\n * Build the page template\n */\n buildTemplate() {\n return `\n <div class=\"table-page-container\">\n\n <div class=\"table-container\" data-container=\"table\"></div>\n\n {{#showStatus}}\n <div class=\"table-status-bar table-status-top\">\n <div class=\"status-info\">\n <div class=\"d-flex justify-content-between w-100\">\n <span class=\"text-muted\">\n <i class=\"bi bi-clock me-1\"></i>\n Last updated: <span data-status=\"last-updated\">{{lastUpdated}}</span>\n </span>\n <span class=\"text-muted\">\n <i class=\"bi bi-list-ol me-1\"></i>\n Total records: <span data-status=\"record-count\">0</span>\n </span>\n </div>\n </div>\n </div>\n {{/showStatus}}\n\n </div>\n `;\n }\n\n /**\n * Initialize the page\n */\n async onInit() {\n await super.onInit();\n\n // Create collection if needed\n if (!this.collection) {\n if (this.Collection) {\n this.collection = new this.Collection();\n } else {\n this.collection = new Collection();\n }\n }\n\n // Apply URL query parameters to collection\n this.applyQueryToCollection();\n\n // Create TableView instance with all configuration\n // Intercept item view to add deep-link URL support\n this.tableView = new TableView({\n collection: this.collection,\n containerId: 'table',\n fetchOnMount: true,\n ...this.tableViewConfig,\n onItemView: async (model, event) => {\n // Allow subclass onItemView to fully override\n if (this.tableViewConfig.onItemView) {\n return this.tableViewConfig.onItemView(model, event);\n }\n await this.showItemDialog(model);\n }\n });\n\n // Add as child view\n this.addChild(this.tableView);\n\n // Set up event listeners\n this.setupEventListeners();\n }\n\n /**\n * Set up event listeners\n */\n setupEventListeners() {\n // Listen for collection changes to sync URL\n if (this.urlSyncEnabled && this.collection) {\n // Sync URL when collection params change\n this.collection.on('fetch:start', () => {\n this.isLoading = true;\n });\n\n this.collection.on('fetch:end', () => {\n this.isLoading = false;\n this.lastUpdated = new Date().toLocaleTimeString();\n this.updateStatusDisplay();\n });\n }\n\n // Listen for params-changed event from TableView to sync URL\n this.tableView.on('params-changed', () => {\n if (this.urlSyncEnabled) {\n this.syncUrl();\n }\n });\n\n // // Listen for table events (these also emit params-changed, but keep for backwards compatibility)\n // this.tableView.on('table:search', ({ searchTerm }) => {\n // // params-changed will handle URL sync\n // });\n\n // this.tableView.on('table:sort', ({ field }) => {\n // // params-changed will handle URL sync\n // });\n\n // this.tableView.on('table:page', ({ page }) => {\n // // params-changed will handle URL sync\n // });\n\n // Filter:edit handling lives entirely on ListView/TableView itself\n // (it owns the edit-modal flow). We do NOT listen for `filter:edit`\n // here anymore — doing so opened a second racing Modal.form. Filter\n // changes still arrive via the `params-changed` event, which drives\n // the URL sync below.\n\n // Row action events\n this.tableView.on('row:view', async ({ model }) => {\n if (this.onItemView) {\n await this.onItemView(model);\n }\n });\n\n this.tableView.on('row:edit', async ({ model }) => {\n if (this.onItemEdit) {\n await this.onItemEdit(model);\n }\n });\n\n this.tableView.on('row:delete', async ({ model }) => {\n if (this.onItemDelete) {\n await this.onItemDelete(model);\n }\n });\n\n // Table action events\n // Note: TableView will call options.onAdd if provided, but we still listen to the event\n // for backwards compatibility and to support event-based patterns\n this.tableView.on('table:add', async ({ event }) => {\n // The handler was already called by TableView if options.onAdd is set,\n // but we keep this listener for external code that might listen to 'table:add'\n // We don't call this.tableViewConfig.onAdd here to avoid double execution\n });\n\n this.tableView.on('table:export', async ({ data }) => {\n if (this.tableViewConfig.onExport) {\n await this.tableViewConfig.onExport(data);\n }\n });\n }\n\n /**\n * Apply URL query parameters to collection\n */\n applyQueryToCollection() {\n const params = {};\n const query = { ...this.defaultQuery, ...this.query };\n if (!query || Object.keys(query).length === 0) {\n return;\n }\n // Pagination\n if (query.start !== undefined) params.start = parseInt(query.start) || 0;\n if (query.size !== undefined) params.size = parseInt(query.size) || 10;\n\n // Sorting\n if (query.sort !== undefined) params.sort = query.sort;\n\n // Search\n if (query.search !== undefined) params.search = query.search;\n\n // Process all other params as potential filters\n const reservedParams = ['start', 'size', 'sort', 'search', 'page', '_item'];\n Object.entries(query).forEach(([key, value]) => {\n if (!reservedParams.includes(key) && value !== undefined && value !== '') {\n // Parse value if it looks like JSON\n if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {\n try {\n params[key] = JSON.parse(value);\n } catch (e) {\n params[key] = value;\n }\n } else {\n params[key] = value;\n }\n }\n });\n\n // Update collection params\n if (Object.keys(params).length > 0) {\n // Deduplicate simple + __in filters (prefer __in / not_in when both are present)\n Object.keys(params).forEach(key => {\n const { field, lookup } = parseFilterKey(key);\n if ((lookup === 'in' || lookup === 'not_in') && params.hasOwnProperty(field)) {\n delete params[field];\n }\n });\n\n this.collection.setParams({\n ...this.collection.params,\n ...params\n });\n }\n }\n\n /**\n * Sync URL with current table state\n */\n syncUrl(force = true) {\n if (!this.urlSyncEnabled || !this.collection || !this.getApp()?.router) {\n return;\n }\n\n // Get current URL params\n const currentUrl = new URL(window.location);\n const currentParams = {};\n for (const [key, value] of currentUrl.searchParams) {\n if (key !== 'page') {\n currentParams[key] = value;\n }\n }\n\n // Get desired params from collection\n const desiredParams = {};\n const collectionParams = this.collection.params || {};\n\n // Only include non-default values\n if (collectionParams.start) {\n desiredParams.start = collectionParams.start;\n }\n if (collectionParams.size) {\n desiredParams.size = collectionParams.size;\n }\n if (collectionParams.sort) {\n desiredParams.sort = collectionParams.sort;\n }\n if (collectionParams.search) {\n desiredParams.search = collectionParams.search;\n }\n\n // Include other filters\n Object.entries(collectionParams).forEach(([key, value]) => {\n if (!['start', 'size', 'sort', 'search'].includes(key) && value !== undefined && value !== '') {\n // Stringify complex values for URL\n if (typeof value === 'object') {\n desiredParams[key] = JSON.stringify(value);\n } else {\n desiredParams[key] = value;\n }\n }\n });\n\n // Check if there are any changes\n const hasChanges =\n Object.keys(desiredParams).some(key =>\n String(currentParams[key] || '') !== String(desiredParams[key] || '')\n ) ||\n Object.keys(currentParams).some(key =>\n !(key in desiredParams)\n );\n\n // Preserve _item param if currently set (deep-link, not a collection param)\n if (this.query._item) {\n desiredParams._item = this.query._item;\n }\n\n this.query = desiredParams;\n if (!hasChanges && !force) return;\n\n // Update URL\n this.updateBrowserUrl(desiredParams, true, false);\n }\n\n /**\n * Update status display\n */\n updateStatusDisplay() {\n if (!this.element) return;\n\n // Update last updated time\n const updatedElement = this.element.querySelector('[data-status=\"last-updated\"]');\n if (updatedElement) {\n updatedElement.textContent = this.lastUpdated || 'Never';\n }\n\n // Update record count\n const countElement = this.element.querySelector('[data-status=\"record-count\"]');\n if (countElement && this.collection) {\n const count = this.collection.meta?.count || this.collection.length();\n countElement.textContent = count;\n }\n }\n\n /**\n * Called when entering this page\n */\n async onEnter() {\n await super.onEnter();\n\n if (this.options.requiresGroup && !this.query[this.groupField] && this.getApp().activeGroup) {\n this.query[this.groupField] = this.getApp().activeGroup.id;\n }\n\n this.applyQueryToCollection();\n\n // Ensure filter pills are shown if there are active filters from URL\n if (this.tableView && this.tableView.element) {\n setTimeout(() => {\n this.tableView.updateFilterPills();\n this.tableView.updateSortIcons();\n }, 100);\n }\n\n // Deep-link: auto-open item dialog if _item param is present\n if (this.query._item) {\n this._openDeepLinkedItem(this.query._item);\n }\n }\n\n /**\n * Open a deep-linked item dialog by fetching the model and showing it\n */\n async _openDeepLinkedItem(itemId) {\n try {\n const model = await this.collection.fetchOne(itemId);\n if (model) {\n await this.showItemDialog(model);\n }\n } catch (e) {\n // Item not found or fetch failed — silently clear the param\n this._clearItemParam();\n }\n }\n\n /**\n * Show the item view dialog with deep-link URL support\n */\n async showItemDialog(model) {\n // Update URL with _item param\n this._setItemParam(model.id);\n\n // Fetch latest model data before showing the dialog\n if (this.tableView.fetchOnView) {\n try {\n Modal.loading();\n await model.fetch();\n } catch (error) {\n Modal.hideLoading(true);\n Modal.showError(error?.data?.error || error?.message || 'Failed to load item details');\n this._clearItemParam();\n return;\n } finally {\n Modal.hideLoading(true);\n }\n }\n\n const ViewClass = this.tableView.getItemViewClass(model);\n\n if (ViewClass) {\n const viewInstance = new ViewClass({ model, collection: this.collection });\n await Modal.dialog({\n header: false,\n body: viewInstance,\n size: 'lg',\n centered: false,\n ...this.tableView.getFormDialogConfig(this.tableView.getModelClass(model)),\n // View class declares its own modal presentation via static DIALOG_OPTIONS.\n ...ViewClass.DIALOG_OPTIONS,\n ...this.tableView.viewDialogOptions\n });\n } else {\n await Modal.data({\n title: `View ${this.tableView.getModelName(model)} #${model.id}`,\n model\n });\n }\n\n // Dialog closed — remove _item from URL\n this._clearItemParam();\n }\n\n /**\n * Add _item param to URL\n */\n _setItemParam(itemId) {\n const params = { ...this.query, _item: itemId };\n this.query = params;\n this.updateBrowserUrl(params, true, false);\n }\n\n /**\n * Remove _item param from URL\n */\n _clearItemParam() {\n delete this.query._item;\n this.updateBrowserUrl(this.query, true, false);\n }\n\n /**\n * Public method to refresh the table\n */\n async refresh() {\n await this.tableView.refresh();\n }\n\n /**\n * Public method to get selected items\n */\n getSelectedItems() {\n return this.tableView.getSelectedItems();\n }\n\n /**\n * Public method to clear selection\n */\n clearSelection() {\n this.tableView.clearSelection();\n }\n\n /**\n * Batch-action helper — encapsulates the confirm → save/destroy → toast →\n * refresh pattern shared by every admin batch handler.\n *\n * Pass one of three modes:\n * - { field, value, label, message? } → save({ [field]: value }) on each\n * - { destroy: true, label, message? } → destroy() each\n * - { handler: async (model) => …, label, … } → run handler(model) per item\n *\n * Behavior:\n * - Resolves to 0 (no-op) when the selection is empty.\n * - Confirms via Modal.confirm unless `confirm: false` is passed.\n * - Runs items in parallel via Promise.allSettled so one failure\n * doesn't abort the rest.\n * - Surfaces a ToastService message — success-only or \"N succeeded,\n * M failed\" — based on the settled outcomes.\n * - Always clears selection and refreshes the table on a non-cancel\n * path so the user sees the partial state on failure.\n *\n * @param {object} options\n * @param {string} [options.field] - Field name to set on each model (save mode).\n * @param {*} [options.value] - Value to assign when in save mode.\n * @param {boolean}[options.destroy=false] - Destroy each model instead of saving.\n * @param {Function}[options.handler] - Custom async (model) => * handler.\n * @param {string} [options.label='Action'] - Verb used in the confirm + toast.\n * @param {string} [options.message] - Override the confirm message.\n * @param {boolean}[options.confirm=true] - Skip the confirm prompt when false.\n * @returns {Promise<number>} The count of successful operations.\n */\n async batchAction({\n field,\n value,\n destroy = false,\n handler = null,\n label = 'Action',\n message,\n confirm = true\n } = {}) {\n const items = this.tableView ? this.tableView.getSelectedItems() : [];\n if (!items.length) return 0;\n\n if (confirm) {\n const ok = await Modal.confirm(\n message || `${label} ${items.length} item(s)?`\n );\n if (!ok) return 0;\n }\n\n const results = await Promise.allSettled(items.map(({ model }) => {\n if (handler) return handler(model);\n if (destroy) return model.destroy();\n return model.save({ [field]: value });\n }));\n\n const successes = results.filter(r => r.status === 'fulfilled').length;\n const failures = results.length - successes;\n\n const app = this.getApp ? this.getApp() : null;\n if (failures === 0) {\n app?.toast?.success?.(`${label}: ${successes} item(s) updated`);\n } else if (successes === 0) {\n app?.toast?.error?.(`${label} failed for all ${failures} item(s)`);\n } else {\n app?.toast?.warning?.(\n `${label}: ${successes} succeeded, ${failures} failed`\n );\n }\n\n this.tableView.clearSelection();\n await this.tableView.refresh();\n return successes;\n }\n\n /**\n * Handle filter edit dialog\n */\n async handleFilterEdit(filterKey) {\n const filterConfig = this.tableView.getAllAvailableFilters().find(f => f.key === filterKey);\n const currentValue = this.collection.params[filterKey];\n\n if (!filterConfig) return;\n\n // Build form field for the filter\n const field = {\n name: 'filter_value',\n label: filterConfig.label || filterKey,\n value: currentValue,\n ...filterConfig.config\n };\n\n const result = await Modal.form({\n title: `Edit ${field.label} Filter`,\n size: 'md',\n fields: [field]\n });\n\n if (result && result.filter_value !== undefined) {\n this.tableView.setFilter(filterKey, result.filter_value);\n\n if (this.collection.restEnabled) {\n this.collection.fetch();\n }\n await this.tableView.render();\n this.syncUrl();\n }\n }\n\n /**\n * Clear all filters\n */\n clearAllFilters() {\n if (!this.collection) return;\n\n // Keep only pagination and sort params\n const { start, size, sort } = this.collection.params;\n this.collection.params = { start, size };\n if (sort) this.collection.params.sort = sort;\n\n this.syncUrl();\n\n if (this.collection.restEnabled) {\n this.collection.fetch();\n } else {\n this.tableView.render();\n }\n }\n\n async onGroupChange(group) {\n if (!group || !this.collection || !this.options.requiresGroup) return;\n this.query[this.groupField] = group.id;\n this.applyQueryToCollection();\n if (this.collection && this.collection.restEnabled) {\n this.collection.fetch();\n }\n }\n\n /**\n * Cleanup on destroy\n */\n async onBeforeDestroy() {\n // Remove event listeners\n if (this.collection) {\n this.collection.off('fetch:start');\n this.collection.off('fetch:end');\n }\n\n if (this.tableView) {\n this.tableView.off('params-changed');\n this.tableView.off('table:search');\n this.tableView.off('table:sort');\n this.tableView.off('table:page');\n this.tableView.off('row:view');\n this.tableView.off('row:edit');\n this.tableView.off('row:delete');\n this.tableView.off('table:add');\n this.tableView.off('table:export');\n }\n\n await super.onBeforeDestroy();\n }\n\n /**\n * Show/hide status display\n */\n get showStatus() {\n return this.options.showStatus === true;\n }\n\n /**\n * Static factory method\n */\n static create(options = {}) {\n return new this(options);\n }\n}\n\nexport default TablePage;\n","/**\n * Built-in `groupBy*` helpers for ListView.\n *\n * Each helper returns an object `{ groupBy, groupHeaderLabel }` ready to\n * spread into the ListView constructor:\n *\n * import { groupByDay } from '@core/views/list/grouping.js';\n *\n * new ListView({\n * collection: loginEvents,\n * itemTemplate: '...',\n * ...groupByDay('created')\n * });\n *\n * Helpers always produce a stable bucket key (deterministic equality\n * regardless of input format) and a separate display formatter for the\n * `{{key}}` slot in the header template.\n *\n * Shipped helpers:\n * - `groupByDay` — chronological feeds (Today / Yesterday / May 5 / May 5, 2025)\n * - `groupByField` — categorical bucketing with explicit label maps\n * - `groupByRecency` — six fixed buckets (Today / Yesterday / This week / This month / Earlier this year / Older)\n * - `groupByBoolean` — binary on/off split with consumer-supplied labels\n *\n * Additional helpers (`groupByMonth`, `groupByYear`, `groupByLetter`, etc.)\n * are tracked in `planning/requests/listview-grouping-helpers.md` and ship\n * when a real consumer asks.\n */\n\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\n/**\n * Resolve the raw value off a model — accept either a field-name string\n * (resolved via `model.get(field)`) or an accessor function. Shared by\n * every helper in this module — do not duplicate when adding new ones.\n * @private\n */\nfunction resolveAccessor(fieldOrAccessor) {\n if (typeof fieldOrAccessor === 'function') return fieldOrAccessor;\n if (typeof fieldOrAccessor === 'string') {\n return (model) => (model && typeof model.get === 'function' ? model.get(fieldOrAccessor) : null);\n }\n throw new TypeError('grouping helper expects a field name string or an accessor function');\n}\n\n/**\n * Convert a raw date-ish value (epoch seconds / epoch ms / ISO string /\n * Date instance) into a JavaScript Date in local time, or null when the\n * value is missing / unparseable. Uses `dataFormatter.normalizeEpoch`\n * (matches the existing AssistantConversationListView convention).\n * Shared by all date-bucket helpers — do not duplicate when adding new ones.\n * @private\n */\nfunction toDate(raw) {\n if (raw == null || raw === '') return null;\n try {\n const ms = dataFormatter.normalizeEpoch(raw);\n if (ms === '' || ms == null || Number.isNaN(ms)) return null;\n const d = ms instanceof Date ? ms : new Date(ms);\n return Number.isNaN(d.getTime()) ? null : d;\n } catch (_err) {\n return null;\n }\n}\n\n/**\n * Format a YYYY-MM-DD bucket key into a human display label, relative to\n * the local \"today\" / \"yesterday\" anchor:\n * - 2026-05-09 → \"Today\" (when today is 2026-05-09)\n * - 2026-05-08 → \"Yesterday\"\n * - 2026-04-25 → \"Apr 25\" (current year)\n * - 2025-12-19 → \"Dec 19, 2025\" (prior year)\n * @private\n */\nfunction formatDayLabel(bucketKey) {\n if (!bucketKey || typeof bucketKey !== 'string') return '';\n\n const parts = bucketKey.split('-');\n if (parts.length !== 3) return bucketKey;\n const year = Number(parts[0]);\n const month = Number(parts[1]);\n const day = Number(parts[2]);\n if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return bucketKey;\n\n const now = new Date();\n const todayKey = isoDayKey(now);\n const yesterdayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n const yesterdayKey = isoDayKey(yesterdayDate);\n\n if (bucketKey === todayKey) return 'Today';\n if (bucketKey === yesterdayKey) return 'Yesterday';\n\n const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n const monthLabel = months[month - 1] || '';\n if (year === now.getFullYear()) {\n return `${monthLabel} ${day}`;\n }\n return `${monthLabel} ${day}, ${year}`;\n}\n\n/**\n * Build a stable YYYY-MM-DD bucket key from a Date — local time, so the\n * bucket aligns with the user's idea of \"the day this happened\" rather\n * than UTC.\n * @private\n */\nfunction isoDayKey(date) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\n/**\n * Day-bucketing helper for chronological feeds.\n *\n * Buckets each model into its local-day ISO key (e.g. `'2026-05-09'`).\n * Stable keys make equality deterministic regardless of input format\n * (epoch / ISO / Date). The label formatter renders 'Today' / 'Yesterday'\n * / 'May 5' / 'May 5, 2025' depending on how recent the bucket is.\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: loginEvents,\n * itemTemplate: '...',\n * ...groupByDay('created')\n * });\n *\n * @example\n * // Custom accessor for fallback fields\n * ...groupByDay((m) => m.get('updated') || m.get('created'))\n */\nexport function groupByDay(fieldOrAccessor) {\n const access = resolveAccessor(fieldOrAccessor);\n return {\n groupBy: (model) => {\n const date = toDate(access(model));\n return date ? isoDayKey(date) : null;\n },\n groupHeaderLabel: (key) => formatDayLabel(key)\n };\n}\n\n/**\n * Categorical-field bucketing helper.\n *\n * Buckets each model on the raw value at `fieldOrAccessor`, coerced to a\n * string (`String(raw)`) for deterministic equality. Three optional\n * formatting controls drive the displayed header:\n *\n * - `labels` — explicit map: `{ active: 'Active', resolved: 'Resolved' }`\n * - `format` — fallback transform applied when no `labels` entry matches\n * - `fallback` — bucket key used when raw is `null` / `undefined` / `''`\n * (omitted by default → ungrouped tail)\n *\n * `labels` wins over `format` when both are passed.\n *\n * **Falsy-but-stringable raw values** (`0`, `false`) coerce to non-empty\n * strings (`'0'`, `'false'`) and DO produce buckets — only `null` /\n * `undefined` / `''` go to the fallback / null-bucket path. If you want\n * `0` collapsed into the ungrouped tail, pass a custom accessor that\n * returns `null` for those values.\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @param {object} [opts]\n * @param {Object<string,string>} [opts.labels] - Explicit `rawKey → display` map\n * @param {Function} [opts.format] - `(rawKey) => display` fallback formatter\n * @param {string} [opts.fallback] - Bucket key when raw is null/undefined/''\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: incidents,\n * itemTemplate: '...',\n * ...groupByField('status', {\n * labels: { active: 'Active', resolved: 'Resolved', closed: 'Closed' },\n * fallback: 'Other'\n * })\n * });\n */\nexport function groupByField(fieldOrAccessor, opts = {}) {\n const access = resolveAccessor(fieldOrAccessor);\n const { labels, format, fallback } = opts;\n\n return {\n groupBy: (model) => {\n const raw = access(model);\n if (raw == null || raw === '') {\n return fallback != null ? String(fallback) : null;\n }\n return String(raw);\n },\n groupHeaderLabel: (key) => {\n if (labels && Object.prototype.hasOwnProperty.call(labels, key)) {\n return labels[key];\n }\n if (typeof format === 'function') {\n return format(key);\n }\n return key;\n }\n };\n}\n\n// Sort-ordered bucket keys so descending-by-date sort renders buckets in\n// natural reading order (Today on top, Older on bottom).\nconst RECENCY_LABELS = {\n 'recency-0-today': 'Today',\n 'recency-1-yesterday': 'Yesterday',\n 'recency-2-this-week': 'This week',\n 'recency-3-this-month': 'This month',\n 'recency-4-this-year': 'Earlier this year',\n 'recency-5-older': 'Older'\n};\n\n/**\n * Map a Date into one of six fixed recency buckets relative to local \"now\".\n * @private\n */\nfunction recencyBucketKey(date) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;\n\n const now = new Date();\n const dateKey = isoDayKey(date);\n const todayKey = isoDayKey(now);\n if (dateKey === todayKey) return 'recency-0-today';\n\n const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n if (dateKey === isoDayKey(yesterday)) return 'recency-1-yesterday';\n\n const sevenDaysAgo = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);\n if (date >= sevenDaysAgo) return 'recency-2-this-week';\n\n if (date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth()) {\n return 'recency-3-this-month';\n }\n\n if (date.getFullYear() === now.getFullYear()) {\n return 'recency-4-this-year';\n }\n\n return 'recency-5-older';\n}\n\n/**\n * Recency-bucketing helper for chronological feeds.\n *\n * Buckets each model into one of six fixed buckets relative to the local\n * current time:\n *\n * - 'Today' — same local calendar day as now\n * - 'Yesterday' — day before today\n * - 'This week' — within the previous 7 calendar days\n * - 'This month' — earlier in the current calendar month\n * - 'Earlier this year' — earlier in the current calendar year\n * - 'Older' — prior calendar years\n *\n * Bucket keys are sort-ordered (`'recency-0-today'`, `'recency-1-yesterday'`, …)\n * so a descending-by-date sort renders buckets in natural reading order.\n *\n * V1 is opinionated — no `opts` parameter. If you need different bucket\n * thresholds, override `groupHeaderLabel` after the spread (label-only\n * customization) or write an inline `groupBy` (bucket-set customization).\n *\n * **Future dates** (rare) bucket as 'Today' if same calendar day,\n * otherwise 'This week' (because `date >= sevenDaysAgo` trivially holds\n * for any future date).\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: notifications,\n * itemTemplate: '...',\n * ...groupByRecency('created')\n * });\n */\nexport function groupByRecency(fieldOrAccessor) {\n const access = resolveAccessor(fieldOrAccessor);\n return {\n groupBy: (model) => recencyBucketKey(toDate(access(model))),\n groupHeaderLabel: (key) => RECENCY_LABELS[key] || key\n };\n}\n\n// Common JSON-string-as-boolean forms. Lower-cased + trimmed before match.\nconst STRING_FALSE_VALUES = new Set(['false', '0', 'no', 'off']);\n\n/**\n * Coerce a raw value into a boolean for `groupByBoolean` bucketing.\n * Returns `null` for missing / empty inputs so they fall into the\n * ungrouped tail rather than a misleading \"false\" bucket.\n * @private\n */\nfunction coerceBoolean(raw) {\n if (raw == null) return null;\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'number') return raw !== 0;\n if (typeof raw === 'string') {\n const lower = raw.trim().toLowerCase();\n if (lower === '') return null;\n return !STRING_FALSE_VALUES.has(lower);\n }\n return Boolean(raw);\n}\n\n/**\n * Binary-flag bucketing helper.\n *\n * Buckets each model into `'true'` / `'false'` based on the raw value at\n * `fieldOrAccessor`. Missing / empty raw values (`null`, `undefined`,\n * empty string) drop into the ungrouped tail rather than misleadingly\n * collapsing to \"false\".\n *\n * **String-false carve-out:** raw string values `'false'`, `'0'`, `'no'`,\n * `'off'` (case-insensitive, trimmed) coerce to `false`. Catches the\n * common backend pattern of returning JSON booleans as strings, which\n * pure JS truthy-coercion would mis-bucket.\n *\n * Defaults to `'Yes'` / `'No'` labels — most admin-UI cases (Active /\n * Inactive, Verified / Unverified, Paid / Unpaid) will override.\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @param {object} [opts]\n * @param {string} [opts.trueLabel='Yes'] - Display label for the `true` bucket\n * @param {string} [opts.falseLabel='No'] - Display label for the `false` bucket\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: users,\n * itemTemplate: '...',\n * ...groupByBoolean('is_active', { trueLabel: 'Active', falseLabel: 'Inactive' })\n * });\n */\nexport function groupByBoolean(fieldOrAccessor, opts = {}) {\n const access = resolveAccessor(fieldOrAccessor);\n const trueLabel = opts.trueLabel != null ? opts.trueLabel : 'Yes';\n const falseLabel = opts.falseLabel != null ? opts.falseLabel : 'No';\n\n return {\n groupBy: (model) => {\n const b = coerceBoolean(access(model));\n if (b === null) return null;\n return b ? 'true' : 'false';\n },\n groupHeaderLabel: (key) => {\n if (key === 'true') return trueLabel;\n if (key === 'false') return falseLabel;\n return key;\n }\n };\n}\n\n// Default export is the helper bag — convenient for environments that\n// can't resolve the named export directly (test simple-module-loader,\n// non-ESM consumers, etc.). Keep both forms in sync as helpers are added.\nexport default {\n groupByDay,\n groupByField,\n groupByRecency,\n groupByBoolean\n};\n","/**\n * SideNavView - Left sidebar navigation with content panel\n *\n * A reusable navigation component that displays a vertical sidebar with\n * nav links, optional group labels, and icons. The content panel mounts\n * one child view at a time, switching on nav click.\n *\n * Features:\n * - Left sidebar with nav links, icons, and group dividers\n * - Active state with accent border\n * - Mount/unmount child views on section switch\n * - Responsive: collapses to dropdown on narrow containers\n * - Permission-aware: skips sections the user lacks permission for\n * - Configurable nav width and content padding\n * - Smooth fade transitions between sections\n *\n * Example Usage:\n * ```javascript\n * const sideNav = new SideNavView({\n * sections: [\n * { key: 'profile', label: 'Profile', icon: 'bi-person', view: profileView },\n * { key: 'security', label: 'Security', icon: 'bi-shield-lock', view: securityView },\n * { type: 'divider', label: 'Activity' },\n * { key: 'sessions', label: 'Sessions', icon: 'bi-clock-history', view: sessionsView },\n * ],\n * activeSection: 'profile',\n * navWidth: 200,\n * contentPadding: '1.5rem 2.5rem',\n * enableResponsive: true\n * });\n * ```\n */\n\nimport View from '@core/View.js';\n\nclass SideNavView extends View {\n constructor(options = {}) {\n const {\n sections = [],\n activeSection,\n navWidth,\n contentPadding,\n enableResponsive,\n minWidth,\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'side-nav-view',\n ...viewOptions\n });\n\n // Configuration\n this.navWidth = navWidth || 200;\n this.contentPadding = contentPadding || '1.5rem 2.5rem';\n this.enableResponsive = enableResponsive !== false;\n this.minWidth = minWidth || 500;\n\n // State\n this.sectionConfigs = []; // Full config array (including dividers)\n this.sectionViews = {}; // key → view instance\n this.sectionKeys = []; // Ordered navigable section keys\n this.activeSection = null;\n this.currentMode = 'sidebar'; // 'sidebar' or 'dropdown'\n this.resizeObserver = null;\n this.lastContainerWidth = 0;\n\n // Process sections config\n for (const config of sections) {\n this._addSectionConfig(config);\n }\n\n // Set initial active section\n this.activeSection = activeSection || this.sectionKeys[0] || null;\n\n // Bind resize handler\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Process and store a section config entry\n * @param {object} config - Section config (navigable or divider)\n * @private\n */\n _addSectionConfig(config) {\n if (config.type === 'divider') {\n this.sectionConfigs.push({ type: 'divider', label: config.label });\n return;\n }\n\n // Skip if user lacks required permission\n if (config.permissions && !this._hasPermission(config.permissions)) {\n return;\n }\n\n this.sectionConfigs.push(config);\n this.sectionKeys.push(config.key);\n\n if (config.view) {\n this.sectionViews[config.key] = config.view;\n config.view.parent = this;\n }\n }\n\n /**\n * Check if the current user has a permission\n * @param {string} perm - Permission string\n * @returns {boolean}\n * @private\n */\n _hasPermission(perm) {\n try {\n return this.getApp().activeUser.hasPerm(perm);\n } catch {\n return true; // If app isn't available yet, allow — will be checked at render\n }\n }\n\n // ───────────────────────────────────────────────\n // Template\n // ───────────────────────────────────────────────\n\n async renderTemplate() {\n // Stylesheet lives in src/core/css/core.css under \"SideNavView\".\n // Two instance-specific values (`navWidth`, `contentPadding`) are\n // applied as inline `style=\"...\"` attributes below.\n const nav = this.currentMode === 'dropdown'\n ? this._buildDropdownNav()\n : this._buildSidebarNav();\n\n const navWidthStyle = `width: ${this.navWidth}px`;\n const contentPaddingStyle = `padding: ${this.contentPadding}`;\n\n return `\n ${this.currentMode === 'dropdown' ? `\n <div class=\"snv-dropdown\">${nav}</div>\n <div class=\"snv-content\" data-container=\"snv-content\" style=\"${contentPaddingStyle}\"></div>\n ` : `\n <div class=\"snv-layout\">\n <nav class=\"snv-nav\" style=\"${navWidthStyle}\">${nav}</nav>\n <div class=\"snv-content\" data-container=\"snv-content\" style=\"${contentPaddingStyle}\"></div>\n </div>\n `}\n `;\n }\n\n /**\n * Normalize a badge config value into { text, variant } or null.\n * Accepts: number, string, { text, variant }, or falsy → null.\n * @param {*} badge\n * @returns {{text: string, variant: string} | null}\n * @private\n */\n _normalizeBadge(badge) {\n if (badge === null || badge === undefined || badge === false || badge === '') return null;\n if (typeof badge === 'number' || typeof badge === 'string') {\n return { text: String(badge), variant: 'muted' };\n }\n if (typeof badge === 'object' && badge.text !== undefined && badge.text !== null && badge.text !== '') {\n const variant = badge.variant || 'muted';\n return { text: String(badge.text), variant };\n }\n return null;\n }\n\n /**\n * Render a badge HTML fragment for a section config.\n * @param {object} config\n * @returns {string}\n * @private\n */\n _renderBadge(config) {\n const badge = this._normalizeBadge(config.badge);\n if (!badge) return '';\n return `<span class=\"snv-badge snv-badge-${this.escapeHtml(badge.variant)}\">${this.escapeHtml(badge.text)}</span>`;\n }\n\n /**\n * Build sidebar navigation HTML\n * @returns {string}\n * @private\n */\n _buildSidebarNav() {\n return this.sectionConfigs.map(config => {\n if (config.type === 'divider') {\n return `<div class=\"snv-nav-label\">${this.escapeHtml(config.label)}</div>`;\n }\n const isActive = config.key === this.activeSection;\n const icon = config.icon ? `<i class=\"bi ${this.escapeHtml(config.icon)}\"></i>` : '';\n const badge = this._renderBadge(config);\n return `<a role=\"button\" class=\"${isActive ? 'active' : ''}\" data-action=\"navigate\" data-section=\"${this.escapeHtml(config.key)}\">${icon} ${this.escapeHtml(config.label)}${badge}</a>`;\n }).join('');\n }\n\n /**\n * Build dropdown navigation HTML (responsive mode)\n * @returns {string}\n * @private\n */\n _buildDropdownNav() {\n const activeConfig = this.sectionConfigs.find(c => c.key === this.activeSection);\n const activeLabel = activeConfig ? activeConfig.label : this.sectionKeys[0];\n\n const items = this.sectionConfigs\n .filter(c => c.type !== 'divider')\n .map(config => {\n const isActive = config.key === this.activeSection;\n const badge = this._renderBadge(config);\n return `\n <li>\n <button class=\"dropdown-item ${isActive ? 'active' : ''}\"\n data-action=\"navigate\"\n data-section=\"${this.escapeHtml(config.key)}\"\n type=\"button\">\n ${config.icon ? `<i class=\"bi ${this.escapeHtml(config.icon)} me-2\"></i>` : ''}\n ${this.escapeHtml(config.label)}\n ${badge}\n ${isActive ? '<i class=\"bi bi-check-lg ms-2\"></i>' : ''}\n </button>\n </li>\n `;\n }).join('');\n\n return `\n <div class=\"dropdown\">\n <button class=\"snv-select-btn\" type=\"button\"\n data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n ${activeConfig?.icon ? `<i class=\"bi ${activeConfig.icon}\"></i>` : ''}\n <span>${this.escapeHtml(activeLabel)}</span>\n </button>\n <ul class=\"dropdown-menu w-100\">${items}</ul>\n </div>\n `;\n }\n\n // ───────────────────────────────────────────────\n // Lifecycle\n // ───────────────────────────────────────────────\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Mount the active section\n if (this.activeSection) {\n await this._mountSection(this.activeSection);\n }\n\n // Set up responsive behavior\n if (this.enableResponsive) {\n this._setupResponsive();\n }\n }\n\n async onBeforeDestroy() {\n await super.onBeforeDestroy();\n\n // Clean up resize observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('resize', this.handleResize);\n }\n\n // Destroy all section views\n for (const view of Object.values(this.sectionViews)) {\n if (view && typeof view.destroy === 'function') {\n await view.destroy();\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Section switching\n // ───────────────────────────────────────────────\n\n /**\n * Navigate to a section\n * @param {string} key - Section key\n * @returns {Promise<boolean>}\n */\n async showSection(key) {\n if (!this.sectionViews[key]) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n if (key === this.activeSection) {\n // Already active — but ensure it's mounted\n const view = this.sectionViews[key];\n if (view && view.isMounted() && this.element?.contains(view.element)) {\n return true;\n }\n }\n\n const previousSection = this.activeSection;\n this.activeSection = key;\n\n // Unmount previous section\n if (previousSection && previousSection !== key) {\n await this._unmountSection(previousSection);\n }\n\n // Mount new section\n await this._mountSection(key);\n\n // Call onSectionActivated hook after the view is mounted and visible\n const activeView = this.sectionViews[key];\n if (activeView?.onSectionActivated) {\n await activeView.onSectionActivated();\n }\n\n // Update nav visual state\n this._updateNavState(key);\n\n this.emit('section:changed', {\n activeSection: key,\n previousSection\n });\n\n return true;\n }\n\n /**\n * Mount a section view into the content area\n * @param {string} key - Section key\n * @private\n */\n async _mountSection(key) {\n const view = this.sectionViews[key];\n if (!view) return;\n\n const container = this.element?.querySelector('[data-container=\"snv-content\"]');\n if (!container) return;\n\n if (!view.isMounted()) {\n this._showContentLoading(container);\n try {\n await view.render(true, container);\n } finally {\n this._hideContentLoading(container);\n }\n }\n }\n\n /**\n * Show a lightweight spinner in the content panel\n * @param {HTMLElement} container\n * @private\n */\n _showContentLoading(container) {\n if (!container) return;\n let spinner = container.querySelector('.snv-loading');\n if (!spinner) {\n spinner = document.createElement('div');\n spinner.className = 'snv-loading';\n spinner.innerHTML = '<div class=\"spinner-border spinner-border-sm text-secondary\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div>';\n spinner.style.cssText = 'display:flex;align-items:center;justify-content:center;padding:3rem;';\n container.prepend(spinner);\n }\n }\n\n /**\n * Remove the content panel spinner\n * @param {HTMLElement} container\n * @private\n */\n _hideContentLoading(container) {\n if (!container) return;\n const spinner = container.querySelector('.snv-loading');\n if (spinner) spinner.remove();\n }\n\n /**\n * Unmount a section view\n * @param {string} key - Section key\n * @private\n */\n async _unmountSection(key) {\n const view = this.sectionViews[key];\n if (!view || !view.isMounted()) return;\n\n await view.unmount();\n }\n\n /**\n * Update nav link active state\n * @param {string} activeKey - Active section key\n * @private\n */\n _updateNavState(activeKey) {\n if (!this.element) return;\n\n // Update sidebar links\n this.element.querySelectorAll('.snv-nav a, .dropdown-item').forEach(link => {\n const section = link.dataset.section;\n if (section) {\n link.classList.toggle('active', section === activeKey);\n }\n });\n\n // Update dropdown button label\n const selectBtn = this.element.querySelector('.snv-select-btn span');\n if (selectBtn) {\n const config = this.sectionConfigs.find(c => c.key === activeKey);\n if (config) {\n selectBtn.textContent = config.label;\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Action handlers\n // ───────────────────────────────────────────────\n\n async onActionNavigate(event, el) {\n event.preventDefault();\n const section = el.dataset.section;\n if (section) {\n await this.showSection(section);\n }\n return true;\n }\n\n // ───────────────────────────────────────────────\n // Responsive\n // ───────────────────────────────────────────────\n\n /**\n * Set up responsive width detection\n * @private\n */\n _setupResponsive() {\n if (!this.element || !this.enableResponsive) return;\n\n this._updateMode();\n\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n this.handleResize();\n });\n const container = this.element.parentElement || this.element;\n this.resizeObserver.observe(container);\n } else {\n window.addEventListener('resize', this.handleResize);\n }\n }\n\n /**\n * Handle resize events\n */\n async handleResize() {\n const containerWidth = this._getContainerWidth();\n if (Math.abs(containerWidth - this.lastContainerWidth) > 50) {\n this.lastContainerWidth = containerWidth;\n await this._updateMode();\n }\n }\n\n /**\n * Get the container width\n * @returns {number}\n * @private\n */\n _getContainerWidth() {\n if (!this.element) return this.minWidth;\n const container = this.element.parentElement || this.element;\n return container.offsetWidth || this.minWidth;\n }\n\n /**\n * Check and switch between sidebar and dropdown modes\n * @private\n */\n async _updateMode() {\n const containerWidth = this._getContainerWidth();\n const newMode = containerWidth < this.minWidth ? 'dropdown' : 'sidebar';\n\n if (newMode !== this.currentMode) {\n this.currentMode = newMode;\n if (this.isMounted()) {\n await this.render();\n }\n this.emit('navigation:modeChanged', {\n mode: this.currentMode,\n containerWidth\n });\n }\n }\n\n // ───────────────────────────────────────────────\n // Public API\n // ───────────────────────────────────────────────\n\n /**\n * Get the active section key\n * @returns {string|null}\n */\n getActiveSection() {\n return this.activeSection;\n }\n\n /**\n * Get all navigable section keys\n * @returns {string[]}\n */\n getSectionKeys() {\n return [...this.sectionKeys];\n }\n\n /**\n * Get a section's view by key\n * @param {string} key - Section key\n * @returns {View|null}\n */\n getSection(key) {\n return this.sectionViews[key] || null;\n }\n\n /**\n * Add a section dynamically\n * @param {object} config - Section config\n * @param {boolean} makeActive - Whether to activate the section\n * @returns {Promise<boolean>}\n */\n async addSection(config, makeActive = false) {\n if (config.key && this.sectionViews[config.key]) {\n console.warn(`SideNavView: Section \"${config.key}\" already exists`);\n return false;\n }\n\n this._addSectionConfig(config);\n\n if (this.isMounted()) {\n await this.render();\n if (makeActive && config.key) {\n await this.showSection(config.key);\n }\n }\n\n this.emit('section:added', { config });\n return true;\n }\n\n /**\n * Remove a section dynamically\n * @param {string} key - Section key to remove\n * @returns {Promise<boolean>}\n */\n async removeSection(key) {\n const view = this.sectionViews[key];\n if (!view) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n // Destroy the view\n if (typeof view.destroy === 'function') {\n await view.destroy();\n }\n\n // Remove from data structures\n delete this.sectionViews[key];\n this.sectionKeys = this.sectionKeys.filter(k => k !== key);\n this.sectionConfigs = this.sectionConfigs.filter(c => c.key !== key);\n\n // Handle active section removal\n if (this.activeSection === key) {\n this.activeSection = this.sectionKeys[0] || null;\n }\n\n if (this.isMounted()) {\n await this.render();\n }\n\n this.emit('section:removed', { key });\n return true;\n }\n\n /**\n * Update a section's badge dynamically without re-rendering the whole nav.\n * Accepts the same shapes as the schema: number, string, { text, variant }, or falsy to clear.\n * @param {string} key - Section key\n * @param {*} value - Badge value\n * @returns {boolean} true if the section exists and was updated\n */\n setBadge(key, value) {\n const config = this.sectionConfigs.find(c => c.key === key);\n if (!config) return false;\n\n config.badge = value;\n\n if (!this.element) return true; // not rendered yet — schema update is enough\n\n // Update both sidebar (<a>) and dropdown-item (<button>) instances\n const links = this.element.querySelectorAll(`[data-section=\"${key}\"]`);\n links.forEach(link => {\n const existing = link.querySelector('.snv-badge');\n const html = this._renderBadge(config);\n if (existing) existing.remove();\n if (html) link.insertAdjacentHTML('beforeend', html);\n });\n return true;\n }\n\n /**\n * Prevent model changes from triggering a full re-render.\n * Section views manage their own model reactivity.\n */\n _onModelChange() {\n // no-op — same pattern as UserView\n }\n\n static create(options = {}) {\n return new SideNavView(options);\n }\n}\n\nexport default SideNavView;\n","/**\n * DetailView - Reusable record-detail layout\n *\n * The standard \"record viewer in a modal\" shape used across web-mojo:\n *\n * ┌──────────────────────────────────────────────────────────┐\n * │ icon Name [active] [edit] x│ ← DetailHeaderView\n * │ Optional reasoning / subtitle │\n * │ [chips] [chips] [chips] │\n * ├────────┬─────────────────────────────────────────────────┤\n * │ Rail │ │\n * │ link │ Active section content │ ← SideNavView\n * │ link │ │\n * │ link │ │\n * └────────┴─────────────────────────────────────────────────┘\n *\n * Subclass `DetailView` and pass config; override action handlers as needed.\n * Pair with `Modal.detail(view)` to get the right modal envelope (no body\n * padding, no footer, X close handled by the header).\n *\n * @example\n * class RuleSetView extends DetailView {\n * constructor(options = {}) {\n * super({\n * ...options,\n * header: {\n * icon: 'bi-gear-wide-connected',\n * titleField: 'name',\n * subtitlePath: 'metadata.reasoning',\n * chips: [\n * { icon: 'bi-tag-fill', textPath: 'category', variant: 'primary' },\n * { icon: 'bi-flag', text: m => `Priority ${m.get('priority')}` },\n * { icon: 'bi-stars', text: 'AI-proposed', variant: 'warning',\n * when: m => m.get('metadata')?.assistant_proposed }\n * ],\n * activeField: 'is_active',\n * actions: [\n * { label: 'Edit', icon: 'bi-pencil', action: 'edit-header' }\n * ],\n * contextMenu: { items: [...] }\n * },\n * sections: [ ... ], // SideNavView shape\n * activeSection: 'Overview'\n * });\n * }\n *\n * async onActionEditHeader() { ... }\n * }\n *\n * await Modal.detail(new RuleSetView({ model }));\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport MOJOUtils from '@core/utils/MOJOUtils.js';\n\n// ── DetailHeaderView ──────────────────────────────────────\n\n/**\n * The flat record-header card used at the top of `DetailView`.\n * Exported separately for cases where you want this header without the\n * full DetailView (e.g. on a Page rather than a Modal).\n *\n * The right-side action cluster reads, left-to-right:\n * [auxFn output] · [active switch] · [actions[]] · | · [⋮ context] · [✕]\n *\n * `auxFn(model) -> htmlString` is the slot for inline state read-outs\n * that don't fit the chip/badge model — presence dots, \"Last seen 4m\n * ago\" lines, attempt counters, etc. The string is rendered as TRUSTED\n * HTML (it comes from source code, not user input). Returning falsy\n * omits the wrapper entirely.\n *\n * Security note: if your `auxFn` interpolates model fields, free-text\n * user data, or any other untrusted value, you MUST escape it\n * yourself (e.g. via `MOJOUtils.escapeHtml(...)`) before composing\n * the returned string. The framework does not escape the result.\n */\nclass DetailHeaderView extends View {\n constructor(options = {}) {\n const {\n icon,\n iconTone, // optional: primary | success | warning | danger | info — toned dh-icon\n iconToneFn, // optional: (model) => tone, for state-driven icons\n iconHtml, // optional: string | (model) => htmlString — render trusted HTML in the icon slot (avatar img, custom badge, etc.) instead of the Bootstrap icon\n titleField,\n titleFn,\n titleAffix, // optional: string | (model) => htmlString — trusted HTML slot rendered next to the title (copy-button, etc.)\n subtitlePath,\n subtitleFn, // optional: (model) => string, takes precedence over subtitlePath\n subtitlePlaceholder, // optional muted text shown when subtitle is empty\n subtitleEditAction, // action name for click-to-edit on the empty placeholder\n chips = [],\n activeField,\n auxFn, // optional: (model) => htmlString — right-gutter aux block (presence, last-seen, etc.)\n actions = [],\n closable = true,\n contextMenu, // { items: [...] } | null\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'detail-header',\n // The header emits several `data-bs-toggle=\"tooltip\"` triggers\n // (active toggle, action buttons, close button). Auto-init them.\n enableTooltips: true,\n ...viewOptions\n });\n\n this.icon = icon || 'bi-file-earmark';\n this.iconTone = iconTone || null;\n this.iconToneFn = iconToneFn || null;\n this.iconHtml = iconHtml || null;\n this.titleField = titleField || null;\n this.titleFn = titleFn || null;\n this.titleAffix = titleAffix || null;\n this.subtitlePath = subtitlePath || null;\n this.subtitleFn = subtitleFn || null;\n this.subtitlePlaceholder = subtitlePlaceholder || null;\n this.subtitleEditAction = subtitleEditAction || null;\n this.chips = chips;\n this.activeField = activeField || null;\n this.auxFn = typeof auxFn === 'function' ? auxFn : null;\n this.actions = actions;\n this.closable = closable;\n this.contextMenuConfig = contextMenu || null;\n\n this.template = () => this._buildTemplate();\n }\n\n _resolveTitle() {\n if (this.titleFn) return this.titleFn(this.model) || '';\n if (this.titleField) return this.model?.get?.(this.titleField) || '';\n return '';\n }\n\n _resolveTitleAffix() {\n if (typeof this.titleAffix === 'function') return this.titleAffix(this.model) || '';\n return this.titleAffix || '';\n }\n\n _resolveSubtitle() {\n if (this.subtitleFn) return this.subtitleFn(this.model) || '';\n if (!this.subtitlePath) return '';\n return MOJOUtils.getNestedValue(this.model?.attributes || {}, this.subtitlePath) || '';\n }\n\n _resolveIconTone() {\n if (this.iconToneFn) return this.iconToneFn(this.model) || null;\n return this.iconTone;\n }\n\n _resolveIconHtml() {\n if (typeof this.iconHtml === 'function') return this.iconHtml(this.model) || '';\n return this.iconHtml || '';\n }\n\n _resolveChips() {\n return (this.chips || [])\n .filter(chip => !chip.when || chip.when(this.model))\n .map(chip => {\n let text;\n if (typeof chip.text === 'function') text = chip.text(this.model);\n else if (chip.textPath) text = this.model?.get?.(chip.textPath);\n else text = chip.text;\n\n let tooltip = '';\n if (typeof chip.tooltip === 'function') tooltip = chip.tooltip(this.model) || '';\n else if (chip.tooltip) tooltip = String(chip.tooltip);\n\n return {\n icon: chip.icon || null,\n text: text != null ? String(text) : '',\n variant: chip.variant || 'light',\n tooltip,\n // Optional kebab-cased action — when set, the chip\n // renders as a click-through <button> carrying\n // data-action so the parent view's onActionKebabName\n // fires on click. Used by UserView for the org chip.\n action: chip.action || null\n };\n })\n .filter(c => c.text !== '');\n }\n\n _buildTemplate() {\n const title = this._resolveTitle();\n const subtitle = this._resolveSubtitle();\n const chips = this._resolveChips();\n const isActive = this.activeField ? !!this.model?.get?.(this.activeField) : null;\n\n const chipsHtml = chips.map(c => {\n const iconHtml = c.icon ? `<i class=\"bi ${this.escapeHtml(c.icon)} me-1\"></i>` : '';\n const tooltipAttrs = c.tooltip\n ? ` data-bs-toggle=\"tooltip\" title=\"${this.escapeHtml(c.tooltip)}\"`\n : '';\n // Click-through chips render as buttons so the parent's\n // EventDelegate picks up `data-action`. Visually identical\n // to the static-span variant (same Bootstrap badge classes).\n if (c.action) {\n return `<button type=\"button\" class=\"badge bg-${this.escapeHtml(c.variant)} dh-chip-action border-0\" data-action=\"${this.escapeHtml(c.action)}\" style=\"cursor:pointer;\"${tooltipAttrs}>${iconHtml}${this.escapeHtml(c.text)}</button>`;\n }\n return `<span class=\"badge bg-${this.escapeHtml(c.variant)}\"${tooltipAttrs}>${iconHtml}${this.escapeHtml(c.text)}</span>`;\n }).join('');\n\n // Right-gutter aux slot — trusted HTML. Falsy result omits the wrapper.\n const auxResult = this.auxFn ? (this.auxFn(this.model) || '') : '';\n const auxHtml = auxResult ? `<div class=\"dh-aux\">${auxResult}</div>` : '';\n\n const switchHtml = this.activeField ? `\n <label class=\"dh-active-switch me-1\" data-bs-toggle=\"tooltip\" title=\"Toggle ${this.escapeHtml(this.activeField)}\">\n <input type=\"checkbox\" data-action=\"toggle-active\" ${isActive ? 'checked' : ''}>\n <span class=\"dh-track\"></span>\n <span class=\"dh-track-label\">${isActive ? 'Active' : 'Inactive'}</span>\n </label>\n ` : '';\n\n const actionsHtml = (this.actions || []).map(a => {\n const iconHtml = a.icon ? `<i class=\"bi ${this.escapeHtml(a.icon)}\"></i>` : '';\n return `<button class=\"dh-action\" data-action=\"${this.escapeHtml(a.action)}\" data-bs-toggle=\"tooltip\" title=\"${this.escapeHtml(a.title || a.label)}\">${iconHtml} ${this.escapeHtml(a.label)}</button>`;\n }).join('');\n\n const closeHtml = this.closable ? `\n <button class=\"dh-action dh-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\" data-bs-toggle=\"tooltip\" title=\"Close\">\n <i class=\"bi bi-x-lg\"></i>\n </button>\n ` : '';\n\n const contextMenuHtml = this.contextMenuConfig ? `<div data-container=\"detail-context-menu\"></div>` : '';\n\n // Group 2 (overflow + close) renders only when at least one of those is present.\n const hasGroup2 = !!this.contextMenuConfig || this.closable;\n\n const tone = this._resolveIconTone();\n const iconHtmlSlot = this._resolveIconHtml();\n\n // When iconHtml is provided (avatar img, custom badge, etc.) drop the\n // tone-tinted background/border so the slot is a frame for the\n // caller's content rather than a colored square. The caller's HTML\n // controls its own visual identity.\n const iconClass = iconHtmlSlot\n ? 'dh-icon dh-icon-image'\n : (tone ? `dh-icon dh-icon-tone-${this.escapeHtml(tone)}` : 'dh-icon');\n const iconBody = iconHtmlSlot || `<i class=\"bi ${this.escapeHtml(this.icon)}\"></i>`;\n\n // Trusted-HTML slot rendered inline next to the title (copy-button,\n // edit-pencil, etc.). Caller is responsible for escaping anything\n // that comes from user input — same contract as `auxFn`.\n const titleAffixHtml = this._resolveTitleAffix();\n\n // Stylesheet lives in src/core/css/core.css under \"DetailHeaderView\".\n return `\n <div class=\"d-flex align-items-start gap-3\">\n <div class=\"${iconClass}\">${iconBody}</div>\n <div class=\"dh-meta\" style=\"min-width: 0; flex: 1;\">\n <div class=\"dh-name-row\">\n <h2 class=\"dh-name\">${this.escapeHtml(title)}</h2>\n ${titleAffixHtml ? `<span class=\"dh-title-affix\">${titleAffixHtml}</span>` : ''}\n </div>\n ${subtitle\n ? `<p class=\"dh-subtitle\">${this.escapeHtml(subtitle)}</p>`\n : (this.subtitlePlaceholder\n ? `<p class=\"dh-subtitle dh-subtitle-empty\"${this.subtitleEditAction ? ` data-action=\"${this.escapeHtml(this.subtitleEditAction)}\" role=\"button\"` : ''}>${this.escapeHtml(this.subtitlePlaceholder)}</p>`\n : '')\n }\n ${chips.length ? `<div class=\"dh-chips\">${chipsHtml}</div>` : ''}\n </div>\n </div>\n <div class=\"dh-actions d-flex align-items-center gap-1\">\n <!-- Group 0 — aux slot (presence, last-seen, attempt counter, …) -->\n ${auxHtml}\n <!-- Group 1 — record actions (state + primary edits) -->\n ${switchHtml}\n ${actionsHtml}\n ${hasGroup2 ? `\n <span class=\"dh-group-sep\" aria-hidden=\"true\"></span>\n <!-- Group 2 — modal chrome (overflow + close) -->\n ${contextMenuHtml}\n ${closeHtml}\n ` : ''}\n </div>\n `;\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n // Mount context menu if configured\n if (this.contextMenuConfig && !this._contextMenuMounted) {\n const cm = new ContextMenu({\n containerId: 'detail-context-menu',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n ...this.contextMenuConfig\n }\n });\n this.addChild(cm);\n await cm.render();\n this._contextMenuMounted = true;\n }\n }\n\n /**\n * Re-dispatch unhandled actions to the parent DetailView so subclasses\n * can keep their handlers in one place.\n *\n * Always re-dispatches programmatically. A previous \"skip when target\n * is contained in the parent\" optimization was wrong for ContextMenu\n * items: the natural DOM bubble carries `data-action=\"menu-item-click\"`,\n * NOT the menu item's action (e.g. `edit-user`). The item action only\n * travels via the programmatic dispatch from `ContextMenu.onActionMenuItemClick`,\n * so short-circuiting based on bubble path dropped it on the floor.\n *\n * Re-dispatching unconditionally is safe: when the parent handles the\n * action and returns truthy, EventDelegate calls `stopPropagation`, so\n * the natural DOM bubble can't fire it a second time.\n */\n async onActionDefault(action, event, el) {\n const dest = this.parent;\n if (!dest || !dest.events || typeof dest.events.dispatch !== 'function') {\n return false;\n }\n return await dest.events.dispatch(action, event, el);\n }\n\n /**\n * Default implementation of the active toggle. Overridable by subclasses.\n *\n * Round-trips `model.save({ [activeField]: checked })`. The visual\n * feedback is the switch itself — no toasts:\n * - Optimistic update: `model.set` fires a change event; the\n * parent DetailView's `_onModelChange` re-renders the header\n * and the switch reflects the new state.\n * - On success: the switch stays in the new position.\n * - On failure: the revert `model.set` fires another change\n * event, the header re-renders, and the switch bounces back.\n * The bounce IS the feedback — no shouting at the user.\n */\n async onActionToggleActive(event, element) {\n if (!this.activeField) return;\n const checked = !!element.checked;\n element.disabled = true;\n try {\n this.model.set(this.activeField, checked);\n const resp = await this.model.save({ [this.activeField]: checked });\n if (resp && resp.status && resp.status >= 400) {\n throw new Error('Save failed');\n }\n this.emit('detail:updated');\n } catch (err) {\n // Revert silently — the bounce IS the feedback.\n this.model.set(this.activeField, !checked);\n } finally {\n // The element may have been replaced by a re-render between\n // the optimistic `model.set` and here; this is best-effort.\n if (element && element.isConnected) element.disabled = false;\n }\n }\n}\n\n\n// ── DetailView ────────────────────────────────────────────\n\nclass DetailView extends View {\n constructor(options = {}) {\n const {\n header = {},\n sections = [],\n activeSection,\n navWidth,\n minWidth,\n contentPadding,\n ...viewOptions\n } = options;\n\n // Always include `detail-view` as one of the classes so the framework's\n // DetailView-scoped CSS applies, even when subclasses pass their own\n // className (e.g. `user-view`, `runner-details-view`).\n const subclassClassName = viewOptions.className || '';\n const mergedClassName = subclassClassName.includes('detail-view')\n ? subclassClassName\n : (subclassClassName ? `detail-view ${subclassClassName}` : 'detail-view');\n\n super({\n ...viewOptions,\n className: mergedClassName\n });\n\n if (!this.model && options.model) {\n this.model = options.model;\n }\n\n this.headerConfig = header;\n this.sectionsConfig = sections;\n this.initialActiveSection = activeSection;\n this.navWidth = navWidth ?? 200;\n this.minWidth = minWidth ?? 600;\n // Bootstrap p-4 (1.5rem) breathing room around every section by default.\n // The padding is on the SideNavView's snv-content wrapper, so each\n // section gets uniform inset on all four sides without needing its\n // own padding utility class. Pass `contentPadding: '0'` to opt out.\n this.contentPadding = contentPadding ?? '1.5rem';\n\n this.template = `\n <div class=\"detail-view-shell\">\n <div data-container=\"detail-header\"></div>\n <div data-container=\"detail-sidenav\" class=\"detail-view-body\"></div>\n </div>\n `;\n }\n\n async onInit() {\n await this.onBeforeBuild();\n\n this.headerView = new DetailHeaderView({\n containerId: 'detail-header',\n model: this.model,\n ...this.headerConfig\n });\n // Bubble header events up so subclasses can listen via `this.on(...)`\n this.headerView.on('detail:updated', () => this.emit('detail:updated'));\n this.addChild(this.headerView);\n\n this.sideNav = new SideNavView({\n containerId: 'detail-sidenav',\n sections: this.sectionsConfig,\n activeSection: this.initialActiveSection,\n navWidth: this.navWidth,\n minWidth: this.minWidth,\n contentPadding: this.contentPadding,\n enableResponsive: true\n });\n this.addChild(this.sideNav);\n\n await this.onAfterBuild();\n }\n\n /**\n * Hook for subclasses to do work BEFORE the header + sidenav are built\n * (e.g. pre-create shared collections, instantiate section views).\n */\n async onBeforeBuild() {}\n\n /**\n * Hook for subclasses to do work AFTER the header + sidenav are built\n * (e.g. wire cross-section listeners, register sidenav badges).\n */\n async onAfterBuild() {}\n\n /** Expose the SideNavView's setBadge for subclass convenience */\n setBadge(key, value) {\n return this.sideNav?.setBadge(key, value);\n }\n\n /** Switch to a section programmatically */\n showSection(key) {\n return this.sideNav?.showSection(key);\n }\n\n /**\n * Prevent a model `change` event from triggering a full re-render of the\n * shell. Section views and the header manage their own model reactivity\n * (re-rendering themselves directly when needed). Without this, any\n * `model.set` would wipe the SideNav's mounted section out of the DOM.\n */\n _onModelChange() {\n // Update the header so chips / switch / subtitle reflect the new state\n if (this.headerView?.isMounted()) {\n this.headerView.render().catch(() => {});\n }\n }\n}\n\n// Stylesheet for DetailView + DetailHeaderView lives in\n// src/core/css/core.css under \"DetailView / DetailHeaderView\".\n\nexport { DetailView, DetailHeaderView };\nexport default DetailView;\n","/**\n * MetricCard - At-a-glance KPI card.\n *\n * A compact label / value / hint card with an optional icon and\n * an optional left-border tone accent. Designed to compose in a\n * row of 3-4 cards for \"overview\" sections (RuleSetView Overview,\n * IncidentView Overview, security dashboard).\n *\n * Example:\n * const card = new MetricCard({\n * label: 'Incidents (30d)',\n * value: 42,\n * icon: 'bi-shield-exclamation',\n * tone: 'warning',\n * hint: '14 minutes ago',\n * action: 'view-incidents' // optional — emits via parent's onActionViewIncidents\n * });\n * this.addChild(card, { containerId: 'kpi-1' });\n *\n * Tones map to Bootstrap CSS variables — `default` | `success` |\n * `warning` | `danger` | `info`. Theme-aware via tokens.\n */\n\nimport View from '@core/View.js';\n\nconst VALID_TONES = new Set(['default', 'success', 'warning', 'danger', 'info', 'primary']);\n\nclass MetricCard extends View {\n constructor(options = {}) {\n const {\n label,\n value,\n icon = null,\n valueIcon = null,\n tone = 'default',\n hint = null,\n action = null,\n ...viewOptions\n } = options;\n\n super({\n tagName: action ? 'button' : 'div',\n className: 'metric-card',\n ...viewOptions\n });\n\n this.label = label || '';\n this.value = value;\n this.icon = icon;\n this.valueIcon = valueIcon; // optional Bootstrap Icons class shown left of value\n this.tone = VALID_TONES.has(tone) ? tone : 'default';\n this.hint = hint;\n this.action = action;\n\n this.template = () => this._buildTemplate();\n }\n\n _buildTemplate() {\n const label = this._resolve(this.label, '');\n const hint = this._resolve(this.hint, null);\n const valueText = this._renderValue();\n const iconHtml = this.icon\n ? `<i class=\"bi ${this.escapeHtml(this.icon)} metric-card-icon\"></i>`\n : '';\n const valueIconHtml = this.valueIcon\n ? `<i class=\"bi ${this.escapeHtml(this.valueIcon)} metric-card-value-icon\"></i>`\n : '';\n const hintHtml = hint\n ? `<div class=\"metric-card-hint\">${this.escapeHtml(String(hint))}</div>`\n : '';\n\n // Outer element styling is handled by `className` + the tone class on the root.\n // We toggle the tone via classList in onAfterRender to avoid stomping\n // the user's className override.\n // Stylesheet is injected once at module load (see bottom of file) so\n // multiple MetricCard instances don't duplicate `<style>` blocks that\n // would fight in the cascade.\n return `\n <div class=\"metric-card-label\">${iconHtml}<span>${this.escapeHtml(String(label))}</span></div>\n <div class=\"metric-card-value\">${valueIconHtml}<span>${valueText}</span></div>\n ${hintHtml}\n `.trim();\n }\n\n _resolve(opt, fallback = '') {\n if (typeof opt === 'function') {\n try { return opt(this.model) ?? fallback; } catch (_) { return fallback; }\n }\n return opt ?? fallback;\n }\n\n _renderValue() {\n // Resolve function-valued options against the current model — same\n // pattern as `StatusPanel._resolve()` so callers can pass\n // `value: () => this._someCount()` and have it re-evaluate on every\n // render() rather than rendering the function source.\n let v = this.value;\n if (typeof v === 'function') {\n try { v = v(this.model); } catch (_) { v = null; }\n }\n if (v === null || v === undefined) return '<span class=\"text-muted\">—</span>';\n if (typeof v === 'object' && v !== null && 'text' in v) {\n return this.escapeHtml(String(v.text));\n }\n return this.escapeHtml(String(v));\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n if (!this.element) return;\n // Apply the tone class — resolve function-valued tone against the\n // current model so consumers can vary tone with state.\n const tone = this._resolve(this.tone, 'default');\n const safeTone = VALID_TONES.has(tone) ? tone : 'default';\n VALID_TONES.forEach(t => this.element.classList.remove(`metric-card-tone-${t}`));\n if (safeTone && safeTone !== 'default') {\n this.element.classList.add(`metric-card-tone-${safeTone}`);\n }\n // Apply data-action / type=\"button\" on the root when this is a clickable card\n if (this.action) {\n this.element.setAttribute('data-action', this.action);\n if (this.element.tagName === 'BUTTON' && !this.element.hasAttribute('type')) {\n this.element.setAttribute('type', 'button');\n }\n }\n }\n\n /**\n * Update the displayed value without a full re-render.\n * @param {*} value - New value (string|number|{text})\n */\n setValue(value) {\n this.value = value;\n if (!this.element) return;\n // Update only the text span; preserve the optional valueIcon.\n const valueEl = this.element.querySelector('.metric-card-value');\n if (!valueEl) return;\n const span = valueEl.querySelector(':scope > span');\n if (span) {\n span.innerHTML = this._renderValue();\n } else {\n valueEl.innerHTML = `<span>${this._renderValue()}</span>`;\n }\n }\n\n /**\n * Update the hint line without re-render.\n * @param {string|null} hint\n */\n setHint(hint) {\n this.hint = hint;\n if (!this.element) return;\n let slot = this.element.querySelector('.metric-card-hint');\n if (hint) {\n if (slot) {\n slot.textContent = String(hint);\n } else {\n slot = document.createElement('div');\n slot.className = 'metric-card-hint';\n slot.textContent = String(hint);\n this.element.appendChild(slot);\n }\n } else if (slot) {\n slot.remove();\n }\n }\n}\n\n// Stylesheet for MetricCard lives in src/core/css/core.css under \"MetricCard\".\n\nexport default MetricCard;\n","/**\n * StatusPanel - Hero \"current state\" panel for record-detail views.\n *\n * The big colored panel that opens an Overview section with a\n * dot+state read-out, a primary headline, a secondary meta line, and\n * a row of action buttons. Used in JobDetailsView (job lifecycle),\n * IncidentView (incident triage), RunnerDetailsView (alive/dead),\n * and any other record where the operator's first question is\n * \"what state is this in and what do I do next.\"\n *\n * Security note: `meta` is rendered as TRUSTED HTML so callers can\n * compose `<code>`, `<strong>`, etc. inline. The trust contract is\n * \"the string was assembled in source code, not pulled from user\n * input.\" If you interpolate model fields or other user-controlled\n * data into `meta`, you MUST escape them yourself (e.g. via\n * `MOJOUtils.escapeHtml(...)`). `state`, `headline`, and action\n * `label` are escaped automatically.\n *\n * ┌───────────────────────────────────────────────────────────┐\n * │ ● State label [ primary ] [ alt ] │\n * │ Headline line │\n * │ Optional supporting meta line · with <code>fragments</code>\n * └───────────────────────────────────────────────────────────┘\n *\n * Each option may be a static value OR a function of `model` (the\n * standard DetailHeaderView pattern) so the panel re-renders with the\n * current model state.\n *\n * @example\n * const panel = new StatusPanel({\n * model,\n * tone: m => m.get('status') === 'failed' ? 'danger' : 'info',\n * state: m => m.get('status') === 'running' ? 'Running' : 'Idle',\n * headline: m => `On ${m.get('runner_id') || '—'}`,\n * meta: m => `Last attempt ${m.get('attempt')} of ${m.get('max_retries')}`,\n * actions: m => [\n * m.canRetry?.() && { label: 'Retry now', action: 'retry-job', icon: 'bi-arrow-clockwise', variant: 'primary' },\n * m.canCancel?.() && { label: 'Cancel', action: 'cancel-job', icon: 'bi-x-circle', variant: 'outline-danger' }\n * ].filter(Boolean)\n * });\n * parent.addChild(panel, { containerId: 'status' });\n *\n * Action buttons render with `data-action=\"<action>\"` and dispatch via\n * the standard MOJO action pipeline — handlers live on whichever\n * ancestor wants to react (typically the parent section view or the\n * containing DetailView subclass).\n */\n\nimport View from '@core/View.js';\n\nconst VALID_TONES = new Set(['default', 'primary', 'success', 'info', 'warning', 'danger', 'secondary']);\n\nclass StatusPanel extends View {\n constructor(options = {}) {\n const {\n tone = 'default',\n state = '',\n headline = '',\n meta = '',\n icon = null,\n actions = [],\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'detail-status-panel',\n ...viewOptions\n });\n\n this._toneOpt = tone;\n this._stateOpt = state;\n this._headlineOpt = headline;\n this._metaOpt = meta;\n this._iconOpt = icon;\n this._actionsOpt = actions;\n\n this.template = () => this._buildTemplate();\n }\n\n // ── Resolvers ──────────────────────────────────────────────\n\n _resolve(opt, fallback = '') {\n if (typeof opt === 'function') return opt(this.model) ?? fallback;\n return opt ?? fallback;\n }\n\n _resolveTone() {\n const t = this._resolve(this._toneOpt, 'default');\n return VALID_TONES.has(t) ? t : 'default';\n }\n\n _resolveActions() {\n const arr = this._resolve(this._actionsOpt, []) || [];\n return Array.isArray(arr) ? arr.filter(Boolean) : [];\n }\n\n // ── Rendering ──────────────────────────────────────────────\n\n _buildTemplate() {\n const tone = this._resolveTone();\n const state = String(this._resolve(this._stateOpt, ''));\n const headline = String(this._resolve(this._headlineOpt, ''));\n const meta = this._resolve(this._metaOpt, ''); // trusted HTML\n const icon = this._resolve(this._iconOpt, null);\n const actions = this._resolveActions();\n\n const iconHtml = icon\n ? `<i class=\"bi ${this.escapeHtml(String(icon))} detail-status-icon\"></i>`\n : '<span class=\"detail-status-dot\"></span>';\n\n const actionsHtml = actions.map(a => {\n const variant = a.variant || 'primary';\n const btnClass = variant.startsWith('outline-')\n ? `btn-${variant}`\n : `btn-${variant}`;\n const aIcon = a.icon ? `<i class=\"bi ${this.escapeHtml(a.icon)} me-1\"></i>` : '';\n return `<button class=\"btn ${btnClass} btn-sm\" data-action=\"${this.escapeHtml(a.action || '')}\" type=\"button\">${aIcon}${this.escapeHtml(a.label || '')}</button>`;\n }).join('');\n\n return `\n <div class=\"detail-status-headline\">\n ${state ? `<div class=\"detail-status-state\">${iconHtml}${this.escapeHtml(state)}</div>` : ''}\n ${headline ? `<div class=\"detail-status-line\">${this.escapeHtml(headline)}</div>` : ''}\n ${meta ? `<div class=\"detail-status-meta\">${meta}</div>` : ''}\n </div>\n ${actions.length ? `<div class=\"detail-status-actions\">${actionsHtml}</div>` : ''}\n `.trim();\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n if (!this.element) return;\n\n // Apply tone class fresh on every render (clear any stale tone-* class\n // first) so a state change that flips tone shows up correctly.\n VALID_TONES.forEach(t => this.element.classList.remove(`tone-${t}`));\n const tone = this._resolveTone();\n if (tone && tone !== 'default') {\n this.element.classList.add(`tone-${tone}`);\n }\n }\n}\n\n// Stylesheet for StatusPanel lives in src/core/css/core.css under \"StatusPanel\".\n\nexport default StatusPanel;\nexport { StatusPanel };\n","/**\n * Timeline - Vertical event-feed primitive.\n *\n * A vertical activity feed with a hairline connector and tone-colored\n * dots. Used for incident history, job lifecycle events, recent\n * activity in user / group overviews, audit trails — anywhere the\n * record's story is \"this happened, then this, then this.\"\n *\n * Security note: each item's `detail` field is rendered as TRUSTED\n * HTML so callers can interpolate `<code>`, `<a>`, `<strong>`, etc.\n * The trust contract is \"the string was assembled in source code, not\n * pulled from user input.\" If you build `detail` from event payloads,\n * user IDs, free-text fields, or any other user-controlled data, you\n * MUST escape them yourself (e.g. via `MOJOUtils.escapeHtml(...)`).\n * `headline`, `when`, and `emptyText` are escaped automatically.\n *\n * ┌──────────────────────────────────────────────────┐\n * │ ● Headline line 4m ago │\n * │ │ Optional supporting detail │\n * │ │\n * │ ● Headline line 1h ago │\n * │ │ Detail line │\n * └──────────────────────────────────────────────────┘\n *\n * @example\n * const tl = new Timeline({\n * items: model.getEvents().map(ev => ({\n * tone: EVENT_TONE[ev.event] || 'default',\n * headline: ev.label || ev.event,\n * detail: ev.details || '',\n * when: formatRelative(ev.at)\n * }))\n * });\n * parent.addChild(tl, { containerId: 'lifecycle' });\n *\n * `items` may be an array OR a function of `model` (the standard\n * primitive pattern). When function-valued, the items list\n * re-resolves on every render so the feed reflects the latest model\n * state.\n *\n * Empty timeline renders the `emptyText` (defaults to a muted\n * \"No events yet\" placeholder).\n */\n\nimport View from '@core/View.js';\n\nconst VALID_TONES = new Set(['default', 'primary', 'success', 'info', 'warning', 'danger', 'secondary']);\n\nclass Timeline extends View {\n constructor(options = {}) {\n const {\n items = [],\n emptyText = 'No events yet.',\n limit = null, // optional max items rendered\n ...viewOptions\n } = options;\n\n super({\n tagName: 'ol',\n className: 'detail-timeline',\n ...viewOptions\n });\n\n this._itemsOpt = items;\n this.emptyText = emptyText;\n this.limit = (typeof limit === 'number' && limit > 0) ? Math.floor(limit) : null;\n\n this.template = () => this._buildTemplate();\n }\n\n // ── Resolvers ──────────────────────────────────────────────\n\n _resolveItems() {\n const raw = (typeof this._itemsOpt === 'function')\n ? this._itemsOpt(this.model)\n : this._itemsOpt;\n if (!Array.isArray(raw)) return [];\n const filtered = raw.filter(Boolean);\n return this.limit ? filtered.slice(0, this.limit) : filtered;\n }\n\n _normalizeTone(tone) {\n return VALID_TONES.has(tone) ? tone : null;\n }\n\n // ── Rendering ──────────────────────────────────────────────\n\n _buildTemplate() {\n const items = this._resolveItems();\n\n if (!items.length) {\n // The class on the root <ol> is fine; we render a fallback <li>\n // so the empty state still uses the timeline rail visually.\n return `<li class=\"detail-timeline-empty text-secondary small\">${this.escapeHtml(this.emptyText)}</li>`;\n }\n\n return items.map(it => {\n const tone = this._normalizeTone(it.tone);\n const toneCls = tone ? ` tone-${tone}` : '';\n const headline = String(it.headline ?? it.label ?? '');\n const detail = it.detail != null ? String(it.detail) : ''; // trusted HTML\n const when = it.when != null ? String(it.when) : '';\n\n return `\n <li class=\"detail-timeline-item${toneCls}\">\n <div>\n <div class=\"detail-timeline-headline\">${this.escapeHtml(headline)}</div>\n ${detail ? `<div class=\"detail-timeline-detail\">${detail}</div>` : ''}\n </div>\n ${when ? `<span class=\"detail-timeline-when\">${this.escapeHtml(when)}</span>` : ''}\n </li>\n `;\n }).join('');\n }\n\n /**\n * Replace the items source and re-render. Accepts the same shape\n * as the constructor's `items` option (array or function).\n */\n setItems(items) {\n this._itemsOpt = items ?? [];\n if (this.element) {\n return this.render();\n }\n }\n}\n\n// Stylesheet for Timeline lives in src/core/css/core.css under \"Timeline\".\n\nexport default Timeline;\nexport { Timeline };\n","/**\n * KnownFieldsCard - \"Known JSON keys promoted, raw blob below\" pattern.\n *\n * Many records carry blob-shaped JSON fields — `metadata`, `ip_info`,\n * `device_info`, `payload`, `og_metadata` — that contain a few keys\n * the framework knows about plus an open-ended bag of extras. The\n * Detail / Metadata sections of admin views typically want to:\n *\n * 1. Promote the known keys to a clean 2-column label/value layout.\n * 2. Keep the raw JSON accessible but visually subordinated.\n *\n * That's the pattern this primitive captures. Built on the existing\n * `.detail-flat-row` family for the known-keys grid and a native\n * `<details>` for the collapsible raw blob.\n *\n * ┌──────────────────────────────────────────────┐\n * │ Created by ian@example.com │\n * │ Reasoning brute-force from same /24 │\n * │ Last resolved 2026-04-21 11:42 │\n * │ │\n * │ ▶ Raw metadata │\n * └──────────────────────────────────────────────┘\n *\n * @example\n * const card = new KnownFieldsCard({\n * data: model.get('metadata') || {}, // OR (model) => object\n * knownKeys: [\n * { key: 'created_by', label: 'Created by' },\n * { key: 'last_resolved', label: 'Resolved', formatter: 'datetime' },\n * { key: 'reasoning', label: 'Reasoning' },\n * { key: 'agent_prompt', label: 'Agent prompt',\n * formatter: (v) => `<code>${escapeHtml(v)}</code>` }\n * ],\n * rawLabel: 'Raw metadata',\n * rawCollapsed: true\n * });\n * parent.addChild(card, { containerId: 'metadata-card' });\n *\n * `data` may be a plain object OR a function of `model`. `knownKeys`\n * may be a plain array OR a function of `model` for state-dependent\n * key sets.\n *\n * `formatter` per known-key may be:\n * - a string — looked up via DataFormatter.apply (e.g. 'datetime',\n * 'relative', 'filesize', 'phone'). Returns trusted HTML.\n * - a function `(value, key, data) => string` — returns trusted HTML.\n * - omitted — the value is rendered as escaped text via String(value).\n *\n * Missing keys render with the muted \"—\" placeholder so the row grid\n * stays visually consistent. Pass `hideEmpty: true` on a key to omit\n * the row entirely when the value is null/undefined/''.\n */\n\nimport View from '@core/View.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\nclass KnownFieldsCard extends View {\n constructor(options = {}) {\n const {\n data = {},\n knownKeys = [],\n rawCollapsed = true,\n rawLabel = 'Raw JSON',\n emptyText = 'No data.',\n showRaw = true,\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'detail-known-fields-card',\n ...viewOptions\n });\n\n this._dataOpt = data;\n this._knownKeysOpt = knownKeys;\n this.rawCollapsed = rawCollapsed !== false;\n this.rawLabel = rawLabel;\n this.emptyText = emptyText;\n this.showRaw = showRaw !== false;\n\n this.template = () => this._buildTemplate();\n }\n\n // ── Resolvers ──────────────────────────────────────────────\n\n _resolveData() {\n const raw = (typeof this._dataOpt === 'function')\n ? this._dataOpt(this.model)\n : this._dataOpt;\n return (raw && typeof raw === 'object') ? raw : {};\n }\n\n _resolveKnownKeys() {\n const raw = (typeof this._knownKeysOpt === 'function')\n ? this._knownKeysOpt(this.model)\n : this._knownKeysOpt;\n return Array.isArray(raw) ? raw.filter(Boolean) : [];\n }\n\n // ── Rendering ──────────────────────────────────────────────\n\n _buildTemplate() {\n const data = this._resolveData();\n const knownKeys = this._resolveKnownKeys();\n const isEmpty = !knownKeys.length && Object.keys(data).length === 0;\n\n if (isEmpty) {\n return `<div class=\"text-secondary small\">${this.escapeHtml(this.emptyText)}</div>`;\n }\n\n const rowsHtml = knownKeys\n .map(spec => this._renderRow(spec, data))\n .filter(Boolean)\n .join('');\n\n const knownHtml = rowsHtml\n ? `<div class=\"detail-known-fields-grid\">${rowsHtml}</div>`\n : '';\n\n const rawHtml = this.showRaw\n ? this._renderRaw(data)\n : '';\n\n return `${knownHtml}${rawHtml}`;\n }\n\n _renderRow(spec, data) {\n if (!spec || !spec.key) return '';\n\n const value = this._lookup(data, spec.key);\n const isMissing = value == null || value === '';\n\n if (isMissing && spec.hideEmpty) return '';\n\n const label = String(spec.label ?? spec.key);\n const valueHtml = isMissing\n ? '<span class=\"text-secondary fst-italic\">—</span>'\n : this._formatValue(value, spec, data);\n\n return `\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">${this.escapeHtml(label)}</div>\n <div class=\"detail-flat-row-value\">${valueHtml}</div>\n </div>\n `;\n }\n\n _renderRaw(data) {\n // Empty object — skip raw block entirely.\n if (!data || Object.keys(data).length === 0) return '';\n\n let json;\n try {\n json = JSON.stringify(data, null, 2);\n } catch (err) {\n json = String(data);\n }\n\n const openAttr = this.rawCollapsed ? '' : ' open';\n return `\n <details class=\"detail-known-fields-raw\"${openAttr}>\n <summary class=\"detail-known-fields-raw-summary\">${this.escapeHtml(this.rawLabel)}</summary>\n <pre class=\"detail-known-fields-raw-body\">${this.escapeHtml(json)}</pre>\n </details>\n `;\n }\n\n // Looks up a dotted path on the data object so knownKeys can reference\n // nested keys like `os.family`. Falls back to a flat lookup when the\n // path doesn't traverse anything.\n _lookup(data, key) {\n if (!key || data == null) return undefined;\n if (Object.prototype.hasOwnProperty.call(data, key)) return data[key];\n if (key.indexOf('.') === -1) return undefined;\n const parts = key.split('.');\n let cursor = data;\n for (const part of parts) {\n if (cursor == null || typeof cursor !== 'object') return undefined;\n cursor = cursor[part];\n }\n return cursor;\n }\n\n _formatValue(value, spec, data) {\n const formatter = spec.formatter;\n\n if (typeof formatter === 'function') {\n try {\n const out = formatter(value, spec.key, data);\n return out == null ? '' : String(out);\n } catch (err) {\n return this.escapeHtml(String(value));\n }\n }\n\n if (typeof formatter === 'string' && formatter.length) {\n try {\n const out = dataFormatter.apply(formatter, value);\n return out == null ? '' : String(out);\n } catch (err) {\n return this.escapeHtml(String(value));\n }\n }\n\n // Default — escaped scalar / JSON.\n if (value && typeof value === 'object') {\n return `<code class=\"text-secondary\">${this.escapeHtml(JSON.stringify(value))}</code>`;\n }\n return this.escapeHtml(String(value));\n }\n}\n\n// Stylesheet for KnownFieldsCard lives in src/core/css/core.css under \"KnownFieldsCard\".\n\nexport default KnownFieldsCard;\nexport { KnownFieldsCard };\n","import View from '@core/View.js';\n\nclass FilePreviewView extends View {\n constructor(options = {}) {\n super({\n className: 'file-preview',\n ...options\n });\n this.file = options.file || {};\n this.isImage = this.file.content_type?.startsWith('image/');\n this.isPdf = this.file.content_type === 'application/pdf';\n }\n\n getTemplate() {\n return `\n <div class=\"file-preview-item card card-body p-2 mt-2\">\n <div class=\"d-flex align-items-center\">\n <div class=\"flex-shrink-0\">\n ${this.isImage ? `<img src=\"${this.file.thumbnailUrl || this.file.url}\" class=\"rounded\" style=\"width: 40px; height: 40px; object-fit: cover;\">` : `<i class=\"bi bi-file-earmark-text fs-2 text-secondary\"></i>`}\n </div>\n <div class=\"flex-grow-1 ms-3\">\n <div class=\"fw-bold text-truncate\">{{file.filename}}</div>\n <div class=\"small text-muted\">{{file.file_size|filesize}}</div>\n </div>\n <div class=\"flex-shrink-0\">\n <button class=\"btn btn-sm btn-outline-primary\" data-action=\"view-file\">View</button>\n </div>\n </div>\n </div>\n `;\n }\n\n async onActionViewFile() {\n if (this.isImage) {\n // Check if lightbox extension is available\n const LightboxGallery = window.MOJO?.plugins?.LightboxGallery;\n \n if (LightboxGallery) {\n LightboxGallery.show({ src: this.file.url, alt: this.file.filename });\n } else {\n // Fallback: open in new tab\n window.open(this.file.url, '_blank');\n }\n } else if (this.isPdf) {\n // Check if lightbox extension is available\n const PDFViewer = window.MOJO?.plugins?.PDFViewer;\n \n if (PDFViewer) {\n PDFViewer.showDialog(this.file.url, { title: this.file.filename });\n } else {\n // Fallback: open in new tab\n window.open(this.file.url, '_blank');\n }\n } else {\n window.open(this.file.url, '_blank');\n }\n }\n}\n\nexport default FilePreviewView;\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/* =========================\n * Constants\n * ========================= */\n\nconst SHORTLINK_SOURCE_OPTIONS = [\n { value: 'admin', label: 'Admin' },\n { value: 'email', label: 'Email' },\n { value: 'sms', label: 'SMS' },\n { value: 'push', label: 'Push' },\n { value: 'fileman', label: 'File Manager' },\n { value: 'api', label: 'API' },\n { value: 'other', label: 'Other' },\n];\n\nconst TWITTER_CARD_OPTIONS = [\n { value: '', label: '— None —' },\n { value: 'summary', label: 'summary' },\n { value: 'summary_large_image', label: 'summary_large_image' },\n];\n\n/* =========================\n * Metadata helpers\n * ========================= */\n\n/**\n * Flatten a ShortLink metadata object (keys like \"og:title\") into sibling form\n * fields (og_title, twitter_card, ...). Used to seed edit dialogs.\n */\nfunction flattenShortLinkMetadata(metadata = {}) {\n const m = metadata || {};\n return {\n og_title: m['og:title'] || '',\n og_description: m['og:description'] || '',\n og_image: m['og:image'] || '',\n twitter_card: m['twitter:card'] || '',\n twitter_title: m['twitter:title'] || '',\n twitter_description: m['twitter:description'] || '',\n twitter_image: m['twitter:image'] || '',\n };\n}\n\n/**\n * Inverse of flattenShortLinkMetadata — combines og_ and twitter_ form fields\n * into a flat metadata object with colon-keyed keys. Empty values are dropped.\n */\nfunction buildShortLinkMetadata(formData = {}) {\n const map = {\n og_title: 'og:title',\n og_description: 'og:description',\n og_image: 'og:image',\n twitter_card: 'twitter:card',\n twitter_title: 'twitter:title',\n twitter_description: 'twitter:description',\n twitter_image: 'twitter:image',\n };\n const metadata = {};\n for (const [flatKey, targetKey] of Object.entries(map)) {\n const v = formData[flatKey];\n if (v !== undefined && v !== null && v !== '') {\n metadata[targetKey] = v;\n }\n }\n return metadata;\n}\n\n/**\n * Pull og_ and twitter_ fields out of a form-data object and collapse them into\n * `metadata`. Returns a new object safe to send to the REST API. If no OG or\n * Twitter fields are set, `metadata` is omitted entirely so the backend's\n * auto-scraper can fill in the gaps.\n */\nfunction extractShortLinkPayload(formData = {}) {\n const flatKeys = [\n 'og_title', 'og_description', 'og_image',\n 'twitter_card', 'twitter_title', 'twitter_description', 'twitter_image',\n ];\n const payload = { ...formData };\n for (const k of flatKeys) delete payload[k];\n const metadata = buildShortLinkMetadata(formData);\n if (Object.keys(metadata).length > 0) {\n payload.metadata = metadata;\n }\n return payload;\n}\n\n/* =========================\n * ShortLink Model\n * ========================= */\n\nclass ShortLink extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/shortlink/link',\n });\n }\n}\n\n/* =========================\n * ShortLink Collection\n * ========================= */\n\nclass ShortLinkList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ShortLink,\n endpoint: '/api/shortlink/link',\n ...options,\n });\n }\n}\n\n/* =========================\n * ShortLinkClick (read-only)\n * ========================= */\n\nclass ShortLinkClick extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/shortlink/history',\n });\n }\n}\n\nclass ShortLinkClickList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ShortLinkClick,\n endpoint: '/api/shortlink/history',\n ...options,\n });\n }\n}\n\n/* =========================\n * Forms\n * ========================= */\n\nconst _shortLinkSharedFields = [\n { name: 'url', type: 'url', label: 'Destination URL', required: true, placeholder: 'https://example.com/page', cols: 12 },\n { name: 'source', type: 'select', label: 'Source', options: SHORTLINK_SOURCE_OPTIONS, value: 'admin', cols: 6 },\n { name: 'expire_days', type: 'number', label: 'Expire (days)', value: 3, min: 0, cols: 3, help: '0 = never' },\n { name: 'expire_hours', type: 'number', label: 'Expire (hours)', value: 0, min: 0, cols: 3 },\n { name: 'track_clicks', type: 'switch', label: 'Track clicks', value: false, cols: 4, help: 'Record per-click history and per-link metrics.' },\n { name: 'bot_passthrough', type: 'switch', label: 'Bypass bot preview', value: false, cols: 4, help: 'Bots receive a plain redirect (use for transactional links).' },\n { name: 'is_protected', type: 'switch', label: 'Protected', value: false, cols: 4, help: 'Prevents accidental deletion.' },\n // ── OG Metadata (optional) ──\n { type: 'heading', label: 'OpenGraph Preview (optional)', cols: 12 },\n { name: 'og_title', type: 'text', label: 'og:title', placeholder: 'Shown in Slack/iMessage preview', cols: 12 },\n { name: 'og_description', type: 'textarea', label: 'og:description', rows: 2, cols: 12 },\n { name: 'og_image', type: 'url', label: 'og:image', placeholder: 'https://example.com/preview.jpg', cols: 12 },\n];\n\nconst ShortLinkForms = {\n create: {\n title: 'Create Shortlink',\n size: 'md',\n fields: [\n ..._shortLinkSharedFields,\n ],\n help: 'Leave OG fields blank to let the server scrape the destination automatically.',\n },\n\n edit: {\n title: 'Edit Shortlink',\n size: 'md',\n fields: [\n { name: 'is_active', type: 'switch', label: 'Active', cols: 4 },\n ..._shortLinkSharedFields,\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 },\n};\n\nShortLink.EDIT_FORM = ShortLinkForms.edit;\n\nexport {\n ShortLink,\n ShortLinkList,\n ShortLinkClick,\n ShortLinkClickList,\n ShortLinkForms,\n SHORTLINK_SOURCE_OPTIONS,\n TWITTER_CARD_OPTIONS,\n flattenShortLinkMetadata,\n buildShortLinkMetadata,\n extractShortLinkPayload,\n};\n","/**\n * FileView - Unified viewer for fileman File records\n *\n * Canonical viewer for `File` models returned by /api/fileman/file. Mirrors the\n * UserView pattern: a header block with a ContextMenu, plus a SideNavView that\n * switches between Preview / Details / Renditions / Metadata sections.\n *\n * Preview is driven by the backend `category` field (image, video, audio, pdf,\n * document, spreadsheet, presentation, archive, other). Each category has a\n * defined preview experience. Image and PDF previews delegate to the optional\n * lightbox extension (`window.MOJO.plugins.LightboxGallery` / `PDFViewer`);\n * when that extension isn't loaded, actions fall back to `window.open`.\n *\n * Features:\n * - Category-aware Preview section (inline <video>/<audio>, lightbox, pdf viewer)\n * - DataView-powered Details with all file metadata\n * - TableView-powered Renditions (hidden when there are no renditions)\n * - Auto-generated Metadata DataView from `model.metadata` (hidden when empty)\n * - ContextMenu actions: View, Download, Edit Details, Make Public/Private,\n * Copy URL, Delete\n * - Emits `file:deleted` after successful delete\n */\n\nimport View from '@core/View.js';\nimport DetailView from '@core/views/data/DetailView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport MetricCard from '@core/views/data/MetricCard.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\nimport { File, FileForms } from '@core/models/Files.js';\nimport { ShortLinkList } from '@core/models/ShortLink.js';\n\n// CATEGORY_CONFIG.badgeClass → DetailHeaderView iconTone mapping. Anything\n// not in here renders without tone tinting (the regular dh-icon background).\nconst CATEGORY_TONE = {\n 'bg-info': 'info',\n 'bg-primary': 'primary',\n 'bg-danger': 'danger',\n 'bg-success': 'success',\n 'bg-warning': 'warning'\n};\n\nfunction _capitalize(s) {\n if (!s) return '';\n const str = String(s);\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Category → preview config\n// ──────────────────────────────────────────────────────────────────────────\n\nconst CATEGORY_CONFIG = {\n image: { icon: 'bi-image', previewType: 'image', badgeClass: 'bg-info' },\n video: { icon: 'bi-camera-video', previewType: 'video', badgeClass: 'bg-primary' },\n audio: { icon: 'bi-music-note-beamed', previewType: 'audio', badgeClass: 'bg-primary' },\n pdf: { icon: 'bi-file-earmark-pdf', previewType: 'pdf', badgeClass: 'bg-danger' },\n document: { icon: 'bi-file-earmark-text', previewType: 'document', badgeClass: 'bg-secondary' },\n spreadsheet: { icon: 'bi-file-earmark-spreadsheet', previewType: 'document', badgeClass: 'bg-success' },\n presentation: { icon: 'bi-file-earmark-slides', previewType: 'document', badgeClass: 'bg-warning' },\n archive: { icon: 'bi-file-earmark-zip', previewType: 'download', badgeClass: 'bg-dark' },\n other: { icon: 'bi-file-earmark', previewType: 'download', badgeClass: 'bg-secondary' }\n};\n\nfunction getCategoryConfig(model) {\n const cat = (model && typeof model.getCategory === 'function')\n ? model.getCategory()\n : (model?.get?.('category') || 'other');\n return CATEGORY_CONFIG[cat] || CATEGORY_CONFIG.other;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FilePreviewSection — category-aware preview (inside SideNavView content)\n//\n// NOTE: SideNavView assigns `view.parent = this` to its section views\n// (SideNavView.js:102), so events bubble to SideNavView, not FileView.\n// Action handlers are therefore duplicated here; both touch only\n// `this.model` + `window.MOJO.plugins`, so there is no cross-talk risk.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FilePreviewSection extends View {\n constructor(options = {}) {\n // Pass `model` through — View.setModel wires up the 'change'\n // listener automatically, no need to duplicate it here.\n super({\n className: 'file-preview-section p-3',\n ...options\n });\n this.categoryConfig = options.categoryConfig || CATEGORY_CONFIG.other;\n }\n\n /**\n * Override the base re-render-on-model-change behavior.\n * - video/audio elements must NOT re-render: the rendition poll fires\n * model `change` every 5s while transcoding, and re-rendering would\n * destroy and recreate the media element on every tick (visible\n * reload cycle, lost playback state).\n * - image/pdf/document/archive previews are idempotent — re-render\n * safely to pick up new rendition URLs as they arrive.\n */\n _onModelChange() {\n const type = this.categoryConfig?.previewType;\n if (type === 'video' || type === 'audio') return;\n if (this.isMounted()) this.render();\n }\n\n getTemplate() {\n const type = this.categoryConfig.previewType;\n const url = this.model.get('url') || '';\n const filename = this.model.get('filename') || '';\n\n if (type === 'image') {\n const previewUrl = (this.model.getThumbnailUrl && this.model.getThumbnailUrl()) || url;\n return `\n <div class=\"text-center\">\n <img src=\"${escapeAttr(previewUrl)}\"\n alt=\"${escapeAttr(filename)}\"\n class=\"img-fluid rounded shadow-sm\"\n style=\"max-height: 70vh; cursor: zoom-in;\"\n data-action=\"view-file\" role=\"button\">\n <div class=\"text-muted small mt-2\">Click image for full view</div>\n </div>\n `;\n }\n\n if (type === 'video') {\n const poster = this.model.getThumbnailUrl && this.model.getThumbnailUrl();\n return `\n <div class=\"text-center\">\n <video controls preload=\"metadata\"\n src=\"${escapeAttr(url)}\"\n ${poster ? `poster=\"${escapeAttr(poster)}\"` : ''}\n style=\"width: 100%; max-height: 70vh; background:#000;\"></video>\n </div>\n `;\n }\n\n if (type === 'audio') {\n return `\n <div class=\"p-4 bg-light rounded text-center\">\n <i class=\"bi ${this.categoryConfig.icon} display-4 text-secondary\"></i>\n <h5 class=\"mt-3 mb-3 text-break\">${escapeHtml(filename)}</h5>\n <audio controls class=\"w-100\" src=\"${escapeAttr(url)}\"></audio>\n </div>\n `;\n }\n\n if (type === 'pdf') {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi ${this.categoryConfig.icon} text-danger\" style=\"font-size: 5rem;\"></i>\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <div class=\"mt-4\">\n <button type=\"button\" class=\"btn btn-primary me-2\" data-action=\"view-file\">\n <i class=\"bi bi-eye me-1\"></i>Open PDF Viewer\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n if (type === 'document') {\n const preview = this.model.getBestImageRendition && this.model.getBestImageRendition();\n return `\n <div class=\"text-center p-4 bg-light rounded\">\n ${preview\n ? `<img src=\"${escapeAttr(preview.url)}\" alt=\"${escapeAttr(filename)} preview\" class=\"img-fluid rounded mb-3\" style=\"max-height: 50vh;\">`\n : `<i class=\"bi ${this.categoryConfig.icon} text-secondary\" style=\"font-size: 5rem;\"></i>`}\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary me-2\" data-action=\"view-file\">\n <i class=\"bi bi-box-arrow-up-right me-1\"></i>Open\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n // archive / other / download fallback\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi ${this.categoryConfig.icon} text-secondary\" style=\"font-size: 5rem;\"></i>\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <p class=\"text-muted small\">No inline preview available for this file type.</p>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n async onActionViewFile() {\n openFileInPreview(this.model, this.categoryConfig);\n }\n\n async onActionDownloadFile() {\n downloadFile(this.model);\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileRenditionsSection — Renditions gallery\n//\n// Renders each rendition as a card tile: inline preview (image thumbnail,\n// video poster with play overlay, or category icon), role badge, dimensions\n// + size, and three actions (Preview, Copy URL, Download).\n//\n// Three states:\n// - Gallery grid when renditions exist\n// - Processing placeholder when upload_status === 'completed' and the\n// backend worker hasn't populated renditions yet\n// - \"Upload in progress\" placeholder otherwise\n//\n// `model:` flows through to View.setModel which wires the built-in\n// 'change' → _onModelChange → render() listener (guarded by isMounted).\n// That's exactly what we want: swap placeholder for the grid, or add new\n// rendition cards, as the rendition poll brings them in.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileRenditionsSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-renditions-section p-3',\n ...options\n });\n }\n\n getTemplate() {\n if (this.model.hasRenditions && this.model.hasRenditions()) {\n return this._buildGalleryTemplate();\n }\n if (this.model.isUploadPending && this.model.isUploadPending()) {\n return this._buildWaitingTemplate();\n }\n // Upload finished, but the rendition map is empty — no work is in\n // progress (per the backend: completed === done). Show a clean empty\n // state with a Regenerate button instead of an indefinite spinner.\n return this._buildEmptyTemplate();\n }\n\n _buildGalleryTemplate() {\n const renditions = this.model.getRenditions();\n const cards = renditions.map(r => this._buildCard(r)).join('');\n const count = renditions.length;\n return `\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"text-muted small\">\n ${count} rendition${count === 1 ? '' : 's'}\n </div>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"refresh-renditions\" title=\"Refresh list\">\n <i class=\"bi bi-arrow-clockwise\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"regenerate-from-section\" title=\"Rebuild all previews\">\n <i class=\"bi bi-arrow-repeat me-1\"></i>Regenerate\n </button>\n </div>\n </div>\n <div class=\"row g-3\">${cards}</div>\n `;\n }\n\n _buildCard(r) {\n const url = r && r.url ? r.url : '';\n const ct = r && typeof r.content_type === 'string' ? r.content_type : '';\n const role = r && r.role ? r.role : 'rendition';\n const filename = r && r.filename ? r.filename : role;\n const size = r && r.file_size ? formatBytes(r.file_size) : '';\n const dimensions = (r && r.width && r.height) ? `${r.width} × ${r.height}` : '';\n\n const viewData = [\n `data-action=\"view-rendition\"`,\n `data-url=\"${escapeAttr(url)}\"`,\n `data-ct=\"${escapeAttr(ct)}\"`,\n `data-filename=\"${escapeAttr(filename)}\"`,\n `data-role=\"${escapeAttr(role)}\"`\n ].join(' ');\n\n let preview;\n if (ct.startsWith('image/') && url) {\n preview = `<img src=\"${escapeAttr(url)}\" alt=\"${escapeAttr(role)}\"\n loading=\"lazy\"\n class=\"w-100\"\n style=\"height: 140px; object-fit: cover; background: #f8f9fa; border-top-left-radius: var(--bs-card-inner-border-radius); border-top-right-radius: var(--bs-card-inner-border-radius);\">`;\n } else if (ct.startsWith('video/')) {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center position-relative\"\n style=\"height: 140px; background: linear-gradient(135deg, #212529 0%, #343a40 100%); color: #fff;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-play-circle-fill\" style=\"font-size: 2.75rem; opacity: 0.9;\"></i>\n <span class=\"position-absolute bottom-0 start-0 end-0 text-center small py-1\"\n style=\"background: rgba(0,0,0,0.35); font-variant-numeric: tabular-nums;\">\n ${escapeHtml(ct)}\n </span>\n </div>`;\n } else if (ct.startsWith('audio/')) {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center\"\n style=\"height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-music-note-beamed text-secondary\" style=\"font-size: 2.5rem;\"></i>\n </div>`;\n } else {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center\"\n style=\"height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-file-earmark text-secondary\" style=\"font-size: 2.5rem;\"></i>\n </div>`;\n }\n\n const footer = url ? `\n <div class=\"card-footer p-1 d-flex gap-1 bg-white border-top-0\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary flex-fill\"\n ${viewData}\n title=\"Preview\">\n <i class=\"bi bi-eye\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary flex-fill\"\n data-action=\"copy-rendition-url\" data-url=\"${escapeAttr(url)}\"\n title=\"Copy URL\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n <a href=\"${escapeAttr(url)}\" download=\"${escapeAttr(filename)}\"\n class=\"btn btn-sm btn-outline-secondary flex-fill\"\n title=\"Download\"\n data-stop-propagation>\n <i class=\"bi bi-download\"></i>\n </a>\n </div>\n ` : '';\n\n return `\n <div class=\"col-sm-6 col-md-4 col-lg-3\">\n <div class=\"card h-100 shadow-sm rendition-card\">\n <div ${url ? viewData : ''} ${url ? 'role=\"button\" style=\"cursor: pointer;\"' : ''}>\n ${preview}\n </div>\n <div class=\"card-body p-2\">\n <div class=\"d-flex justify-content-between align-items-start gap-2 mb-1\">\n <span class=\"badge bg-secondary text-truncate\" style=\"max-width: 100%;\" title=\"${escapeAttr(role)}\">${escapeHtml(role)}</span>\n ${size ? `<small class=\"text-muted flex-shrink-0\" style=\"font-variant-numeric: tabular-nums;\">${escapeHtml(size)}</small>` : ''}\n </div>\n ${dimensions ? `<div class=\"small text-muted\" style=\"font-variant-numeric: tabular-nums;\">${escapeHtml(dimensions)}</div>` : ''}\n </div>\n ${footer}\n </div>\n </div>\n `;\n }\n\n _buildEmptyTemplate() {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi bi-layers display-6 text-muted\"></i>\n <h6 class=\"mt-3 mb-1\">No renditions for this file</h6>\n <p class=\"text-muted small mb-3\">\n Click <strong>Regenerate</strong> to (re)build thumbnails and previews on the backend.\n </p>\n <div class=\"d-inline-flex gap-2\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" data-action=\"refresh-renditions\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Refresh\n </button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" data-action=\"regenerate-from-section\">\n <i class=\"bi bi-arrow-repeat me-1\"></i>Regenerate\n </button>\n </div>\n </div>\n `;\n }\n\n _buildWaitingTemplate() {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi bi-hourglass-split display-6 text-muted\"></i>\n <h6 class=\"mt-3 mb-1\">Upload still in progress</h6>\n <p class=\"text-muted small mb-0\">Renditions will be generated once the upload completes.</p>\n </div>\n `;\n }\n\n // ── Actions ─────────────────────────────────────\n\n async onActionRefreshRenditions() {\n try {\n await this.model.fetch();\n } catch (err) {\n console.warn('FileView: refresh-renditions fetch failed:', err);\n }\n }\n\n async onActionViewRendition(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const url = element.dataset.url;\n const ct = element.dataset.ct || '';\n if (!url) return;\n\n if (ct.startsWith('image/')) {\n const Lightbox = typeof window !== 'undefined' ? window.MOJO?.plugins?.LightboxGallery : null;\n if (Lightbox && typeof Lightbox.show === 'function') {\n const images = this.model.getRenditions()\n .filter(r => r && r.url && typeof r.content_type === 'string' && r.content_type.startsWith('image/'))\n .map(r => ({ src: r.url, alt: r.role || '' }));\n const startIndex = Math.max(0, images.findIndex(img => img.src === url));\n Lightbox.show(images, { startIndex, fitToScreen: false });\n return;\n }\n }\n // Non-image renditions (video, audio, anything else) open in a new tab\n // — browsers have native players for these and the lightbox plugin\n // only handles images.\n window.open(url, '_blank', 'noopener');\n }\n\n async onActionCopyRenditionUrl(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const url = element.dataset.url;\n if (!url) return;\n try {\n if (navigator.clipboard && window.isSecureContext) {\n await navigator.clipboard.writeText(url);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = url;\n document.body.appendChild(textarea);\n textarea.select();\n document.execCommand('copy');\n document.body.removeChild(textarea);\n }\n // Short-lived visual confirmation on the clicked button\n const icon = element.querySelector('i');\n if (icon) {\n const orig = icon.className;\n icon.className = 'bi bi-check-lg text-success';\n setTimeout(() => { icon.className = orig; }, 1200);\n }\n this.getApp()?.toast?.success?.('URL copied to clipboard');\n } catch (err) {\n console.error('Failed to copy rendition URL:', err);\n this.getApp()?.toast?.error?.('Failed to copy URL');\n }\n }\n\n // Delegates up the parent chain to FileView.onActionRegenerateRenditions\n // so the section button behaves identically to the ContextMenu item.\n async onActionRegenerateFromSection() {\n let node = this.parent;\n while (node) {\n if (typeof node.onActionRegenerateRenditions === 'function') {\n return node.onActionRegenerateRenditions();\n }\n node = node.parent;\n }\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Shared action helpers (also used by FileView's ContextMenu-triggered actions)\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction openFileInPreview(model, categoryConfig) {\n const url = model.get('url');\n if (!url) return;\n const type = categoryConfig.previewType;\n\n if (type === 'image') {\n const Lightbox = typeof window !== 'undefined' ? window.MOJO?.plugins?.LightboxGallery : null;\n const renditions = model.get('renditions') || {};\n const images = [\n { src: url, alt: 'Original' },\n ...Object.values(renditions)\n .filter(r => r && r.url && typeof r.content_type === 'string' && r.content_type.startsWith('image/'))\n .map(r => ({ src: r.url, alt: r.role || '' }))\n ];\n if (Lightbox && typeof Lightbox.show === 'function') {\n Lightbox.show(images, { fitToScreen: false });\n } else {\n window.open(url, '_blank');\n }\n return;\n }\n\n if (type === 'pdf') {\n const PDFViewer = typeof window !== 'undefined' ? window.MOJO?.plugins?.PDFViewer : null;\n if (PDFViewer && typeof PDFViewer.showDialog === 'function') {\n PDFViewer.showDialog(url, { title: model.get('filename') });\n } else {\n window.open(url, '_blank');\n }\n return;\n }\n\n window.open(url, '_blank');\n}\n\nfunction downloadFile(model) {\n const url = model.get('url');\n if (!url) return;\n const a = document.createElement('a');\n a.href = url;\n a.download = model.get('filename') || '';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileSharesSection — Active shortlink shares for this file\n//\n// Lists ShortLink rows where source=fileman-share and file=<this.id>. Each\n// share is a distinct shortlink minted by `model.share({...})` (see\n// Files.js). Revoking a share flips `is_active=false`; the audit row is\n// preserved per the backend contract — never DELETE the row, that just\n// causes a fresh shortlink to be minted on next read.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileSharesSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-shares-section p-3',\n ...options\n });\n this.template = `\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"text-muted small\">\n Active and historical shareable links for this file.\n </div>\n <button type=\"button\" class=\"btn btn-sm btn-primary\"\n data-action=\"share-file-from-section\">\n <i class=\"bi bi-link-45deg me-1\"></i>Share new\n </button>\n </div>\n <div data-container=\"file-shares-table\"></div>\n `;\n }\n\n async onInit() {\n const fileId = this.model.get('id');\n if (!fileId) return;\n\n const collection = new ShortLinkList({\n params: {\n source: 'fileman-share',\n file: fileId,\n sort: '-created',\n size: 25,\n },\n });\n this._sharesCollection = collection;\n\n this.sharesTable = new TableView({\n containerId: 'file-shares-table',\n collection,\n hideActivePillNames: ['source', 'file'],\n columns: [\n {\n key: 'code',\n label: 'Short URL',\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-share-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 { key: 'user.display_name', label: 'Shared By', formatter: \"default('—')\" },\n { key: 'hit_count', label: 'Hits', width: '80px', sortable: true },\n { key: 'track_clicks', label: 'Tracked', width: '90px', formatter: 'yesnoicon' },\n { key: 'is_active', label: 'Active', width: '80px', formatter: 'yesnoicon' },\n { key: 'expires_at', label: 'Expires', width: '160px', formatter: \"datetime|default('Never')\", sortable: true },\n { key: 'created', label: 'Created', width: '160px', formatter: 'datetime', sortable: true },\n { key: 'metadata.note', label: 'Note', formatter: \"truncate(40)|default('—')\", visibility: 'lg' },\n ],\n paginated: true,\n sortable: true,\n searchable: false,\n filterable: false,\n contextMenu: [\n { label: 'Copy Short URL', action: 'copy-share-code', icon: 'bi-clipboard' },\n { divider: true },\n { label: 'Revoke', action: 'revoke-share', icon: 'bi-slash-circle', danger: true },\n ],\n tableOptions: {\n hover: true,\n size: 'sm',\n emptyMessage: 'No shares yet — click “Share new” to mint a tracked link.',\n emptyIcon: 'bi-link-45deg',\n actions: [],\n },\n });\n this.addChild(this.sharesTable);\n }\n\n /** Re-fetch the shares list (called after a new share is minted). */\n refreshShares() {\n return this._sharesCollection?.fetch();\n }\n\n // Section bubbles section-scoped actions up to FileView. Mirrors the\n // FileRenditionsSection.onActionRegenerateFromSection pattern.\n async onActionShareFileFromSection() {\n let node = this.parent;\n while (node) {\n if (typeof node.onActionShareFile === 'function') {\n return node.onActionShareFile();\n }\n node = node.parent;\n }\n return null;\n }\n\n async onActionCopyShareCode(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const code = element?.dataset?.code;\n if (!code) return;\n const url = buildShortUrl(code, 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 async onActionRevokeShare(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n // The contextMenu wires `data-id` on the row; resolve from the table.\n const row = element?.closest?.('[data-row-id]');\n const id = row?.dataset?.rowId || element?.dataset?.id;\n if (!id) return;\n const target = this._sharesCollection?.get?.(id);\n if (!target) return;\n\n const confirmed = await Modal.confirm(\n 'Revoke this share? Anyone with the short URL will get a 404 — the audit row is preserved.',\n 'Revoke Share',\n { confirmText: 'Revoke', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n try {\n await target.save({ is_active: false });\n this.getApp()?.toast?.success?.('Share revoked');\n await this.refreshShares();\n } catch (err) {\n console.error('Failed to revoke share:', err);\n this.getApp()?.toast?.error?.('Failed to revoke share');\n }\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileOverviewSection — canonical DetailView landing surface\n//\n// Mirrors the round-1 KPIs + flat-row identity card + recent-activity shape\n// every other DetailView consumer leads with (UserView, GroupView,\n// IncidentView, ShortLinkView). Preview / Renditions / Details remain as\n// dedicated sections; the user lands here first.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileOverviewSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-overview-section',\n // The Public-URL row uses the `clipboard` formatter which emits a\n // [data-bs-toggle=\"tooltip\"] copy button.\n enableTooltips: true,\n template: `\n <div class=\"detail-section-eyebrow\">Snapshot</div>\n <div class=\"detail-kpi-grid\">\n <div data-container=\"file-kpi-size\"></div>\n <div data-container=\"file-kpi-type\"></div>\n <div data-container=\"file-kpi-status\"></div>\n <div data-container=\"file-kpi-uploaded\"></div>\n </div>\n\n <div class=\"detail-section-eyebrow\">Identity</div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Filename</div>\n <div class=\"detail-flat-row-value text-break\">{{model.filename|default:'Unnamed file'}}</div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Content type</div>\n <div class=\"detail-flat-row-value\"><code>{{model.content_type|default:'unknown'}}</code></div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Uploaded by</div>\n <div class=\"detail-flat-row-value\">{{model.user.display_name|default:'—'}}</div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Storage</div>\n <div class=\"detail-flat-row-value\">{{model.file_manager.name|default:'—'}}</div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Visibility</div>\n <div class=\"detail-flat-row-value\">\n {{#model.is_public|bool}}<span class=\"badge bg-success\"><i class=\"bi bi-unlock me-1\"></i>Public</span>{{/model.is_public|bool}}\n {{^model.is_public|bool}}<span class=\"badge bg-secondary\"><i class=\"bi bi-lock me-1\"></i>Private</span>{{/model.is_public|bool}}\n </div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Public URL</div>\n <div class=\"detail-flat-row-value detail-flat-row-value--url\">\n {{#hasUrl|bool}}{{{model.url|clipboard}}}{{/hasUrl|bool}}\n {{^hasUrl|bool}}<span class=\"text-secondary\">—</span>{{/hasUrl|bool}}\n </div>\n </div>\n `,\n ...options\n });\n }\n\n get hasUrl() { return !!this.model?.get?.('url'); }\n\n async onInit() {\n const m = this.model;\n this.kpiSize = new MetricCard({\n containerId: 'file-kpi-size',\n label: 'File size',\n value: dataFormatter.apply('filesize', m.get('file_size')) || '—'\n });\n this.kpiType = new MetricCard({\n containerId: 'file-kpi-type',\n label: 'Type',\n value: _capitalize(m.get('category') || 'other')\n });\n this.kpiStatus = new MetricCard({\n containerId: 'file-kpi-status',\n label: 'Status',\n value: _capitalize(m.get('upload_status') || '—')\n });\n const created = m.get('created');\n this.kpiUploaded = new MetricCard({\n containerId: 'file-kpi-uploaded',\n label: 'Uploaded',\n value: created ? (dataFormatter.apply('relative', created) || '—') : '—'\n });\n [this.kpiSize, this.kpiType, this.kpiStatus, this.kpiUploaded]\n .forEach(c => this.addChild(c));\n }\n}\n\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileView (main component)\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileView extends DetailView {\n constructor(options = {}) {\n const model = options.model || new File(options.data || {});\n const categoryConfig = getCategoryConfig(model);\n\n // ── Section views (built before super() so SideNavView mounts them) ──\n\n const overviewSection = new FileOverviewSection({ model });\n\n const previewSection = new FilePreviewSection({ model, categoryConfig });\n\n const detailsSection = new DataView({\n model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'id', label: 'ID' },\n { name: 'filename', label: 'Filename' },\n { name: 'storage_filename', label: 'Storage Filename' },\n { name: 'content_type', label: 'Content Type' },\n { name: 'file_size', label: 'File Size', format: 'filesize' },\n { name: 'category', label: 'Category' },\n { name: 'upload_status', label: 'Status', format: 'badge' },\n { name: 'created', label: 'Created', format: 'datetime' },\n { name: 'modified', label: 'Modified', format: 'datetime' },\n { name: 'user.display_name', label: 'Uploaded By' },\n { name: 'file_manager.name', label: 'Storage Backend' },\n { name: 'storage_file_path', label: 'Storage Path' },\n { name: 'url', label: 'Public URL', format: 'url' },\n { name: 'is_public', label: 'Is Public', format: 'boolean' }\n ]\n });\n\n const renditionsSection = new FileRenditionsSection({ model });\n\n const sections = [\n { key: 'overview', label: 'Overview', icon: 'bi-grid-1x2', view: overviewSection },\n { key: 'preview', label: 'Preview', icon: categoryConfig.icon, view: previewSection },\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsSection },\n { key: 'renditions', label: 'Renditions', icon: 'bi-layers', view: renditionsSection }\n ];\n\n // Shares — list of shortlinks (source=fileman-share) for this file.\n // Hidden for unsaved models — the section needs an `id` to scope.\n let sharesSection = null;\n if (model.get('id')) {\n sharesSection = new FileSharesSection({ model });\n sections.push({ key: 'shares', label: 'Shares', icon: 'bi-link-45deg', view: sharesSection });\n }\n\n // Metadata — only when backend returned a non-empty metadata object\n const metadata = model.get('metadata');\n if (metadata && typeof metadata === 'object' && Object.keys(metadata).length) {\n const metadataView = new DataView({\n data: metadata,\n className: 'p-3',\n columns: 2,\n showEmptyValues: false\n });\n sections.push({ key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: metadataView });\n }\n\n // ── Header config ───────────────────────────────────\n\n // Thumbnail — render real preview if the model exposes one; otherwise\n // fall back to the category Bootstrap icon (DetailView default).\n const thumbnailUrl = (typeof model.getThumbnailUrl === 'function') ? model.getThumbnailUrl() : null;\n const iconHtml = thumbnailUrl\n ? `<img src=\"${escapeAttr(thumbnailUrl)}\" alt=\"\">`\n : null;\n const iconTone = CATEGORY_TONE[categoryConfig.badgeClass] || null;\n\n const chips = [\n { icon: 'bi-hdd', text: m => dataFormatter.apply('filesize', m.get('file_size')), variant: 'light',\n when: m => !!m.get('file_size') },\n { text: m => m.get('content_type') || null, variant: 'light',\n when: m => !!m.get('content_type') },\n { text: m => _capitalize(m.get('category') || 'other'),\n variant: (categoryConfig.badgeClass || 'bg-secondary').replace(/^bg-/, '') },\n { text: m => _capitalize(m.get('upload_status') || ''), variant: 'secondary',\n when: m => !!m.get('upload_status') },\n { icon: 'bi-unlock', text: 'Public', variant: 'success',\n when: m => !!m.get('is_public') },\n { icon: 'bi-lock', text: 'Private', variant: 'secondary',\n when: m => !m.get('is_public') }\n ];\n\n const contextItems = [\n { label: 'View', action: 'view-file', icon: 'bi-eye' },\n { label: 'Download', action: 'download-file', icon: 'bi-download' },\n { label: 'Copy URL', action: 'copy-url', icon: 'bi-clipboard' },\n { label: 'Share Link…', action: 'share-file', icon: 'bi-link-45deg' },\n { type: 'divider' },\n { label: 'Edit Details', action: 'edit-file', icon: 'bi-pencil' },\n model.get('is_public')\n ? { label: 'Make Private', action: 'make-private', icon: 'bi-lock' }\n : { label: 'Make Public', action: 'make-public', icon: 'bi-unlock' },\n { label: 'Regenerate Previews', action: 'regenerate-renditions', icon: 'bi-arrow-repeat' },\n { type: 'divider' },\n { label: 'Delete File', action: 'delete-file', icon: 'bi-trash', danger: true }\n ];\n\n super({\n className: 'file-view',\n ...options,\n model,\n header: {\n icon: categoryConfig.icon,\n iconTone,\n iconHtml,\n titleFn: m => m.get('filename') || 'Unnamed file',\n subtitleFn: m => {\n const created = m.get('created');\n if (!created) return '';\n const fmt = dataFormatter.apply('datetime', dataFormatter.apply('epoch', created));\n return fmt ? `Uploaded ${fmt}` : '';\n },\n chips,\n contextMenu: { items: contextItems }\n },\n sections,\n activeSection: 'overview',\n navWidth: 200,\n minWidth: 500,\n contentPadding: '1.25rem 1.5rem'\n });\n\n // Stash references for action handlers + cross-section wiring\n this.overviewSection = overviewSection;\n this.previewSection = previewSection;\n this.detailsSection = detailsSection;\n this.renditionsSection = renditionsSection;\n this.sharesSection = sharesSection;\n\n // No auto-poll: a `completed` upload means renditions are done. If the\n // user explicitly clicks \"Regenerate\", _maybeStartRenditionsPoll is\n // invoked with { force: true } from that handler so the new renditions\n // appear without a manual refresh.\n }\n\n _getCategoryConfig() {\n return getCategoryConfig(this.model);\n }\n\n async onBeforeDestroy() {\n this._stopRenditionsPoll();\n }\n\n // ── Action handlers ─────────────────────────────\n\n async onActionViewFile() {\n openFileInPreview(this.model, this._getCategoryConfig());\n }\n\n async onActionDownloadFile() {\n downloadFile(this.model);\n }\n\n async onActionCopyUrl() {\n const url = this.model.get('url');\n if (!url) return;\n try {\n if (navigator.clipboard && window.isSecureContext) {\n await navigator.clipboard.writeText(url);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = url;\n document.body.appendChild(textarea);\n textarea.select();\n document.execCommand('copy');\n document.body.removeChild(textarea);\n }\n this.getApp()?.toast?.success?.('URL copied to clipboard');\n } catch (error) {\n console.error('Failed to copy URL:', error);\n this.getApp()?.toast?.error?.('Failed to copy URL');\n }\n }\n\n async onActionEditFile() {\n const resp = await Modal.modelForm({\n title: `Edit File - ${this.model.get('filename')}`,\n model: this.model,\n formConfig: FileForms.edit\n });\n if (resp) {\n this.render();\n }\n }\n\n async onActionMakePublic() {\n await this.model.save({ is_public: true });\n this.render();\n }\n\n async onActionMakePrivate() {\n await this.model.save({ is_public: false });\n this.render();\n }\n\n async onActionShareFile() {\n if (!this.model.get('id')) {\n this.getApp()?.toast?.warning?.('Save the file before sharing it.');\n return;\n }\n\n // Step 1 — collect share options.\n const formResult = await Modal.form({\n title: 'Share Link',\n size: 'sm',\n help: 'Each share creates a distinct, audit-tracked short URL attributed to you.',\n fields: [\n { name: 'expire_days', type: 'number', label: 'Expire after (days)', value: 30, min: 0, cols: 12, help: '0 = never expires. Server max: 3650.' },\n { name: 'track_clicks', type: 'switch', label: 'Track clicks', value: true, cols: 12, help: 'Records per-click history (IP, user-agent, bot/human).' },\n { name: 'note', type: 'textarea', label: 'Note (optional)', rows: 2, cols: 12, maxlength: 512, help: 'Private audit note — not shown to recipients.' },\n ],\n submitText: 'Share',\n });\n if (!formResult) return;\n\n // Build options dict — only include keys with meaningful values.\n const opts = {};\n if (formResult.expire_days !== undefined && formResult.expire_days !== null && formResult.expire_days !== '') {\n opts.expire_days = Number(formResult.expire_days);\n }\n if (formResult.track_clicks !== undefined) {\n opts.track_clicks = !!formResult.track_clicks;\n }\n if (formResult.note) {\n opts.note = String(formResult.note).slice(0, 512);\n }\n\n // Step 2 — mint the share.\n let resp;\n try {\n resp = await this.model.share(Object.keys(opts).length ? opts : true);\n } catch (err) {\n console.error('Share failed:', err);\n Modal.showError(err?.data?.error || err?.message || 'Failed to create share link');\n return;\n }\n\n const data = resp?.data;\n const shortUrl = data?.url;\n if (!resp?.success || !shortUrl) {\n Modal.showError(data?.error || 'Failed to create share link');\n return;\n }\n\n // Best-effort copy to clipboard so the user can paste immediately.\n let copied = false;\n try {\n await navigator.clipboard?.writeText?.(shortUrl);\n copied = true;\n } catch (_e) {\n copied = false;\n }\n\n // Step 3 — show the result with the URL + summary.\n const expiry = data.expires_at\n ? new Date(data.expires_at).toLocaleString()\n : 'Never';\n const tracked = data.track_clicks ? 'Yes' : 'No';\n const copyHint = copied\n ? '<div class=\"form-text text-success mb-2\"><i class=\"bi bi-check-circle me-1\"></i>Copied to clipboard.</div>'\n : '<div class=\"form-text text-muted mb-2\">Select the URL above to copy.</div>';\n const summary = `\n <div class=\"mb-2\">\n <label class=\"form-label small text-muted mb-1\">Share URL</label>\n <input type=\"text\" class=\"form-control font-monospace\" readonly value=\"${escapeAttr(shortUrl)}\">\n ${copyHint}\n </div>\n <dl class=\"row small mb-0\">\n <dt class=\"col-4 text-muted\">Expires</dt><dd class=\"col-8\">${escapeHtml(expiry)}</dd>\n <dt class=\"col-4 text-muted\">Tracked</dt><dd class=\"col-8\">${escapeHtml(tracked)}</dd>\n ${data.shortlink_code ? `<dt class=\"col-4 text-muted\">Code</dt><dd class=\"col-8\"><code>${escapeHtml(data.shortlink_code)}</code></dd>` : ''}\n </dl>\n `;\n await Modal.alert(summary, 'Share link created', { type: 'success' });\n\n // Step 4 — refresh the Shares section if it's mounted.\n try {\n await this.sharesSection?.refreshShares?.();\n } catch (err) {\n console.warn('Failed to refresh shares section:', err);\n }\n }\n\n async onActionRegenerateRenditions() {\n const confirmed = await Modal.confirm(\n 'Rebuild all previews and thumbnails for this file? Existing renditions will be replaced. Generation runs in the background and may take several minutes for video.',\n 'Regenerate Previews',\n { confirmText: 'Regenerate' }\n );\n if (!confirmed) return;\n\n try {\n await this.model.regenerateRenditions();\n this.getApp()?.toast?.success?.('Regenerating previews in the background…');\n } catch (err) {\n console.error('Failed to trigger regenerate_renditions:', err);\n this.getApp()?.toast?.error?.('Failed to start preview regeneration');\n return;\n }\n // Start polling so the new renditions appear automatically as the\n // worker finishes.\n this._maybeStartRenditionsPoll({ force: true });\n }\n\n // ── Renditions polling ──────────────────────────\n // Mirrors the IncidentView analysis-progress polling shape: recursive\n // setTimeout, attempt counter, no overlap with a previous fetch.\n\n _maybeStartRenditionsPoll(options = {}) {\n if (this._renditionsPollTimer) return; // already polling\n // Only the explicit `force: true` path remains — this is invoked from\n // onActionRegenerateRenditions to catch newly built renditions as the\n // worker finishes.\n if (!options.force) return;\n const maxAttempts = 60; // 5 minutes at 5s\n const intervalMs = 5000;\n let attempts = 0;\n\n const tick = () => {\n this._renditionsPollTimer = null;\n if (!this.model) return;\n if (this.model.hasRenditions && this.model.hasRenditions()) return;\n if (++attempts > maxAttempts) return;\n\n this._renditionsPollTimer = setTimeout(async () => {\n try {\n await this.model.fetch();\n } catch (err) {\n console.warn('FileView: renditions poll fetch failed:', err);\n }\n tick();\n }, intervalMs);\n };\n\n tick();\n }\n\n _stopRenditionsPoll() {\n if (this._renditionsPollTimer) {\n clearTimeout(this._renditionsPollTimer);\n this._renditionsPollTimer = null;\n }\n }\n\n async onActionDeleteFile() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete the file \"${this.model.get('filename')}\"? This action cannot be undone.`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n if (!confirmed) return;\n\n const resp = await this.model.destroy();\n if (resp && resp.success) {\n this.emit('file:deleted', { model: this.model });\n }\n }\n\n // Prevent model changes from triggering a full re-render.\n // Section views manage their own reactivity. Same pattern as UserView.\n _onModelChange() {\n // no-op\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 static create(options = {}) {\n return new FileView(options);\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Small HTML helpers — kept private to this file\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction escapeHtml(str) {\n if (str == null) return '';\n return String(str)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Build a full short URL from a shortlink code. Prefers\n * `app.config.shortlink_base_url`, falls back to `window.location.origin`.\n * Mirrors the helper in src/extensions/admin/shortlinks/ShortLinkView.js\n * but lives here so this core view doesn't depend on the admin extension.\n */\nfunction buildShortUrl(code, app) {\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 escapeAttr(str) {\n return escapeHtml(str);\n}\n\nfunction formatBytes(bytes) {\n if (bytes == null || isNaN(bytes)) return '';\n const n = Number(bytes);\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;\n return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;\n}\n\nFile.VIEW_CLASS = FileView;\nFile.MODEL_REF = 'fileman.File';\n\nexport default FileView;\nexport {\n FileView,\n FileOverviewSection,\n FilePreviewSection,\n FileRenditionsSection,\n FileSharesSection\n};\n","import View from '@core/View.js';\nimport FilePreviewView from '@core/views/data/FilePreviewView.js';\n\n/**\n * ChatMessageView - Individual message display with theme support\n * \n * Supports two themes:\n * - 'compact': List-based admin/activity feed style\n * - 'bubbles': Chat bubble style with left/right positioning\n */\nclass ChatMessageView extends View {\n constructor(options = {}) {\n // Compute full className before super() so _syncAttrs picks it up\n const message = options.message || {};\n const theme = options.theme || 'compact';\n const isCurrentUser = options.isCurrentUser || false;\n const role = message.role || (isCurrentUser ? 'user' : null);\n\n let cls = 'chat-message';\n if (theme === 'bubbles') {\n cls += isCurrentUser ? ' message-right' : ' message-left';\n }\n if (role === 'assistant') cls += ' message-assistant';\n else if (role === 'user') cls += ' message-user';\n\n super({\n className: cls,\n ...options\n });\n\n this.message = message;\n this.theme = theme;\n this.isCurrentUser = isCurrentUser;\n this.role = role;\n }\n\n getTemplate() {\n // System event messages (same for both themes). Content may contain\n // pre-rendered HTML (e.g. status-change badges), so use triple-brace.\n if (this.message.type === 'system_event') {\n return `\n <div class=\"chat-message-system text-center text-muted small py-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n {{{message.content}}}\n </div>\n `;\n }\n\n // Theme-specific templates\n if (this.theme === 'bubbles') {\n return this.getBubblesTemplate();\n } else {\n return this.getCompactTemplate();\n }\n }\n\n /**\n * Get compact theme template (Option 4 - Admin/Activity Feed Style)\n */\n getCompactTemplate() {\n const userClass = this.isCurrentUser ? 'bg-primary' : 'bg-secondary';\n const isAssistant = this.role === 'assistant';\n\n return `\n <div class=\"message-item\">\n <div class=\"message-avatar ${isAssistant ? 'bg-dark' : userClass}\">\n ${isAssistant ? '<img src=\"https://mojo-verify.s3.amazonaws.com/signatures/14e7aab75c2749cb846f7d57298691ac/mojo_ai_7c0322e9.png\" class=\"mojo-avatar-icon\" alt=\"\">' : `\n {{#message.author.avatarUrl}}\n <img src=\"{{message.author.avatarUrl}}\" alt=\"{{message.author.name}}\" class=\"w-100 h-100 rounded-circle\">\n {{/message.author.avatarUrl}}\n {{^message.author.avatarUrl}}\n {{message.author.name|initials}}\n {{/message.author.avatarUrl}}\n `}\n </div>\n <div class=\"message-content\">\n <div class=\"message-header\">\n <div class=\"message-author\">\n ${isAssistant ? 'Mojo' : '{{message.author.name}}'}\n {{#isCurrentUser}}\n <span class=\"badge bg-primary badge-sm ms-1\">You</span>\n {{/isCurrentUser}}\n </div>\n <div class=\"message-time text-muted\">{{message.timestamp|relative}}</div>\n </div>\n <div class=\"message-text\">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container=\"blocks-${this.message.id || this.id}\"></div>\n <div data-container=\"attachments\"></div>\n </div>\n </div>\n `;\n }\n\n /**\n * Get bubbles theme template (Option 1 - Modern Chat Bubbles)\n */\n getBubblesTemplate() {\n const isAssistant = this.role === 'assistant';\n\n return `\n <div class=\"message-bubble-wrapper\">\n <div class=\"message-meta\">\n <strong>${isAssistant ? '<img src=\"https://mojo-verify.s3.amazonaws.com/signatures/14e7aab75c2749cb846f7d57298691ac/mojo_ai_7c0322e9.png\" class=\"mojo-avatar-icon me-1\" alt=\"\">Mojo' : '{{message.author.name}}'}</strong>\n <span class=\"text-muted\">· {{message.timestamp|relative}}</span>\n </div>\n <div class=\"message-bubble\">\n <div class=\"message-text\">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container=\"blocks-${this.message.id || this.id}\"></div>\n <div data-container=\"attachments\"></div>\n </div>\n </div>\n `;\n }\n\n /**\n * Get tool calls display template (collapsible section showing tool usage)\n * @private\n */\n _getToolCallsTemplate() {\n if (!this.message.tool_calls || this.message.tool_calls.length === 0) {\n return '';\n }\n const esc = (str) => {\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n };\n const toolBadges = this.message.tool_calls.map(tc => {\n const name = esc(tc.name || tc.function?.name || 'tool');\n const statusClass = tc.status === 'error' ? 'bg-danger' : 'bg-info';\n return `<span class=\"badge ${statusClass} me-1\">${name}</span>`;\n }).join('');\n\n const collapseId = `tools-${this.message.id || this.id}`;\n return `\n <div class=\"message-tool-calls mt-1\">\n <a class=\"text-muted small\" data-bs-toggle=\"collapse\" href=\"#${collapseId}\" role=\"button\" aria-expanded=\"false\">\n <i class=\"bi bi-tools me-1\"></i>${this.message.tool_calls.length} tool call${this.message.tool_calls.length > 1 ? 's' : ''}\n </a>\n <div class=\"collapse\" id=\"${collapseId}\">\n <div class=\"mt-1\">${toolBadges}</div>\n </div>\n </div>\n `;\n }\n\n async onAfterRender() {\n // Render attachments if any. Clean up any prior FilePreview children first\n // so a re-render doesn't duplicate them.\n if (this.message.attachments && this.message.attachments.length > 0) {\n const attachmentsContainer = this.element.querySelector('[data-container=\"attachments\"]');\n if (attachmentsContainer) {\n attachmentsContainer.innerHTML = '';\n for (const id in this.children) {\n const child = this.children[id];\n if (child instanceof FilePreviewView) this.removeChild(child);\n }\n this.message.attachments.forEach(file => {\n const filePreview = new FilePreviewView({ file });\n this.addChild(filePreview);\n filePreview.render(true, attachmentsContainer);\n });\n }\n }\n }\n}\n\nexport default ChatMessageView;\n","import View from '@core/View.js';\nimport applyFileDropMixin from '@core/mixins/FileDropMixin.js';\nimport { File } from '@core/models/Files.js';\n\n/**\n * ChatInputView - Input area with file drop support and attachment preview\n */\nclass ChatInputView extends View {\n constructor(options = {}) {\n super({\n className: 'chat-input-view',\n ...options\n });\n\n this.placeholder = options.placeholder || 'Type a message...';\n this.buttonText = options.buttonText || 'Send';\n this.showFileInput = options.showFileInput !== false; // default true\n this.attachments = []; // Array of uploaded file data\n this.pendingUploads = new Map(); // Track in-progress uploads\n }\n\n getTemplate() {\n return `\n <div class=\"chat-input-container\">\n <div class=\"chat-input-attachments\" data-container=\"attachments\"></div>\n <div class=\"chat-input-wrapper\">\n <textarea\n class=\"chat-input form-control\"\n placeholder=\"${this.placeholder}\"\n rows=\"2\"></textarea>\n <button class=\"chat-send-btn btn btn-primary\" data-action=\"send-message\" type=\"button\">\n <i class=\"bi bi-send-fill\"></i>\n <span class=\"spinner-border spinner-border-sm d-none\" role=\"status\" aria-hidden=\"true\"></span>\n </button>\n </div>\n ${this.showFileInput ? `\n <div class=\"chat-input-footer\">\n <small class=\"text-muted\">\n <i class=\"bi bi-paperclip\"></i>\n Drag & drop files to attach\n </small>\n </div>\n ` : ''}\n </div>\n `;\n }\n\n async onAfterRender() {\n // Enable file drop on the entire input container (if file input is shown)\n if (this.showFileInput) {\n this.enableFileDrop({\n dropZoneSelector: '.chat-input-container',\n multiple: true,\n acceptedTypes: ['*/*'], // Accept all file types\n visualFeedback: true,\n dragOverClass: 'drag-over',\n dragActiveClass: 'drag-active'\n });\n }\n\n // Auto-resize textarea as user types and handle Enter key\n const textarea = this.element.querySelector('.chat-input');\n if (textarea) {\n textarea.addEventListener('input', () => this.autoResizeTextarea(textarea));\n textarea.addEventListener('keydown', (e) => this.handleKeydown(e));\n }\n }\n\n /**\n * Handle textarea keydown (send on Enter without Shift)\n */\n handleKeydown(event) {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n this.onActionSendMessage(event, event.target);\n }\n }\n\n /**\n * Handle file drop\n * @param {File[]} files - Dropped files\n */\n async onFileDrop(files) {\n for (const file of files) {\n await this.uploadFile(file);\n }\n }\n\n /**\n * Upload a file\n * @param {File} file - File to upload\n */\n async uploadFile(file) {\n const fileModel = new File();\n const uploadId = Date.now() + Math.random();\n\n // Add preview immediately\n this.addFilePreview(uploadId, file, 0);\n this.pendingUploads.set(uploadId, { file, fileModel });\n\n try {\n const result = await fileModel.upload({\n file: file,\n onProgress: (progress) => {\n this.updateFileProgress(uploadId, progress);\n },\n onComplete: (uploadResult) => {\n this.handleUploadComplete(uploadId, fileModel);\n }\n });\n\n } catch (error) {\n console.error('File upload failed:', error);\n this.handleUploadError(uploadId, error);\n }\n }\n\n /**\n * Add file preview to UI\n * @param {string} uploadId - Unique upload ID\n * @param {File} file - File object\n * @param {number} progress - Upload progress (0-100)\n */\n addFilePreview(uploadId, file, progress) {\n const container = this.element.querySelector('[data-container=\"attachments\"]');\n if (!container) return;\n\n const preview = document.createElement('div');\n preview.className = 'attachment-preview';\n preview.dataset.uploadId = uploadId;\n preview.innerHTML = `\n <div class=\"attachment-info\">\n <i class=\"bi bi-file-earmark\"></i>\n <span class=\"attachment-name\">${this.escapeHtml(file.name)}</span>\n <span class=\"attachment-size\">(${this.formatFileSize(file.size)})</span>\n </div>\n <div class=\"attachment-progress\">\n <div class=\"progress\" style=\"height: 4px;\">\n <div class=\"progress-bar\" role=\"progressbar\" style=\"width: ${progress}%\"></div>\n </div>\n </div>\n <button class=\"attachment-remove btn btn-sm btn-link text-danger\" data-action=\"remove-attachment\" data-upload-id=\"${uploadId}\" type=\"button\">\n <i class=\"bi bi-x\"></i>\n </button>\n `;\n\n container.appendChild(preview);\n }\n\n /**\n * Update file upload progress\n * @param {string} uploadId - Upload ID\n * @param {number} progress - Progress (0-100)\n */\n updateFileProgress(uploadId, progress) {\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n const progressBar = preview.querySelector('.progress-bar');\n if (progressBar) {\n progressBar.style.width = `${progress}%`;\n }\n }\n }\n\n /**\n * Handle upload completion\n * @param {string} uploadId - Upload ID\n * @param {Object} result - Upload result data (contains file.id)\n */\n handleUploadComplete(uploadId, fileModel) {\n // Store the file data with its ID\n this.attachments.push({\n id: fileModel.id,\n name: fileModel.get(\"name\"),\n uploadId: uploadId\n });\n this.pendingUploads.delete(uploadId);\n\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n preview.classList.add('upload-complete');\n const progressContainer = preview.querySelector('.attachment-progress');\n if (progressContainer) {\n progressContainer.remove();\n }\n }\n }\n\n /**\n * Handle upload error\n * @param {string} uploadId - Upload ID\n * @param {Error} error - Error object\n */\n handleUploadError(uploadId, error) {\n this.pendingUploads.delete(uploadId);\n\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n preview.classList.add('upload-error');\n preview.querySelector('.attachment-info').innerHTML +=\n `<span class=\"text-danger ms-2\">Upload failed</span>`;\n }\n }\n\n /**\n * Remove attachment\n */\n async onActionRemoveAttachment(event, element) {\n const uploadId = element.dataset.uploadId;\n\n // Remove from pending uploads\n this.pendingUploads.delete(uploadId);\n\n // Remove from completed attachments\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n // TODO: Get the file ID from the preview and remove from attachments array\n preview.remove();\n }\n }\n\n\n\n /**\n * Send message\n */\n async onActionSendMessage(event, element) {\n const textarea = this.element.querySelector('.chat-input');\n const text = textarea.value.trim();\n\n // Don't send if empty and no attachments\n if (!text && this.attachments.length === 0) {\n return;\n }\n\n // Don't send if uploads are pending\n if (this.pendingUploads.size > 0) {\n // TODO: Show message that uploads are in progress\n return;\n }\n\n // Show busy state\n this.setBusy(true);\n\n // Emit event with message data\n this.emit('message:send', {\n text: text,\n files: this.attachments\n });\n\n // Note: Don't clear here - let the parent ChatView call clearInput() after successful send\n }\n\n /**\n * Enable or disable the entire input area\n * @param {boolean} enabled - Whether the input should be enabled\n */\n setEnabled(enabled) {\n const textarea = this.element?.querySelector('.chat-input');\n const button = this.element?.querySelector('.chat-send-btn');\n if (textarea) textarea.disabled = !enabled;\n if (button) button.disabled = !enabled;\n }\n\n /**\n * Set busy state (show/hide spinner)\n * @param {boolean} busy - Whether to show busy state\n */\n setBusy(busy) {\n const button = this.element.querySelector('.chat-send-btn');\n const icon = button.querySelector('.bi-send-fill');\n const spinner = button.querySelector('.spinner-border');\n\n if (busy) {\n button.disabled = true;\n icon.classList.add('d-none');\n spinner.classList.remove('d-none');\n } else {\n button.disabled = false;\n icon.classList.remove('d-none');\n spinner.classList.add('d-none');\n }\n }\n\n /**\n * Clear input and attachments\n */\n clearInput() {\n const textarea = this.element.querySelector('.chat-input');\n if (textarea) {\n textarea.value = '';\n textarea.style.height = 'auto';\n }\n\n const container = this.element.querySelector('[data-container=\"attachments\"]');\n if (container) {\n container.innerHTML = '';\n }\n\n this.attachments = [];\n this.pendingUploads.clear();\n \n // Reset busy state\n this.setBusy(false);\n }\n\n /**\n * Auto-resize textarea based on content\n * @param {HTMLTextAreaElement} textarea\n */\n autoResizeTextarea(textarea) {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';\n }\n\n /**\n * Format file size for display\n * @param {number} bytes\n * @returns {string}\n */\n formatFileSize(bytes) {\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];\n }\n\n /**\n * Escape HTML to prevent XSS\n * @param {string} text\n * @returns {string}\n */\n escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n}\n\n// Apply FileDropMixin\napplyFileDropMixin(ChatInputView);\n\nexport default ChatInputView;\n","import View from '@core/View.js';\nimport ChatMessageView from './ChatMessageView.js';\nimport ChatInputView from './ChatInputView.js';\n\n/**\n * ChatView - Modern chat interface with theme support\n * \n * Themes:\n * - 'compact' (default): Admin/activity feed style, list-based layout\n * - 'bubbles': Modern chat bubbles with left/right positioning\n * \n * Usage:\n * const chat = new ChatView({\n * adapter: myAdapter,\n * theme: 'compact', // or 'bubbles'\n * currentUserId: 123,\n * inputPlaceholder: 'Add a comment...',\n * inputButtonText: 'Send'\n * });\n */\nclass ChatView extends View {\n constructor(options = {}) {\n super({\n className: 'chat-view',\n ...options\n });\n\n this.adapter = options.adapter;\n this.theme = options.theme || 'compact'; // 'compact' or 'bubbles'\n this.currentUserId = options.currentUserId;\n this.inputPlaceholder = options.inputPlaceholder || 'Type a message...';\n this.inputButtonText = options.inputButtonText || 'Send';\n this.showFileInput = options.showFileInput !== false; // default true\n this.showInput = options.showInput !== false; // default true\n this.MessageViewClass = options.messageViewClass || ChatMessageView;\n this.messages = [];\n this.messageViews = new Map(); // Track message views by ID\n this._thinkingEl = null;\n }\n\n getTemplate() {\n return `\n <div class=\"chat-container chat-theme-${this.theme}\">\n <div class=\"chat-messages\" data-container=\"messages\"></div>\n ${this.showInput ? '<div class=\"chat-input-wrapper\" data-container=\"input\"></div>' : ''}\n </div>\n `;\n }\n\n async onInit() {\n // Initial fetch of messages\n this.messages = await this.adapter.fetch();\n\n // Create input view (if not hidden)\n if (this.showInput) {\n this.inputView = new ChatInputView({\n containerId: 'input',\n placeholder: this.inputPlaceholder,\n buttonText: this.inputButtonText,\n showFileInput: this.showFileInput\n });\n this.addChild(this.inputView);\n\n // Listen for new messages\n this.inputView.on('message:send', async (data) => {\n await this.handleSendMessage(data);\n });\n }\n }\n\n async onAfterRender() {\n // Build message views\n this._buildMessageViews();\n \n // Render children (like ListView does)\n await this._renderChildren();\n \n // Scroll to bottom\n this.scrollToBottom();\n }\n\n /**\n * Render child message views (similar to ListView._renderChildren)\n * @private\n */\n async _renderChildren() {\n // Collect message view IDs so we can skip them in the standard path\n const messageViewIds = new Set();\n this.messageViews.forEach(v => messageViewIds.add(v.id));\n\n // Render non-message children (e.g. input view) via the standard path\n for (const id in this.children) {\n const child = this.children[id];\n if (!child || messageViewIds.has(id)) continue;\n child.parent = this;\n await Promise.resolve(child.render()).catch(err =>\n console.warn(`ChatView child render error (${id})`, err)\n );\n }\n\n // Then place message views into the messages container\n const messagesContainer = this.element.querySelector('[data-container=\"messages\"]');\n if (!messagesContainer) return;\n\n this.messageViews.forEach((messageView) => {\n messagesContainer.appendChild(messageView.element);\n messageView.render(false);\n });\n }\n\n /**\n * Build message views for all messages (similar to ListView._buildItems)\n * @private\n */\n _buildMessageViews() {\n if (!this.messages || this.messages.length === 0) return;\n \n this.messages.forEach(message => {\n if (!this.messageViews.has(message.id)) {\n this._createMessageView(message);\n }\n });\n }\n\n /**\n * Create a message view (similar to ListView._createItemView)\n * @private\n */\n _createMessageView(message) {\n if (this.messageViews.has(message.id)) return;\n \n const isCurrentUser = message.author && message.author.id === this.currentUserId;\n \n const messageView = new this.MessageViewClass({\n message: message,\n theme: this.theme,\n isCurrentUser: isCurrentUser\n });\n \n this.addChild(messageView);\n this.messageViews.set(message.id, messageView);\n \n return messageView;\n }\n\n /**\n * Add a new message to the chat (for real-time updates)\n * @param {Object} message - Message data\n * @param {boolean} scroll - Whether to scroll to bottom after adding\n */\n addMessage(message, scroll = true) {\n if (this.messageViews.has(message.id)) return;\n \n const messageView = this._createMessageView(message);\n \n // If already rendered, append to DOM immediately\n if (this.isMounted()) {\n const messagesContainer = this.element.querySelector('[data-container=\"messages\"]');\n if (messagesContainer) {\n messagesContainer.appendChild(messageView.element);\n messageView.render(false);\n }\n }\n \n if (scroll) {\n this.scrollToBottom();\n }\n }\n\n /**\n * Handle sending a new message\n * @param {Object} data - Message data {text, files}\n * @private\n */\n async handleSendMessage(data) {\n try {\n // If there's text, send it as a note\n if (data.text && data.text.trim()) {\n const result = await this.adapter.addNote({\n text: data.text,\n files: data.files && data.files.length > 0 ? [data.files[0]] : []\n });\n \n if (!result.success) {\n throw new Error('Failed to send message');\n }\n }\n \n // If there are multiple files, or files without text, create a note for each\n const startIndex = (data.text && data.text.trim() && data.files?.length > 0) ? 1 : 0;\n \n for (let i = startIndex; i < (data.files?.length || 0); i++) {\n const file = data.files[i];\n const result = await this.adapter.addNote({\n text: '', // Empty text, just the file\n files: [file]\n });\n \n if (!result.success) {\n console.error('Failed to upload file:', file);\n }\n }\n \n // Fetch updated messages\n this.messages = await this.adapter.fetch();\n \n // Find the new message(s) and add them\n this.messages.forEach(message => {\n if (!this.messageViews.has(message.id)) {\n this.addMessage(message, true);\n }\n });\n \n // Clear input (this also resets busy state)\n this.inputView.clearInput();\n \n } catch (error) {\n console.error('Failed to send message:', error);\n // Reset busy state on error\n this.inputView.setBusy(false);\n try {\n this.getApp().toast.error('Failed to send message');\n } catch (e) {\n // Toast not available — fail silently\n }\n }\n }\n\n /**\n * Show an animated thinking indicator at the bottom of the messages area.\n * Only one indicator is shown at a time — subsequent calls update the text.\n * @param {string} [text='Thinking...'] - Status text to display\n */\n showThinking(text = 'Thinking...') {\n const container = this.element?.querySelector('[data-container=\"messages\"]');\n if (!container) return;\n\n if (!this._thinkingEl) {\n this._thinkingEl = document.createElement('div');\n this._thinkingEl.className = 'chat-thinking';\n this._thinkingEl.innerHTML = `\n <div class=\"chat-thinking-content\">\n <span class=\"chat-thinking-dots\">\n <span></span><span></span><span></span>\n </span>\n <span class=\"chat-thinking-text\"></span>\n <span class=\"chat-thinking-timer text-muted\"></span>\n </div>\n `;\n container.appendChild(this._thinkingEl);\n\n this._thinkingStart = Date.now();\n this._thinkingInterval = setInterval(() => {\n const elapsed = Math.floor((Date.now() - this._thinkingStart) / 1000);\n const mins = Math.floor(elapsed / 60);\n const secs = elapsed % 60;\n const timerEl = this._thinkingEl?.querySelector('.chat-thinking-timer');\n if (timerEl) {\n timerEl.textContent = mins > 0\n ? `${mins}m ${String(secs).padStart(2, '0')}s`\n : `${secs}s`;\n }\n }, 1000);\n }\n\n this._thinkingEl.querySelector('.chat-thinking-text').textContent = text;\n this.scrollToBottom();\n }\n\n /**\n * Remove the thinking indicator\n */\n hideThinking() {\n if (this._thinkingInterval) {\n clearInterval(this._thinkingInterval);\n this._thinkingInterval = null;\n }\n if (this._thinkingEl) {\n this._thinkingEl.remove();\n this._thinkingEl = null;\n }\n }\n\n /**\n * Enable or disable the chat input\n * @param {boolean} enabled - Whether the input should be enabled\n */\n setInputEnabled(enabled) {\n if (this.inputView?.setEnabled) {\n this.inputView.setEnabled(enabled);\n }\n }\n\n /**\n * Scroll chat to bottom\n */\n scrollToBottom() {\n const container = this.element.querySelector('.chat-messages');\n if (container) {\n requestAnimationFrame(() => {\n container.scrollTop = container.scrollHeight;\n });\n }\n }\n\n /**\n * Clear all messages\n */\n clearMessages() {\n this.messageViews.forEach((view) => {\n // Remove from parent's children hash so _renderChildren won't re-render them\n delete this.children[view.id];\n view.destroy();\n });\n this.messageViews.clear();\n this.messages = [];\n\n const container = this.element?.querySelector('[data-container=\"messages\"]');\n if (container) {\n container.innerHTML = '';\n }\n }\n\n /**\n * Refresh messages from adapter\n */\n async refresh() {\n this.clearMessages();\n this.messages = await this.adapter.fetch();\n this._buildMessageViews();\n \n if (this.isMounted()) {\n await this._renderChildren();\n this.scrollToBottom();\n }\n }\n}\n\nexport default ChatView;\n"],"names":["MetricsPermission","Model","constructor","data","super","endpoint","id_key","MetricsPermissionList","Collection","options","ModelClass","MetricsForms","edit","title","fields","name","type","label","columns","help","GeoIPForms","size","required","readonly","cols","step","GeoLocatedIP","lookup","ip","model","resp","rest","GET","success","EDIT_FORM","EDIT_LOCATION_FORM","EDIT_SECURITY_FORM","value","EDIT_NETWORK_FORM","GeoLocatedIPList","TablePage","Page","pageName","this","description","collection","defaultQuery","groupField","tableViewConfig","actions","contextMenu","batchActions","batchBarLocation","clickAction","addForm","formFields","formCreate","editForm","formEdit","itemView","itemViewClass","deleteTemplate","formDialogConfig","viewDialogOptions","searchable","sortable","filterable","paginated","selectionMode","selectable","filters","additionalFilters","hideActivePills","hideActivePillNames","searchPlacement","dayRangeFilter","groupBy","groupHeaderTemplate","groupHeaderLabel","groupHeaderStyle","rowStripe","tableOptions","striped","bordered","hover","responsive","emptyMessage","searchPlaceholder","showAdd","showExport","onItemView","onItemEdit","onItemDelete","onAdd","onExport","tableViewOptions","urlSyncEnabled","lastUpdated","isLoading","template","buildTemplate","onInit","applyQueryToCollection","tableView","TableView","containerId","fetchOnMount","async","event","showItemDialog","addChild","setupEventListeners","on","Date","toLocaleTimeString","updateStatusDisplay","syncUrl","params","query","Object","keys","length","start","parseInt","sort","search","reservedParams","entries","forEach","key","includes","startsWith","JSON","parse","e","field","parseFilterKey","hasOwnProperty","setParams","force","getApp","router","currentUrl","URL","window","location","currentParams","searchParams","desiredParams","collectionParams","stringify","hasChanges","some","String","_item","updateBrowserUrl","element","updatedElement","querySelector","textContent","countElement","count","meta","onEnter","requiresGroup","activeGroup","id","setTimeout","updateFilterPills","updateSortIcons","_openDeepLinkedItem","itemId","fetchOne","_clearItemParam","_setItemParam","fetchOnView","Modal","loading","fetch","error","hideLoading","showError","message","ViewClass","getItemViewClass","viewInstance","dialog","header","body","centered","getFormDialogConfig","getModelClass","DIALOG_OPTIONS","getModelName","refresh","getSelectedItems","clearSelection","batchAction","destroy","handler","confirm","items","results","Promise","allSettled","map","save","successes","filter","r","status","failures","app","toast","warning","handleFilterEdit","filterKey","filterConfig","getAllAvailableFilters","find","f","currentValue","config","result","form","filter_value","setFilter","restEnabled","render","clearAllFilters","onGroupChange","group","onBeforeDestroy","off","showStatus","create","resolveAccessor","fieldOrAccessor","get","TypeError","toDate","raw","ms","dataFormatter","normalizeEpoch","Number","isNaN","d","getTime","_err","isoDayKey","date","getFullYear","getMonth","padStart","getDate","groupByDay","access","bucketKey","parts","split","year","month","day","isFinite","now","todayKey","yesterdayKey","monthLabel","formatDayLabel","groupByField","opts","labels","format","fallback","prototype","call","RECENCY_LABELS","groupByRecency","dateKey","recencyBucketKey","STRING_FALSE_VALUES","Set","groupByBoolean","trueLabel","falseLabel","b","lower","trim","toLowerCase","has","Boolean","coerceBoolean","SideNavView","View","sections","activeSection","navWidth","contentPadding","enableResponsive","minWidth","viewOptions","tagName","className","sectionConfigs","sectionViews","sectionKeys","currentMode","resizeObserver","lastContainerWidth","_addSectionConfig","handleResize","bind","permissions","_hasPermission","push","view","parent","perm","activeUser","hasPerm","renderTemplate","nav","_buildDropdownNav","_buildSidebarNav","navWidthStyle","contentPaddingStyle","_normalizeBadge","badge","text","variant","_renderBadge","escapeHtml","isActive","icon","join","activeConfig","c","activeLabel","onAfterRender","_mountSection","_setupResponsive","disconnect","removeEventListener","values","showSection","console","warn","isMounted","contains","previousSection","_unmountSection","activeView","onSectionActivated","_updateNavState","emit","container","_showContentLoading","_hideContentLoading","spinner","document","createElement","innerHTML","style","cssText","prepend","remove","unmount","activeKey","querySelectorAll","link","section","dataset","classList","toggle","selectBtn","onActionNavigate","el","preventDefault","_updateMode","ResizeObserver","parentElement","observe","addEventListener","containerWidth","_getContainerWidth","Math","abs","offsetWidth","newMode","mode","getActiveSection","getSectionKeys","getSection","addSection","makeActive","removeSection","k","setBadge","existing","html","insertAdjacentHTML","_onModelChange","DetailHeaderView","iconTone","iconToneFn","iconHtml","titleField","titleFn","titleAffix","subtitlePath","subtitleFn","subtitlePlaceholder","subtitleEditAction","chips","activeField","auxFn","closable","enableTooltips","contextMenuConfig","_buildTemplate","_resolveTitle","_resolveTitleAffix","_resolveSubtitle","MOJOUtils","getNestedValue","attributes","_resolveIconTone","_resolveIconHtml","_resolveChips","chip","when","textPath","tooltip","action","subtitle","chipsHtml","tooltipAttrs","auxResult","auxHtml","switchHtml","actionsHtml","a","closeHtml","contextMenuHtml","hasGroup2","tone","iconHtmlSlot","iconClass","iconBody","titleAffixHtml","_contextMenuMounted","cm","ContextMenu","context","onActionDefault","dest","events","dispatch","onActionToggleActive","checked","disabled","set","Error","err","isConnected","DetailView","subclassClassName","mergedClassName","headerConfig","sectionsConfig","initialActiveSection","onBeforeBuild","headerView","sideNav","onAfterBuild","catch","VALID_TONES","MetricCard","valueIcon","hint","_resolve","valueText","_renderValue","valueIconHtml","hintHtml","opt","_","v","safeTone","t","add","setAttribute","hasAttribute","setValue","valueEl","span","setHint","slot","appendChild","StatusPanel","state","headline","_toneOpt","_stateOpt","_headlineOpt","_metaOpt","_iconOpt","_actionsOpt","_resolveTone","_resolveActions","arr","Array","isArray","btnClass","aIcon","Timeline","emptyText","limit","_itemsOpt","floor","_resolveItems","filtered","slice","_normalizeTone","it","toneCls","detail","setItems","KnownFieldsCard","knownKeys","rawCollapsed","rawLabel","showRaw","_dataOpt","_knownKeysOpt","_resolveData","_resolveKnownKeys","rowsHtml","spec","_renderRow","_renderRaw","_lookup","isMissing","hideEmpty","valueHtml","_formatValue","json","indexOf","cursor","part","formatter","out","apply","FilePreviewView","file","isImage","content_type","isPdf","getTemplate","thumbnailUrl","url","onActionViewFile","LightboxGallery","MOJO","plugins","show","src","alt","filename","open","PDFViewer","showDialog","SHORTLINK_SOURCE_OPTIONS","TWITTER_CARD_OPTIONS","flattenShortLinkMetadata","metadata","m","og_title","og_description","og_image","twitter_card","twitter_title","twitter_description","twitter_image","buildShortLinkMetadata","formData","flatKey","targetKey","extractShortLinkPayload","flatKeys","payload","ShortLink","ShortLinkList","ShortLinkClick","ShortLinkClickList","_shortLinkSharedFields","placeholder","min","rows","ShortLinkForms","CATEGORY_TONE","_capitalize","s","str","charAt","toUpperCase","CATEGORY_CONFIG","image","previewType","badgeClass","video","audio","pdf","spreadsheet","presentation","archive","other","getCategoryConfig","cat","getCategory","FilePreviewSection","categoryConfig","escapeAttr","getThumbnailUrl","poster","preview","getBestImageRendition","openFileInPreview","onActionDownloadFile","downloadFile","FileRenditionsSection","hasRenditions","_buildGalleryTemplate","isUploadPending","_buildWaitingTemplate","_buildEmptyTemplate","renditions","getRenditions","cards","_buildCard","ct","role","file_size","bytes","n","toFixed","formatBytes","dimensions","width","height","viewData","footer","onActionRefreshRenditions","onActionViewRendition","stopPropagation","Lightbox","images","startIndex","max","findIndex","img","fitToScreen","onActionCopyRenditionUrl","navigator","clipboard","isSecureContext","writeText","textarea","select","execCommand","removeChild","orig","onActionRegenerateFromSection","node","onActionRegenerateRenditions","href","download","click","FileSharesSection","fileId","source","_sharesCollection","sharesTable","visibility","divider","danger","emptyIcon","refreshShares","onActionShareFileFromSection","onActionShareFile","onActionCopyShareCode","code","base","shortlink_base_url","origin","replace","buildShortUrl","_e","onActionRevokeShare","row","closest","rowId","target","confirmText","confirmClass","is_active","FileOverviewSection","hasUrl","kpiSize","kpiType","kpiStatus","created","kpiUploaded","FileView","File","overviewSection","previewSection","detailsSection","DataView","showEmptyValues","emptyValueText","renditionsSection","sharesSection","metadataView","contextItems","fmt","_getCategoryConfig","_stopRenditionsPoll","onActionCopyUrl","onActionEditFile","modelForm","formConfig","FileForms","onActionMakePublic","is_public","onActionMakePrivate","formResult","maxlength","submitText","expire_days","track_clicks","note","share","shortUrl","copied","expiry","expires_at","toLocaleString","tracked","copyHint","summary","shortlink_code","alert","regenerateRenditions","_maybeStartRenditionsPoll","_renditionsPollTimer","attempts","tick","clearTimeout","onActionDeleteFile","sideNavView","VIEW_CLASS","MODEL_REF","ChatMessageView","theme","isCurrentUser","cls","getBubblesTemplate","getCompactTemplate","userClass","isAssistant","_getToolCallsTemplate","tool_calls","toolBadges","tc","div","esc","function","collapseId","attachments","attachmentsContainer","children","child","filePreview","ChatInputView","buttonText","showFileInput","pendingUploads","Map","enableFileDrop","dropZoneSelector","multiple","acceptedTypes","visualFeedback","dragOverClass","dragActiveClass","autoResizeTextarea","handleKeydown","shiftKey","onActionSendMessage","onFileDrop","files","uploadFile","fileModel","uploadId","random","addFilePreview","upload","onProgress","progress","updateFileProgress","onComplete","uploadResult","handleUploadComplete","handleUploadError","formatFileSize","progressBar","delete","progressContainer","onActionRemoveAttachment","setBusy","setEnabled","enabled","button","busy","clearInput","clear","scrollHeight","i","log","parseFloat","pow","applyFileDropMixin","ChatView","adapter","currentUserId","inputPlaceholder","inputButtonText","showInput","MessageViewClass","messageViewClass","messages","messageViews","_thinkingEl","inputView","handleSendMessage","_buildMessageViews","_renderChildren","scrollToBottom","messageViewIds","resolve","messagesContainer","messageView","_createMessageView","author","addMessage","scroll","addNote","showThinking","_thinkingStart","_thinkingInterval","setInterval","elapsed","mins","secs","timerEl","hideThinking","clearInterval","setInputEnabled","requestAnimationFrame","scrollTop","clearMessages"],"mappings":"qVAGA,MAAMA,0BAA0BC,EAC5B,WAAAC,CAAYC,EAAO,IACfC,MAAMD,EAAM,CACRE,SAAU,2BACVC,OAAQ,WAEhB,EAGJ,MAAMC,8BAA8BC,EAChC,WAAAN,CAAYO,EAAU,IAClBL,MAAM,CACFM,WAAYV,kBACZK,SAAU,8BACPI,GAEX,EAIC,MAACE,EAAe,CACjBC,KAAM,CACFC,MAAO,2BACPC,OAAQ,CACJ,CAAEC,KAAM,UAAWC,KAAM,OAAQC,MAAO,UAAWC,QAAQ,IAC3D,CAAEH,KAAM,mBAAoBC,KAAM,OAAQC,MAAO,mBAAoBE,KAAM,gCAAiCD,QAAQ,IACpH,CAAEH,KAAM,oBAAqBC,KAAM,OAAQC,MAAO,oBAAqBE,KAAM,oBAAqBD,QAAQ,OCtBhHE,EACY,CACVP,MAAO,gBACPQ,KAAM,KACNP,OAAQ,CACJ,CAAEC,KAAM,aAAcE,MAAO,aAAcD,KAAM,OAAQM,UAAU,EAAMC,UAAU,EAAMC,KAAM,GAC/F,CAAET,KAAM,SAAUE,MAAO,SAAUD,KAAM,OAAQQ,KAAM,GACvD,CAAET,KAAM,eAAgBE,MAAO,UAAWD,KAAM,OAAQQ,KAAM,GAC9D,CAAET,KAAM,eAAgBE,MAAO,eAAgBD,KAAM,OAAQQ,KAAM,GACnE,CAAET,KAAM,SAAUE,MAAO,SAAUD,KAAM,OAAQQ,KAAM,GACvD,CAAET,KAAM,OAAQE,MAAO,OAAQD,KAAM,OAAQQ,KAAM,GACnD,CAAET,KAAM,cAAeE,MAAO,cAAeD,KAAM,OAAQQ,KAAM,GACjE,CAAET,KAAM,WAAYE,MAAO,WAAYD,KAAM,OAAQQ,KAAM,GAC3D,CAAET,KAAM,WAAYE,MAAO,WAAYD,KAAM,SAAUS,KAAM,MAAOD,KAAM,GAC1E,CAAET,KAAM,YAAaE,MAAO,YAAaD,KAAM,SAAUS,KAAM,MAAOD,KAAM,KAgDxF,MAAME,qBAAqBzB,EACvB,WAAAC,CAAYC,EAAO,IACfC,MAAMD,EAAM,CACRE,SAAU,qBAElB,CAEA,mBAAasB,CAAOC,GAChB,MAAMC,EAAQ,IAAIH,aACZI,QAAaD,EAAME,KAAKC,IAAI,2BAA4B,CAAEJ,OAChE,OAAIE,EAAKG,SAAWH,EAAK3B,MAAQ2B,EAAK3B,KAAKA,KAChC,IAAIuB,aAAaI,EAAK3B,KAAKA,MAE/B,IACX,EAIJuB,aAAaQ,UAAYd,EACzBM,aAAaS,mBAAqBf,EAClCM,aAAaU,mBAjEK,CACVvB,MAAO,gBACPQ,KAAM,KACNP,OAAQ,CACJ,CACIC,KAAM,eACNE,MAAO,eACPD,KAAM,SACNQ,KAAM,GACNf,QAAS,CACL,CAAE4B,MAAO,GAAIpB,MAAO,QACpB,CAAEoB,MAAO,MAAOpB,MAAO,OACvB,CAAEoB,MAAO,SAAUpB,MAAO,UAC1B,CAAEoB,MAAO,OAAQpB,MAAO,QACxB,CAAEoB,MAAO,WAAYpB,MAAO,cAGpC,CAAEF,KAAM,YAAaE,MAAO,SAAUD,KAAM,SAAUQ,KAAM,GAC5D,CAAET,KAAM,gBAAiBE,MAAO,aAAcD,KAAM,SAAUQ,KAAM,GACpE,CAAET,KAAM,oBAAqBE,MAAO,iBAAkBD,KAAM,SAAUQ,KAAM,GAC5E,CAAET,KAAM,kBAAmBE,MAAO,eAAgBD,KAAM,SAAUQ,KAAM,GACxE,CAAET,KAAM,aAAcE,MAAO,aAAcD,KAAM,SAAUQ,KAAM,GACjE,CAAET,KAAM,SAAUE,MAAO,gBAAiBD,KAAM,SAAUQ,KAAM,GAChE,CAAET,KAAM,SAAUE,MAAO,MAAOD,KAAM,SAAUQ,KAAM,GACtD,CAAET,KAAM,WAAYE,MAAO,QAASD,KAAM,SAAUQ,KAAM,GAC1D,CAAET,KAAM,WAAYE,MAAO,iBAAkBD,KAAM,SAAUQ,KAAM,GACnE,CAAET,KAAM,gBAAiBE,MAAO,aAAcD,KAAM,SAAUQ,KAAM,KAwChFE,aAAaY,kBArCI,CACTzB,MAAO,eACPQ,KAAM,KACNP,OAAQ,CACJ,CAAEC,KAAM,MAAOE,MAAO,MAAOD,KAAM,OAAQQ,KAAM,GACjD,CAAET,KAAM,UAAWE,MAAO,mBAAoBD,KAAM,OAAQQ,KAAM,GAClE,CAAET,KAAM,MAAOE,MAAO,MAAOD,KAAM,OAAQQ,KAAM,IACjD,CAAET,KAAM,kBAAmBE,MAAO,kBAAmBD,KAAM,OAAQQ,KAAM,GACzE,CAAET,KAAM,WAAYE,MAAO,WAAYD,KAAM,OAAQQ,KAAM,GAC3D,CAAET,KAAM,YAAaE,MAAO,oBAAqBD,KAAM,SAAUQ,KAAM,GACvE,CAAET,KAAM,iBAAkBE,MAAO,iBAAkBD,KAAM,OAAQQ,KAAM,GACvE,CAAET,KAAM,YAAaE,MAAO,YAAaD,KAAM,WAAYQ,KAAM,MA4B7E,MAAMe,yBAAyB/B,EAC3B,WAAAN,CAAYO,EAAU,IAClBL,MAAM,CACFM,WAAYgB,aACZrB,SAAU,uBACPI,GAEX,ECzEJ,MAAM+B,kBAAkBC,EACtB,WAAAvC,CAAYO,EAAU,IACpBL,MAAM,IACDK,EACHiC,SAAUjC,EAAQiC,UAAYjC,EAAQM,MAAQ,UAIhD4B,KAAK9B,MAAQJ,EAAQI,OAAS8B,KAAKD,SACnCC,KAAKC,YAAcnC,EAAQmC,aAAe,GAG1CD,KAAKnC,WAAaC,EAAQD,YAAc,KACxCmC,KAAKE,WAAapC,EAAQoC,YAAc,KAGxCF,KAAKG,aAAerC,EAAQqC,cAAgB,CAAA,EAG5CH,KAAKI,WAAatC,EAAQsC,YAAc,QAIxCJ,KAAKK,gBAAkB,CAErB9B,QAAST,EAAQS,SAAW,GAC5B+B,QAASxC,EAAQwC,SAAW,KAC5BC,YAAazC,EAAQyC,aAAe,KACpCC,aAAc1C,EAAQ0C,cAAgB,KACtCC,iBAAkB3C,EAAQ2C,kBAAoB,MAC9CC,YAAa5C,EAAQ4C,aAAe,OAEpCC,QAAS7C,EAAQ6C,SAAW7C,EAAQ8C,YAAc9C,EAAQ+C,WAC1DC,SAAUhD,EAAQgD,UAAYhD,EAAQiD,UAAYjD,EAAQ8C,WAG1DI,SAAUlD,EAAQkD,UAAYlD,EAAQmD,cACtCC,eAAgBpD,EAAQoD,eACxBC,iBAAkBrD,EAAQqD,iBAC1BC,kBAAmBtD,EAAQsD,kBAG3BC,YAAmC,IAAvBvD,EAAQuD,WACpBC,UAA+B,IAArBxD,EAAQwD,SAClBC,YAAmC,IAAvBzD,EAAQyD,WACpBC,WAAiC,IAAtB1D,EAAQ0D,UAGnBC,cAAe3D,EAAQ2D,gBAAkB3D,EAAQ4D,WAAa,WAAa,QAG3EC,QAAS7D,EAAQ6D,SAAW7D,EAAQ8D,mBAAqB,GACzDC,gBAAiB/D,EAAQ+D,kBAAmB,EAC5CC,oBAAqBhE,EAAQgE,qBAAuB,GACpDC,gBAAiBjE,EAAQiE,iBAAmB,UAI5CC,eAAgBlE,EAAQkE,eACxBC,QAASnE,EAAQmE,QACjBC,oBAAqBpE,EAAQoE,oBAC7BC,iBAAkBrE,EAAQqE,iBAC1BC,iBAAkBtE,EAAQsE,iBAG1BC,UAAWvE,EAAQuE,UAGnBC,aAAc,CACZC,SAAS,EACTC,UAAU,EACVC,OAAO,EACPC,YAAY,KACT5E,EAAQwE,cAIbK,aAAc7E,EAAQ6E,cAAgB,oBACtCC,kBAAmB9E,EAAQ8E,mBAAqB,YAChDC,SAA6B,IAApB/E,EAAQ+E,QACjBC,YAAmC,IAAvBhF,EAAQgF,WAGpBC,WAAYjF,EAAQiF,WACpBC,WAAYlF,EAAQkF,WACpBC,aAAcnF,EAAQmF,aACtBC,MAAOpF,EAAQoF,MACfC,SAAUrF,EAAQqF,YAGfrF,EAAQsF,kBAIbpD,KAAKqD,gBAA4C,IAA3BvF,EAAQuF,eAG9BrD,KAAKsD,YAAc,KACnBtD,KAAKuD,WAAY,EAGjBvD,KAAKwD,SAAW1F,EAAQ0F,UAAYxD,KAAKyD,eAC3C,CAKA,aAAAA,GACE,MAAO,mzBAwBT,CAKA,YAAMC,SACEjG,MAAMiG,SAGP1D,KAAKE,aACJF,KAAKnC,WACPmC,KAAKE,WAAa,IAAIF,KAAKnC,WAE3BmC,KAAKE,WAAa,IAAIrC,GAK1BmC,KAAK2D,yBAIL3D,KAAK4D,UAAY,IAAIC,EAAU,CAC7B3D,WAAYF,KAAKE,WACjB4D,YAAa,QACbC,cAAc,KACX/D,KAAKK,gBACR0C,WAAYiB,MAAO9E,EAAO+E,KAExB,GAAIjE,KAAKK,gBAAgB0C,WACvB,OAAO/C,KAAKK,gBAAgB0C,WAAW7D,EAAO+E,SAE1CjE,KAAKkE,eAAehF,MAK9Bc,KAAKmE,SAASnE,KAAK4D,WAGnB5D,KAAKoE,qBACP,CAKA,mBAAAA,GAEMpE,KAAKqD,gBAAkBrD,KAAKE,aAE9BF,KAAKE,WAAWmE,GAAG,cAAe,KAChCrE,KAAKuD,WAAY,IAGnBvD,KAAKE,WAAWmE,GAAG,YAAa,KAC9BrE,KAAKuD,WAAY,EACjBvD,KAAKsD,4BAAA,IAAkBgB,MAAOC,qBAC9BvE,KAAKwE,yBAKTxE,KAAK4D,UAAUS,GAAG,iBAAkB,KAC9BrE,KAAKqD,gBACPrD,KAAKyE,YAwBTzE,KAAK4D,UAAUS,GAAG,WAAYL,OAAS9E,YACjCc,KAAK+C,kBACD/C,KAAK+C,WAAW7D,KAI1Bc,KAAK4D,UAAUS,GAAG,WAAYL,OAAS9E,YACjCc,KAAKgD,kBACDhD,KAAKgD,WAAW9D,KAI1Bc,KAAK4D,UAAUS,GAAG,aAAcL,OAAS9E,YACnCc,KAAKiD,oBACDjD,KAAKiD,aAAa/D,KAO5Bc,KAAK4D,UAAUS,GAAG,YAAaL,OAASC,eAMxCjE,KAAK4D,UAAUS,GAAG,eAAgBL,OAASxG,WACrCwC,KAAKK,gBAAgB8C,gBACjBnD,KAAKK,gBAAgB8C,SAAS3F,IAG1C,CAKA,sBAAAmG,GACE,MAAMe,EAAS,CAAA,EACTC,EAAQ,IAAK3E,KAAKG,gBAAiBH,KAAK2E,OAC9C,IAAKA,GAAuC,IAA9BC,OAAOC,KAAKF,GAAOG,OAC7B,YAGgB,IAAhBH,EAAMI,QAAqBL,EAAOK,MAAQC,SAASL,EAAMI,QAAU,QACpD,IAAfJ,EAAMjG,OAAoBgG,EAAOhG,KAAOsG,SAASL,EAAMjG,OAAS,SAGjD,IAAfiG,EAAMM,OAAoBP,EAAOO,KAAON,EAAMM,WAG7B,IAAjBN,EAAMO,SAAsBR,EAAOQ,OAASP,EAAMO,QAGtD,MAAMC,EAAiB,CAAC,QAAS,OAAQ,OAAQ,SAAU,OAAQ,SACnEP,OAAOQ,QAAQT,GAAOU,QAAQ,EAAEC,EAAK5F,MACnC,IAAKyF,EAAeI,SAASD,SAAkB,IAAV5F,GAAiC,KAAVA,EAE1D,GAAqB,iBAAVA,IAAuBA,EAAM8F,WAAW,MAAQ9F,EAAM8F,WAAW,MAC1E,IACEd,EAAOY,GAAOG,KAAKC,MAAMhG,EAC3B,OAASiG,GACPjB,EAAOY,GAAO5F,CAChB,MAEAgF,EAAOY,GAAO5F,IAMhBkF,OAAOC,KAAKH,GAAQI,OAAS,IAE/BF,OAAOC,KAAKH,GAAQW,QAAQC,IAC1B,MAAMM,MAAEA,EAAA5G,OAAOA,GAAW6G,EAAeP,GACzB,OAAXtG,GAA8B,WAAXA,IAAwB0F,EAAOoB,eAAeF,WAC7DlB,EAAOkB,KAIlB5F,KAAKE,WAAW6F,UAAU,IACrB/F,KAAKE,WAAWwE,UAChBA,IAGT,CAKA,OAAAD,CAAQuB,GAAQ,GACd,IAAKhG,KAAKqD,iBAAmBrD,KAAKE,aAAeF,KAAKiG,UAAUC,OAC9D,OAIF,MAAMC,EAAa,IAAIC,IAAIC,OAAOC,UAC5BC,EAAgB,CAAA,EACtB,IAAA,MAAYjB,EAAK5F,KAAUyG,EAAWK,aACxB,SAARlB,IACFiB,EAAcjB,GAAO5F,GAKzB,MAAM+G,EAAgB,CAAA,EAChBC,EAAmB1G,KAAKE,WAAWwE,QAAU,CAAA,EAG/CgC,EAAiB3B,QACnB0B,EAAc1B,MAAQ2B,EAAiB3B,OAErC2B,EAAiBhI,OACnB+H,EAAc/H,KAAOgI,EAAiBhI,MAEpCgI,EAAiBzB,OACnBwB,EAAcxB,KAAOyB,EAAiBzB,MAEpCyB,EAAiBxB,SACnBuB,EAAcvB,OAASwB,EAAiBxB,QAI1CN,OAAOQ,QAAQsB,GAAkBrB,QAAQ,EAAEC,EAAK5F,MACzC,CAAC,QAAS,OAAQ,OAAQ,UAAU6F,SAASD,SAAkB,IAAV5F,GAAiC,KAAVA,IAG7E+G,EAAcnB,GADK,iBAAV5F,EACY+F,KAAKkB,UAAUjH,GAEfA,KAM3B,MAAMkH,EACJhC,OAAOC,KAAK4B,GAAeI,KAAKvB,GAC9BwB,OAAOP,EAAcjB,IAAQ,MAAQwB,OAAOL,EAAcnB,IAAQ,MAEpEV,OAAOC,KAAK0B,GAAeM,KAAKvB,KAC5BA,KAAOmB,IAITzG,KAAK2E,MAAMoC,QACbN,EAAcM,MAAQ/G,KAAK2E,MAAMoC,OAGnC/G,KAAK2E,MAAQ8B,GACRG,GAAeZ,IAGpBhG,KAAKgH,iBAAiBP,GAAe,GAAM,EAC7C,CAKA,mBAAAjC,GACE,IAAKxE,KAAKiH,QAAS,OAGnB,MAAMC,EAAiBlH,KAAKiH,QAAQE,cAAc,gCAC9CD,IACFA,EAAeE,YAAcpH,KAAKsD,aAAe,SAInD,MAAM+D,EAAerH,KAAKiH,QAAQE,cAAc,gCAChD,GAAIE,GAAgBrH,KAAKE,WAAY,CACnC,MAAMoH,EAAQtH,KAAKE,WAAWqH,MAAMD,OAAStH,KAAKE,WAAW4E,SAC7DuC,EAAaD,YAAcE,CAC7B,CACF,CAKA,aAAME,SACE/J,MAAM+J,UAERxH,KAAKlC,QAAQ2J,gBAAkBzH,KAAK2E,MAAM3E,KAAKI,aAAeJ,KAAKiG,SAASyB,cAC9E1H,KAAK2E,MAAM3E,KAAKI,YAAcJ,KAAKiG,SAASyB,YAAYC,IAG1D3H,KAAK2D,yBAGD3D,KAAK4D,WAAa5D,KAAK4D,UAAUqD,SACnCW,WAAW,KACT5H,KAAK4D,UAAUiE,oBACf7H,KAAK4D,UAAUkE,mBACd,KAID9H,KAAK2E,MAAMoC,OACb/G,KAAK+H,oBAAoB/H,KAAK2E,MAAMoC,MAExC,CAKA,yBAAMgB,CAAoBC,GACxB,IACE,MAAM9I,QAAcc,KAAKE,WAAW+H,SAASD,GACzC9I,SACIc,KAAKkE,eAAehF,EAE9B,OAASyG,GAEP3F,KAAKkI,iBACP,CACF,CAKA,oBAAMhE,CAAehF,GAKnB,GAHAc,KAAKmI,cAAcjJ,EAAMyI,IAGrB3H,KAAK4D,UAAUwE,YACjB,IACEC,EAAMC,gBACApJ,EAAMqJ,OACd,OAASC,GAIP,OAHAH,EAAMI,aAAY,GAClBJ,EAAMK,UAAUF,GAAOhL,MAAMgL,OAASA,GAAOG,SAAW,oCACxD3I,KAAKkI,iBAEP,CAAA,QACEG,EAAMI,aAAY,EACpB,CAGF,MAAMG,EAAY5I,KAAK4D,UAAUiF,iBAAiB3J,GAElD,GAAI0J,EAAW,CACb,MAAME,EAAe,IAAIF,EAAU,CAAE1J,QAAOgB,WAAYF,KAAKE,mBACvDmI,EAAMU,OAAO,CACjBC,QAAQ,EACRC,KAAMH,EACNpK,KAAM,KACNwK,UAAU,KACPlJ,KAAK4D,UAAUuF,oBAAoBnJ,KAAK4D,UAAUwF,cAAclK,OAEhE0J,EAAUS,kBACVrJ,KAAK4D,UAAUxC,mBAEtB,YACQiH,EAAM7K,KAAK,CACfU,MAAO,QAAQ8B,KAAK4D,UAAU0F,aAAapK,OAAWA,EAAMyI,KAC5DzI,UAKJc,KAAKkI,iBACP,CAKA,aAAAC,CAAcH,GACZ,MAAMtD,EAAS,IAAK1E,KAAK2E,MAAOoC,MAAOiB,GACvChI,KAAK2E,MAAQD,EACb1E,KAAKgH,iBAAiBtC,GAAQ,GAAM,EACtC,CAKA,eAAAwD,UACSlI,KAAK2E,MAAMoC,MAClB/G,KAAKgH,iBAAiBhH,KAAK2E,OAAO,GAAM,EAC1C,CAKA,aAAM4E,SACEvJ,KAAK4D,UAAU2F,SACvB,CAKA,gBAAAC,GACE,OAAOxJ,KAAK4D,UAAU4F,kBACxB,CAKA,cAAAC,GACEzJ,KAAK4D,UAAU6F,gBACjB,CA+BA,iBAAMC,EAAY9D,MAChBA,EAAAlG,MACAA,EAAAiK,QACAA,GAAU,EAAAC,QACVA,EAAU,KAAAtL,MACVA,EAAQ,SAAAqK,QACRA,EAAAkB,QACAA,GAAU,GACR,IACF,MAAMC,EAAQ9J,KAAK4D,UAAY5D,KAAK4D,UAAU4F,mBAAqB,GACnE,IAAKM,EAAMhF,OAAQ,OAAO,EAE1B,GAAI+E,WACexB,EAAMwB,QACrBlB,GAAW,GAAGrK,KAASwL,EAAMhF,oBAEtB,OAAO,EAGlB,MAAMiF,QAAgBC,QAAQC,WAAWH,EAAMI,IAAI,EAAGhL,WAChD0K,EAAgBA,EAAQ1K,GACxByK,EAAgBzK,EAAMyK,UACnBzK,EAAMiL,KAAK,CAAEvE,CAACA,GAAQlG,MAGzB0K,EAAYL,EAAQM,UAAyB,cAAbC,EAAEC,QAAwBzF,OAC1D0F,EAAWT,EAAQjF,OAASsF,EAE5BK,EAAMzK,KAAKiG,OAASjG,KAAKiG,SAAW,KAa1C,OAZiB,IAAbuE,EACFC,GAAKC,OAAOpL,UAAU,GAAGhB,MAAU8L,qBACZ,IAAdA,EACTK,GAAKC,OAAOlC,QAAQ,GAAGlK,oBAAwBkM,aAE/CC,GAAKC,OAAOC,UACV,GAAGrM,MAAU8L,gBAAwBI,YAIzCxK,KAAK4D,UAAU6F,uBACTzJ,KAAK4D,UAAU2F,UACda,CACT,CAKA,sBAAMQ,CAAiBC,GACrB,MAAMC,EAAe9K,KAAK4D,UAAUmH,yBAAyBC,KAAKC,GAAKA,EAAE3F,MAAQuF,GAC3EK,EAAelL,KAAKE,WAAWwE,OAAOmG,GAE5C,IAAKC,EAAc,OAGnB,MAAMlF,EAAQ,CACZxH,KAAM,eACNE,MAAOwM,EAAaxM,OAASuM,EAC7BnL,MAAOwL,KACJJ,EAAaK,QAGZC,QAAe/C,EAAMgD,KAAK,CAC9BnN,MAAO,QAAQ0H,EAAMtH,eACrBI,KAAM,KACNP,OAAQ,CAACyH,KAGPwF,QAAkC,IAAxBA,EAAOE,eACnBtL,KAAK4D,UAAU2H,UAAUV,EAAWO,EAAOE,cAEvCtL,KAAKE,WAAWsL,aAClBxL,KAAKE,WAAWqI,cAEZvI,KAAK4D,UAAU6H,SACrBzL,KAAKyE,UAET,CAKA,eAAAiH,GACE,IAAK1L,KAAKE,WAAY,OAGtB,MAAM6E,MAAEA,EAAArG,KAAOA,EAAAuG,KAAMA,GAASjF,KAAKE,WAAWwE,OAC9C1E,KAAKE,WAAWwE,OAAS,CAAEK,QAAOrG,QAC9BuG,IAAMjF,KAAKE,WAAWwE,OAAOO,KAAOA,GAExCjF,KAAKyE,UAEDzE,KAAKE,WAAWsL,YAClBxL,KAAKE,WAAWqI,QAEhBvI,KAAK4D,UAAU6H,QAEnB,CAEA,mBAAME,CAAcC,GACXA,GAAU5L,KAAKE,YAAeF,KAAKlC,QAAQ2J,gBAChDzH,KAAK2E,MAAM3E,KAAKI,YAAcwL,EAAMjE,GACpC3H,KAAK2D,yBACD3D,KAAKE,YAAcF,KAAKE,WAAWsL,aACrCxL,KAAKE,WAAWqI,QAEtB,CAKA,qBAAMsD,GAEA7L,KAAKE,aACPF,KAAKE,WAAW4L,IAAI,eACpB9L,KAAKE,WAAW4L,IAAI,cAGlB9L,KAAK4D,YACP5D,KAAK4D,UAAUkI,IAAI,kBACnB9L,KAAK4D,UAAUkI,IAAI,gBACnB9L,KAAK4D,UAAUkI,IAAI,cACnB9L,KAAK4D,UAAUkI,IAAI,cACnB9L,KAAK4D,UAAUkI,IAAI,YACnB9L,KAAK4D,UAAUkI,IAAI,YACnB9L,KAAK4D,UAAUkI,IAAI,cACnB9L,KAAK4D,UAAUkI,IAAI,aACnB9L,KAAK4D,UAAUkI,IAAI,uBAGfrO,MAAMoO,iBACd,CAKA,cAAIE,GACF,OAAmC,IAA5B/L,KAAKlC,QAAQiO,UACtB,CAKA,aAAOC,CAAOlO,EAAU,IACtB,OAAO,IAAIkC,KAAKlC,EAClB,EC3qBF,SAASmO,EAAgBC,GACvB,GAA+B,mBAApBA,EAAgC,OAAOA,EAClD,GAA+B,iBAApBA,EACT,OAAQhN,GAAWA,GAA8B,mBAAdA,EAAMiN,IAAqBjN,EAAMiN,IAAID,GAAmB,KAE7F,MAAM,IAAIE,UAAU,sEACtB,CAUA,SAASC,EAAOC,GACd,GAAW,MAAPA,GAAuB,KAARA,EAAY,OAAO,KACtC,IACE,MAAMC,EAAKC,EAAcC,eAAeH,GACxC,GAAW,KAAPC,GAAmB,MAANA,GAAcG,OAAOC,MAAMJ,GAAK,OAAO,KACxD,MAAMK,EAAIL,aAAcjI,KAAOiI,EAAK,IAAIjI,KAAKiI,GAC7C,OAAOG,OAAOC,MAAMC,EAAEC,WAAa,KAAOD,CAC5C,OAASE,GACP,OAAO,IACT,CACF,CA2CA,SAASC,EAAUC,GACjB,OAAMA,aAAgB1I,OAASoI,OAAOC,MAAMK,EAAKH,WAI1C,GAHGG,EAAKC,iBACLnG,OAAOkG,EAAKE,WAAa,GAAGC,SAAS,EAAG,QACxCrG,OAAOkG,EAAKI,WAAWD,SAAS,EAAG,OAHuB,IAKtE,CAwBO,SAASE,EAAWnB,GACzB,MAAMoB,EAASrB,EAAgBC,GAC/B,MAAO,CACLjK,QAAU/C,IACR,MAAM8N,EAAOX,EAAOiB,EAAOpO,IAC3B,OAAO8N,EAAOD,EAAUC,GAAQ,MAElC7K,iBAAmBmD,GArEvB,SAAwBiI,GACtB,IAAKA,GAAkC,iBAAdA,EAAwB,MAAO,GAExD,MAAMC,EAAQD,EAAUE,MAAM,KAC9B,GAAqB,IAAjBD,EAAM1I,OAAc,OAAOyI,EAC/B,MAAMG,EAAOhB,OAAOc,EAAM,IACpBG,EAAQjB,OAAOc,EAAM,IACrBI,EAAMlB,OAAOc,EAAM,IACzB,IAAKd,OAAOmB,SAASH,KAAUhB,OAAOmB,SAASF,KAAWjB,OAAOmB,SAASD,GAAM,OAAOL,EAEvF,MAAMO,qBAAUxJ,KACVyJ,EAAWhB,EAAUe,GAErBE,EAAejB,EADC,IAAIzI,KAAKwJ,EAAIb,cAAea,EAAIZ,WAAYY,EAAIV,UAAY,IAGlF,GAAIG,IAAcQ,EAAU,MAAO,QACnC,GAAIR,IAAcS,EAAc,MAAO,YAEvC,MACMC,EADS,CAAC,MAAO,MAAO,MAAO,MAAO,MAAO,MAAO,MAAO,MAAO,MAAO,MAAO,MAAO,OACnEN,EAAQ,IAAM,GACxC,OAAID,IAASI,EAAIb,cACR,GAAGgB,KAAcL,IAEnB,GAAGK,KAAcL,MAAQF,GAClC,CA6C+BQ,CAAe5I,GAE9C,CAuCO,SAAS6I,EAAajC,EAAiBkC,EAAO,IACnD,MAAMd,EAASrB,EAAgBC,IACzBmC,OAAEA,EAAAC,OAAQA,EAAAC,SAAQA,GAAaH,EAErC,MAAO,CACLnM,QAAU/C,IACR,MAAMoN,EAAMgB,EAAOpO,GACnB,OAAW,MAAPoN,GAAuB,KAARA,EACE,MAAZiC,EAAmBzH,OAAOyH,GAAY,KAExCzH,OAAOwF,IAEhBnK,iBAAmBmD,GACb+I,GAAUzJ,OAAO4J,UAAU1I,eAAe2I,KAAKJ,EAAQ/I,GAClD+I,EAAO/I,GAEM,mBAAXgJ,EACFA,EAAOhJ,GAETA,EAGb,CAIA,MAAMoJ,EAAiB,CACrB,kBAAmB,QACnB,sBAAuB,YACvB,sBAAuB,YACvB,uBAAwB,aACxB,sBAAuB,oBACvB,kBAAmB,SAkEd,SAASC,EAAezC,GAC7B,MAAMoB,EAASrB,EAAgBC,GAC/B,MAAO,CACLjK,QAAU/C,GA9Dd,SAA0B8N,GACxB,KAAMA,aAAgB1I,OAASoI,OAAOC,MAAMK,EAAKH,WAAY,OAAO,KAEpE,MAAMiB,qBAAUxJ,KACVsK,EAAU7B,EAAUC,GAE1B,OAAI4B,IADa7B,EAAUe,GACM,kBAG7Bc,IAAY7B,EADE,IAAIzI,KAAKwJ,EAAIb,cAAea,EAAIZ,WAAYY,EAAIV,UAAY,IACjC,sBAGzCJ,GADiB,IAAI1I,KAAKwJ,EAAIb,cAAea,EAAIZ,WAAYY,EAAIV,UAAY,GAChD,sBAE7BJ,EAAKC,gBAAkBa,EAAIb,eAAiBD,EAAKE,aAAeY,EAAIZ,WAC/D,uBAGLF,EAAKC,gBAAkBa,EAAIb,cACtB,sBAGF,iBACT,CAuCwB4B,CAAiBxC,EAAOiB,EAAOpO,KACnDiD,iBAAmBmD,GAAQoJ,EAAepJ,IAAQA,EAEtD,CAGA,MAAMwJ,qBAA0BC,IAAI,CAAC,QAAS,IAAK,KAAM,QAiDlD,SAASC,EAAe9C,EAAiBkC,EAAO,IACrD,MAAMd,EAASrB,EAAgBC,GACzB+C,EAA8B,MAAlBb,EAAKa,UAAoBb,EAAKa,UAAY,MACtDC,EAAgC,MAAnBd,EAAKc,WAAqBd,EAAKc,WAAa,KAE/D,MAAO,CACLjN,QAAU/C,IACR,MAAMiQ,EAhDZ,SAAuB7C,GACrB,GAAW,MAAPA,EAAa,OAAO,KACxB,GAAmB,kBAARA,EAAmB,OAAOA,EACrC,GAAmB,iBAARA,EAAkB,OAAe,IAARA,EACpC,GAAmB,iBAARA,EAAkB,CAC3B,MAAM8C,EAAQ9C,EAAI+C,OAAOC,cACzB,MAAc,KAAVF,EAAqB,MACjBN,EAAoBS,IAAIH,EAClC,CACA,OAAOI,QAAQlD,EACjB,CAsCgBmD,CAAcnC,EAAOpO,IAC/B,OAAU,OAANiQ,EAAmB,KAChBA,EAAI,OAAS,SAEtBhN,iBAAmBmD,GACL,SAARA,EAAuB2J,EACf,UAAR3J,EAAwB4J,EACrB5J,EAGb,CClUA,MAAMoK,oBAAoBC,EACtB,WAAApS,CAAYO,EAAU,IAClB,MAAM8R,SACFA,EAAW,GAAAC,cACXA,EAAAC,SACAA,EAAAC,eACAA,EAAAC,iBACAA,EAAAC,SACAA,KACGC,GACHpS,EAEJL,MAAM,CACF0S,QAAS,MACTC,UAAW,mBACRF,IAIPlQ,KAAK8P,SAAWA,GAAY,IAC5B9P,KAAK+P,eAAiBA,GAAkB,gBACxC/P,KAAKgQ,kBAAwC,IAArBA,EACxBhQ,KAAKiQ,SAAWA,GAAY,IAG5BjQ,KAAKqQ,eAAiB,GACtBrQ,KAAKsQ,aAAe,GACpBtQ,KAAKuQ,YAAc,GACnBvQ,KAAK6P,cAAgB,KACrB7P,KAAKwQ,YAAc,UACnBxQ,KAAKyQ,eAAiB,KACtBzQ,KAAK0Q,mBAAqB,EAG1B,IAAA,MAAWvF,KAAUyE,EACjB5P,KAAK2Q,kBAAkBxF,GAI3BnL,KAAK6P,cAAgBA,GAAiB7P,KAAKuQ,YAAY,IAAM,KAG7DvQ,KAAK4Q,aAAe5Q,KAAK4Q,aAAaC,KAAK7Q,KAC/C,CAOA,iBAAA2Q,CAAkBxF,GACM,YAAhBA,EAAO9M,KAMP8M,EAAO2F,cAAgB9Q,KAAK+Q,eAAe5F,EAAO2F,eAItD9Q,KAAKqQ,eAAeW,KAAK7F,GACzBnL,KAAKuQ,YAAYS,KAAK7F,EAAO7F,KAEzB6F,EAAO8F,OACPjR,KAAKsQ,aAAanF,EAAO7F,KAAO6F,EAAO8F,KACvC9F,EAAO8F,KAAKC,OAASlR,OAdrBA,KAAKqQ,eAAeW,KAAK,CAAE3S,KAAM,UAAWC,MAAO6M,EAAO7M,OAgBlE,CAQA,cAAAyS,CAAeI,GACX,IACI,OAAOnR,KAAKiG,SAASmL,WAAWC,QAAQF,EAC5C,CAAA,MACI,OAAO,CACX,CACJ,CAMA,oBAAMG,GAIF,MAAMC,EAA2B,aAArBvR,KAAKwQ,YACXxQ,KAAKwR,oBACLxR,KAAKyR,mBAELC,EAAgB,UAAU1R,KAAK8P,aAC/B6B,EAAsB,YAAY3R,KAAK+P,iBAE7C,MAAO,iBACoB,aAArB/P,KAAKwQ,YAA6B,+CACJe,yFACmCI,0BAC/D,+FAEkCD,MAAkBH,6FACeI,6DAI/E,CASA,eAAAC,CAAgBC,GACZ,GAAIA,UAAmD,IAAVA,GAA6B,KAAVA,EAAc,OAAO,KACrF,GAAqB,iBAAVA,GAAuC,iBAAVA,EACpC,MAAO,CAAEC,KAAMhL,OAAO+K,GAAQE,QAAS,SAE3C,GAAqB,iBAAVF,QAAqC,IAAfA,EAAMC,MAAqC,OAAfD,EAAMC,MAAgC,KAAfD,EAAMC,KAAa,CACnG,MAAMC,EAAUF,EAAME,SAAW,QACjC,MAAO,CAAED,KAAMhL,OAAO+K,EAAMC,MAAOC,UACvC,CACA,OAAO,IACX,CAQA,YAAAC,CAAa7G,GACT,MAAM0G,EAAQ7R,KAAK4R,gBAAgBzG,EAAO0G,OAC1C,OAAKA,EACE,oCAAoC7R,KAAKiS,WAAWJ,EAAME,aAAa/R,KAAKiS,WAAWJ,EAAMC,eADjF,EAEvB,CAOA,gBAAAL,GACI,OAAOzR,KAAKqQ,eAAenG,IAAIiB,IAC3B,GAAoB,YAAhBA,EAAO9M,KACP,MAAO,8BAA8B2B,KAAKiS,WAAW9G,EAAO7M,eAEhE,MAAM4T,EAAW/G,EAAO7F,MAAQtF,KAAK6P,cAC/BsC,EAAOhH,EAAOgH,KAAO,gBAAgBnS,KAAKiS,WAAW9G,EAAOgH,cAAgB,GAC5EN,EAAQ7R,KAAKgS,aAAa7G,GAChC,MAAO,2BAA2B+G,EAAW,SAAW,4CAA4ClS,KAAKiS,WAAW9G,EAAO7F,SAAS6M,KAAQnS,KAAKiS,WAAW9G,EAAO7M,SAASuT,UAC7KO,KAAK,GACZ,CAOA,iBAAAZ,GACI,MAAMa,EAAerS,KAAKqQ,eAAerF,QAAUsH,EAAEhN,MAAQtF,KAAK6P,eAC5D0C,EAAcF,EAAeA,EAAa/T,MAAQ0B,KAAKuQ,YAAY,GAEnEzG,EAAQ9J,KAAKqQ,eACdhG,OAAOiI,GAAgB,YAAXA,EAAEjU,MACd6L,IAAIiB,IACD,MAAM+G,EAAW/G,EAAO7F,MAAQtF,KAAK6P,cAC/BgC,EAAQ7R,KAAKgS,aAAa7G,GAChC,MAAO,oFAEgC+G,EAAW,SAAW,8GAE7BlS,KAAKiS,WAAW9G,EAAO7F,sFAEzC6F,EAAOgH,KAAO,gBAAgBnS,KAAKiS,WAAW9G,EAAOgH,mBAAqB,mCAC1EnS,KAAKiS,WAAW9G,EAAO7M,uCACvBuT,kCACAK,EAAW,sCAAwC,uFAIlEE,KAAK,IAEZ,MAAO,qMAIOC,GAAcF,KAAO,gBAAgBE,EAAaF,aAAe,iCAC3DnS,KAAKiS,WAAWM,yFAEMzI,sCAG9C,CAMA,mBAAM0I,SACI/U,MAAM+U,gBAGRxS,KAAK6P,qBACC7P,KAAKyS,cAAczS,KAAK6P,eAI9B7P,KAAKgQ,kBACLhQ,KAAK0S,kBAEb,CAEA,qBAAM7G,SACIpO,MAAMoO,kBAGR7L,KAAKyQ,iBACLzQ,KAAKyQ,eAAekC,aACpB3S,KAAKyQ,eAAiB,MAGJ,oBAAXpK,QACPA,OAAOuM,oBAAoB,SAAU5S,KAAK4Q,cAI9C,IAAA,MAAWK,KAAQrM,OAAOiO,OAAO7S,KAAKsQ,cAC9BW,GAAgC,mBAAjBA,EAAKtH,eACdsH,EAAKtH,SAGvB,CAWA,iBAAMmJ,CAAYxN,GACd,IAAKtF,KAAKsQ,aAAahL,GAEnB,OADAyN,QAAQC,KAAK,yBAAyB1N,sBAC/B,EAGX,GAAIA,IAAQtF,KAAK6P,cAAe,CAE5B,MAAMoB,EAAOjR,KAAKsQ,aAAahL,GAC/B,GAAI2L,GAAQA,EAAKgC,aAAejT,KAAKiH,SAASiM,SAASjC,EAAKhK,SACxD,OAAO,CAEf,CAEA,MAAMkM,EAAkBnT,KAAK6P,cAC7B7P,KAAK6P,cAAgBvK,EAGjB6N,GAAmBA,IAAoB7N,SACjCtF,KAAKoT,gBAAgBD,SAIzBnT,KAAKyS,cAAcnN,GAGzB,MAAM+N,EAAarT,KAAKsQ,aAAahL,GAarC,OAZI+N,GAAYC,0BACND,EAAWC,qBAIrBtT,KAAKuT,gBAAgBjO,GAErBtF,KAAKwT,KAAK,kBAAmB,CACzB3D,cAAevK,EACf6N,qBAGG,CACX,CAOA,mBAAMV,CAAcnN,GAChB,MAAM2L,EAAOjR,KAAKsQ,aAAahL,GAC/B,IAAK2L,EAAM,OAEX,MAAMwC,EAAYzT,KAAKiH,SAASE,cAAc,kCAC9C,GAAKsM,IAEAxC,EAAKgC,YAAa,CACnBjT,KAAK0T,oBAAoBD,GACzB,UACUxC,EAAKxF,QAAO,EAAMgI,EAC5B,CAAA,QACIzT,KAAK2T,oBAAoBF,EAC7B,CACJ,CACJ,CAOA,mBAAAC,CAAoBD,GAChB,IAAKA,EAAW,OAChB,IAAIG,EAAUH,EAAUtM,cAAc,gBACjCyM,IACDA,EAAUC,SAASC,cAAc,OACjCF,EAAQxD,UAAY,cACpBwD,EAAQG,UAAY,mIACpBH,EAAQI,MAAMC,QAAU,uEACxBR,EAAUS,QAAQN,GAE1B,CAOA,mBAAAD,CAAoBF,GAChB,IAAKA,EAAW,OAChB,MAAMG,EAAUH,EAAUtM,cAAc,gBACpCyM,KAAiBO,QACzB,CAOA,qBAAMf,CAAgB9N,GAClB,MAAM2L,EAAOjR,KAAKsQ,aAAahL,GAC1B2L,GAASA,EAAKgC,mBAEbhC,EAAKmD,SACf,CAOA,eAAAb,CAAgBc,GACZ,IAAKrU,KAAKiH,QAAS,OAGnBjH,KAAKiH,QAAQqN,iBAAiB,8BAA8BjP,QAAQkP,IAChE,MAAMC,EAAUD,EAAKE,QAAQD,QACzBA,GACAD,EAAKG,UAAUC,OAAO,SAAUH,IAAYH,KAKpD,MAAMO,EAAY5U,KAAKiH,QAAQE,cAAc,wBAC7C,GAAIyN,EAAW,CACX,MAAMzJ,EAASnL,KAAKqQ,eAAerF,KAAKsH,GAAKA,EAAEhN,MAAQ+O,GACnDlJ,IACAyJ,EAAUxN,YAAc+D,EAAO7M,MAEvC,CACJ,CAMA,sBAAMuW,CAAiB5Q,EAAO6Q,GAC1B7Q,EAAM8Q,iBACN,MAAMP,EAAUM,EAAGL,QAAQD,QAI3B,OAHIA,SACMxU,KAAK8S,YAAY0B,IAEpB,CACX,CAUA,gBAAA9B,GACI,GAAK1S,KAAKiH,SAAYjH,KAAKgQ,iBAI3B,GAFAhQ,KAAKgV,cAEyB,oBAAnBC,eAAgC,CACvCjV,KAAKyQ,eAAiB,IAAIwE,eAAe,KACrCjV,KAAK4Q,iBAET,MAAM6C,EAAYzT,KAAKiH,QAAQiO,eAAiBlV,KAAKiH,QACrDjH,KAAKyQ,eAAe0E,QAAQ1B,EAChC,MACIpN,OAAO+O,iBAAiB,SAAUpV,KAAK4Q,aAE/C,CAKA,kBAAMA,GACF,MAAMyE,EAAiBrV,KAAKsV,qBACxBC,KAAKC,IAAIH,EAAiBrV,KAAK0Q,oBAAsB,KACrD1Q,KAAK0Q,mBAAqB2E,QACpBrV,KAAKgV,cAEnB,CAOA,kBAAAM,GACI,OAAKtV,KAAKiH,UACQjH,KAAKiH,QAAQiO,eAAiBlV,KAAKiH,SACpCwO,aAFSzV,KAAKiQ,QAGnC,CAMA,iBAAM+E,GACF,MAAMK,EAAiBrV,KAAKsV,qBACtBI,EAAUL,EAAiBrV,KAAKiQ,SAAW,WAAa,UAE1DyF,IAAY1V,KAAKwQ,cACjBxQ,KAAKwQ,YAAckF,EACf1V,KAAKiT,mBACCjT,KAAKyL,SAEfzL,KAAKwT,KAAK,yBAA0B,CAChCmC,KAAM3V,KAAKwQ,YACX6E,mBAGZ,CAUA,gBAAAO,GACI,OAAO5V,KAAK6P,aAChB,CAMA,cAAAgG,GACI,MAAO,IAAI7V,KAAKuQ,YACpB,CAOA,UAAAuF,CAAWxQ,GACP,OAAOtF,KAAKsQ,aAAahL,IAAQ,IACrC,CAQA,gBAAMyQ,CAAW5K,EAAQ6K,GAAa,GAClC,OAAI7K,EAAO7F,KAAOtF,KAAKsQ,aAAanF,EAAO7F,MACvCyN,QAAQC,KAAK,yBAAyB7H,EAAO7F,wBACtC,IAGXtF,KAAK2Q,kBAAkBxF,GAEnBnL,KAAKiT,oBACCjT,KAAKyL,SACPuK,GAAc7K,EAAO7F,WACftF,KAAK8S,YAAY3H,EAAO7F,MAItCtF,KAAKwT,KAAK,gBAAiB,CAAErI,YACtB,EACX,CAOA,mBAAM8K,CAAc3Q,GAChB,MAAM2L,EAAOjR,KAAKsQ,aAAahL,GAC/B,OAAK2L,GAMuB,mBAAjBA,EAAKtH,eACNsH,EAAKtH,iBAIR3J,KAAKsQ,aAAahL,GACzBtF,KAAKuQ,YAAcvQ,KAAKuQ,YAAYlG,OAAO6L,GAAKA,IAAM5Q,GACtDtF,KAAKqQ,eAAiBrQ,KAAKqQ,eAAehG,OAAOiI,GAAKA,EAAEhN,MAAQA,GAG5DtF,KAAK6P,gBAAkBvK,IACvBtF,KAAK6P,cAAgB7P,KAAKuQ,YAAY,IAAM,MAG5CvQ,KAAKiT,mBACCjT,KAAKyL,SAGfzL,KAAKwT,KAAK,kBAAmB,CAAElO,SACxB,IAxBHyN,QAAQC,KAAK,yBAAyB1N,sBAC/B,EAwBf,CASA,QAAA6Q,CAAS7Q,EAAK5F,GACV,MAAMyL,EAASnL,KAAKqQ,eAAerF,KAAKsH,GAAKA,EAAEhN,MAAQA,GACvD,QAAK6F,IAELA,EAAO0G,MAAQnS,GAEVM,KAAKiH,UAGIjH,KAAKiH,QAAQqN,iBAAiB,kBAAkBhP,OACxDD,QAAQkP,IACV,MAAM6B,EAAW7B,EAAKpN,cAAc,cAC9BkP,EAAOrW,KAAKgS,aAAa7G,GAC3BiL,KAAmBjC,SACnBkC,GAAM9B,EAAK+B,mBAAmB,YAAaD,MAE5C,GACX,CAMA,cAAAE,GAEA,CAEA,aAAOvK,CAAOlO,EAAU,IACpB,OAAO,IAAI4R,YAAY5R,EAC3B,EC5hBJ,MAAM0Y,yBAAyB7G,EAC3B,WAAApS,CAAYO,EAAU,IAClB,MAAMqU,KACFA,EAAAsE,SACAA,EAAAC,WACAA,EAAAC,SACAA,EAAAC,WACAA,EAAAC,QACAA,EAAAC,WACAA,EAAAC,aACAA,EAAAC,WACAA,EAAAC,oBACAA,EAAAC,mBACAA,EAAAC,MACAA,EAAQ,GAAAC,YACRA,EAAAC,MACAA,EAAA/W,QACAA,EAAU,GAAAgX,SACVA,GAAW,EAAA/W,YACXA,KACG2P,GACHpS,EAEJL,MAAM,CACF0S,QAAS,MACTC,UAAW,gBAGXmH,gBAAgB,KACbrH,IAGPlQ,KAAKmS,KAAOA,GAAQ,kBACpBnS,KAAKyW,SAAWA,GAAY,KAC5BzW,KAAK0W,WAAaA,GAAc,KAChC1W,KAAK2W,SAAWA,GAAY,KAC5B3W,KAAK4W,WAAaA,GAAc,KAChC5W,KAAK6W,QAAUA,GAAW,KAC1B7W,KAAK8W,WAAaA,GAAc,KAChC9W,KAAK+W,aAAeA,GAAgB,KACpC/W,KAAKgX,WAAaA,GAAc,KAChChX,KAAKiX,oBAAsBA,GAAuB,KAClDjX,KAAKkX,mBAAqBA,GAAsB,KAChDlX,KAAKmX,MAAQA,EACbnX,KAAKoX,YAAcA,GAAe,KAClCpX,KAAKqX,MAAyB,mBAAVA,EAAuBA,EAAQ,KACnDrX,KAAKM,QAAUA,EACfN,KAAKsX,SAAWA,EAChBtX,KAAKwX,kBAAoBjX,GAAe,KAExCP,KAAKwD,SAAW,IAAMxD,KAAKyX,gBAC/B,CAEA,aAAAC,GACI,OAAI1X,KAAK6W,QAAgB7W,KAAK6W,QAAQ7W,KAAKd,QAAU,GACjDc,KAAK4W,YAAmB5W,KAAKd,OAAOiN,MAAMnM,KAAK4W,aAC5C,EACX,CAEA,kBAAAe,GACI,MAA+B,mBAApB3X,KAAK8W,WAAkC9W,KAAK8W,WAAW9W,KAAKd,QAAU,GAC1Ec,KAAK8W,YAAc,EAC9B,CAEA,gBAAAc,GACI,OAAI5X,KAAKgX,WAAmBhX,KAAKgX,WAAWhX,KAAKd,QAAU,GACtDc,KAAK+W,cACHc,EAAUC,eAAe9X,KAAKd,OAAO6Y,YAAc,GAAI/X,KAAK+W,eADpC,EAEnC,CAEA,gBAAAiB,GACI,OAAIhY,KAAK0W,WAAmB1W,KAAK0W,WAAW1W,KAAKd,QAAU,KACpDc,KAAKyW,QAChB,CAEA,gBAAAwB,GACI,MAA6B,mBAAlBjY,KAAK2W,SAAgC3W,KAAK2W,SAAS3W,KAAKd,QAAU,GACtEc,KAAK2W,UAAY,EAC5B,CAEA,aAAAuB,GACI,OAAQlY,KAAKmX,OAAS,IACjB9M,WAAgB8N,EAAKC,MAAQD,EAAKC,KAAKpY,KAAKd,QAC5CgL,IAAIiO,IACD,IAAIrG,IACqB,mBAAdqG,EAAKrG,KAA4BqG,EAAKrG,KAAK9R,KAAKd,OAClDiZ,EAAKE,SAAiBrY,KAAKd,OAAOiN,MAAMgM,EAAKE,UAC1CF,EAAKrG,KAEjB,IAAIwG,EAAU,GAId,MAH4B,mBAAjBH,EAAKG,UAAkCH,EAAKG,QAAQtY,KAAKd,QAAU,GACrEiZ,EAAKG,UAASA,EAAUxR,OAAOqR,EAAKG,UAEtC,CACHnG,KAAMgG,EAAKhG,MAAQ,KACnBL,KAAc,MAARA,EAAehL,OAAOgL,GAAQ,GACpCC,QAASoG,EAAKpG,SAAW,QACzBuG,UAKAC,OAAQJ,EAAKI,QAAU,QAG9BlO,OAAOiI,GAAgB,KAAXA,EAAER,KACvB,CAEA,cAAA2F,GACI,MAAMvZ,EAAQ8B,KAAK0X,gBACbc,EAAWxY,KAAK4X,mBAChBT,EAAQnX,KAAKkY,gBACbhG,EAAWlS,KAAKoX,cAAgBpX,KAAKd,OAAOiN,MAAMnM,KAAKoX,aAAe,KAEtEqB,EAAYtB,EAAMjN,IAAIoI,IACxB,MAAMqE,EAAWrE,EAAEH,KAAO,gBAAgBnS,KAAKiS,WAAWK,EAAEH,mBAAqB,GAC3EuG,EAAepG,EAAEgG,QACjB,oCAAoCtY,KAAKiS,WAAWK,EAAEgG,YACtD,GAIN,OAAIhG,EAAEiG,OACK,yCAAyCvY,KAAKiS,WAAWK,EAAEP,kDAAkD/R,KAAKiS,WAAWK,EAAEiG,mCAAmCG,KAAgB/B,IAAW3W,KAAKiS,WAAWK,EAAER,iBAEnN,yBAAyB9R,KAAKiS,WAAWK,EAAEP,YAAY2G,KAAgB/B,IAAW3W,KAAKiS,WAAWK,EAAER,iBAC5GM,KAAK,IAGFuG,EAAY3Y,KAAKqX,OAASrX,KAAKqX,MAAMrX,KAAKd,QAAgB,GAC1D0Z,EAAUD,EAAY,uBAAuBA,UAAoB,GAEjEE,EAAa7Y,KAAKoX,YAAc,6FAC4CpX,KAAKiS,WAAWjS,KAAKoX,sFAC1ClF,EAAW,UAAY,qGAE7CA,EAAW,SAAW,oDAEzD,GAEE4G,GAAe9Y,KAAKM,SAAW,IAAI4J,IAAI6O,IACzC,MAAMpC,EAAWoC,EAAE5G,KAAO,gBAAgBnS,KAAKiS,WAAW8G,EAAE5G,cAAgB,GAC5E,MAAO,0CAA0CnS,KAAKiS,WAAW8G,EAAER,4CAA4CvY,KAAKiS,WAAW8G,EAAE7a,OAAS6a,EAAEza,WAAWqY,KAAY3W,KAAKiS,WAAW8G,EAAEza,oBACtL8T,KAAK,IAEF4G,EAAYhZ,KAAKsX,SAAW,mNAI9B,GAEE2B,EAAkBjZ,KAAKwX,kBAAoB,mDAAqD,GAGhG0B,IAAclZ,KAAKwX,mBAAqBxX,KAAKsX,SAE7C6B,EAAOnZ,KAAKgY,mBACZoB,EAAepZ,KAAKiY,mBAMpBoB,EAAYD,EACZ,wBACCD,EAAO,wBAAwBnZ,KAAKiS,WAAWkH,KAAU,UAC1DG,EAAWF,GAAgB,gBAAgBpZ,KAAKiS,WAAWjS,KAAKmS,cAKhEoH,EAAiBvZ,KAAK2X,qBAG5B,MAAO,2FAEe0B,MAAcC,6KAGEtZ,KAAKiS,WAAW/T,oCACpCqb,EAAiB,gCAAgCA,WAA0B,uDAE/Ef,EACI,0BAA0BxY,KAAKiS,WAAWuG,SACzCxY,KAAKiX,oBACF,2CAA2CjX,KAAKkX,mBAAqB,iBAAiBlX,KAAKiS,WAAWjS,KAAKkX,qCAAuC,MAAMlX,KAAKiS,WAAWjS,KAAKiX,2BAC7K,2BAERE,EAAMrS,OAAS,yBAAyB2T,UAAoB,sOAKhEG,uGAEAC,sBACAC,sBACAI,EAAY,kLAGRD,0BACAD,sBACF,kCAGhB,CAEA,mBAAMxG,GAGF,SAFM/U,MAAM+U,gBAERxS,KAAKwX,oBAAsBxX,KAAKwZ,oBAAqB,CACrD,MAAMC,EAAK,IAAIC,EAAY,CACvB5V,YAAa,sBACb6V,QAAS3Z,KAAKd,MACdiM,OAAQ,CACJgH,KAAM,4BACHnS,KAAKwX,qBAGhBxX,KAAKmE,SAASsV,SACRA,EAAGhO,SACTzL,KAAKwZ,qBAAsB,CAC/B,CACJ,CAiBA,qBAAMI,CAAgBrB,EAAQtU,EAAO6Q,GACjC,MAAM+E,EAAO7Z,KAAKkR,OAClB,SAAK2I,IAASA,EAAKC,QAA0C,mBAAzBD,EAAKC,OAAOC,iBAGnCF,EAAKC,OAAOC,SAASxB,EAAQtU,EAAO6Q,EACrD,CAeA,0BAAMkF,CAAqB/V,EAAOgD,GAC9B,IAAKjH,KAAKoX,YAAa,OACvB,MAAM6C,IAAYhT,EAAQgT,QAC1BhT,EAAQiT,UAAW,EACnB,IACIla,KAAKd,MAAMib,IAAIna,KAAKoX,YAAa6C,GACjC,MAAM9a,QAAaa,KAAKd,MAAMiL,KAAK,CAAE,CAACnK,KAAKoX,aAAc6C,IACzD,GAAI9a,GAAQA,EAAKoL,QAAUpL,EAAKoL,QAAU,IACtC,MAAM,IAAI6P,MAAM,eAEpBpa,KAAKwT,KAAK,iBACd,OAAS6G,GAELra,KAAKd,MAAMib,IAAIna,KAAKoX,aAAc6C,EACtC,CAAA,QAGQhT,GAAWA,EAAQqT,cAAarT,EAAQiT,UAAW,EAC3D,CACJ,EAMJ,MAAMK,mBAAmB5K,EACrB,WAAApS,CAAYO,EAAU,IAClB,MAAMkL,OACFA,EAAS,CAAA,EAAA4G,SACTA,EAAW,GAAAC,cACXA,EAAAC,SACAA,EAAAG,SACAA,EAAAF,eACAA,KACGG,GACHpS,EAKE0c,EAAoBtK,EAAYE,WAAa,GAC7CqK,EAAkBD,EAAkBjV,SAAS,eAC7CiV,EACCA,EAAoB,eAAeA,IAAsB,cAEhE/c,MAAM,IACCyS,EACHE,UAAWqK,KAGVza,KAAKd,OAASpB,EAAQoB,QACvBc,KAAKd,MAAQpB,EAAQoB,OAGzBc,KAAK0a,aAAe1R,EACpBhJ,KAAK2a,eAAiB/K,EACtB5P,KAAK4a,qBAAuB/K,EAC5B7P,KAAK8P,SAAWA,GAAY,IAC5B9P,KAAKiQ,SAAWA,GAAY,IAK5BjQ,KAAK+P,eAAiBA,GAAkB,SAExC/P,KAAKwD,SAAW,+NAMpB,CAEA,YAAME,SACI1D,KAAK6a,gBAEX7a,KAAK8a,WAAa,IAAItE,iBAAiB,CACnC1S,YAAa,gBACb5E,MAAOc,KAAKd,SACTc,KAAK0a,eAGZ1a,KAAK8a,WAAWzW,GAAG,iBAAkB,IAAMrE,KAAKwT,KAAK,mBACrDxT,KAAKmE,SAASnE,KAAK8a,YAEnB9a,KAAK+a,QAAU,IAAIrL,YAAY,CAC3B5L,YAAa,iBACb8L,SAAU5P,KAAK2a,eACf9K,cAAe7P,KAAK4a,qBACpB9K,SAAU9P,KAAK8P,SACfG,SAAUjQ,KAAKiQ,SACfF,eAAgB/P,KAAK+P,eACrBC,kBAAkB,IAEtBhQ,KAAKmE,SAASnE,KAAK+a,eAEb/a,KAAKgb,cACf,CAMA,mBAAMH,GAAiB,CAMvB,kBAAMG,GAAgB,CAGtB,QAAA7E,CAAS7Q,EAAK5F,GACV,OAAOM,KAAK+a,SAAS5E,SAAS7Q,EAAK5F,EACvC,CAGA,WAAAoT,CAAYxN,GACR,OAAOtF,KAAK+a,SAASjI,YAAYxN,EACrC,CAQA,cAAAiR,GAEQvW,KAAK8a,YAAY7H,aACjBjT,KAAK8a,WAAWrP,SAASwP,MAAM,OAEvC,EC9bJ,MAAMC,iBAAc,IAAInM,IAAI,CAAC,UAAW,UAAW,UAAW,SAAU,OAAQ,YAEhF,MAAMoM,mBAAmBxL,EACrB,WAAApS,CAAYO,EAAU,IAClB,MAAMQ,MACFA,EAAAoB,MACAA,EAAAyS,KACAA,EAAO,KAAAiJ,UACPA,EAAY,KAAAjC,KACZA,EAAO,UAAAkC,KACPA,EAAO,KAAA9C,OACPA,EAAS,QACNrI,GACHpS,EAEJL,MAAM,CACF0S,QAASoI,EAAS,SAAW,MAC7BnI,UAAW,iBACRF,IAGPlQ,KAAK1B,MAAQA,GAAS,GACtB0B,KAAKN,MAAQA,EACbM,KAAKmS,KAAOA,EACZnS,KAAKob,UAAYA,EACjBpb,KAAKmZ,KAAO+B,EAAY3L,IAAI4J,GAAQA,EAAO,UAC3CnZ,KAAKqb,KAAOA,EACZrb,KAAKuY,OAASA,EAEdvY,KAAKwD,SAAW,IAAMxD,KAAKyX,gBAC/B,CAEA,cAAAA,GACI,MAAMnZ,EAAQ0B,KAAKsb,SAAStb,KAAK1B,MAAO,IAClC+c,EAAQrb,KAAKsb,SAAStb,KAAKqb,KAAM,MACjCE,EAAYvb,KAAKwb,eACjB7E,EAAW3W,KAAKmS,KAChB,gBAAgBnS,KAAKiS,WAAWjS,KAAKmS,+BACrC,GACAsJ,EAAgBzb,KAAKob,UACrB,gBAAgBpb,KAAKiS,WAAWjS,KAAKob,0CACrC,GACAM,EAAWL,EACX,iCAAiCrb,KAAKiS,WAAWnL,OAAOuU,YACxD,GAQN,MAAO,gDAC8B1E,UAAiB3W,KAAKiS,WAAWnL,OAAOxI,gEACxCmd,UAAsBF,+BACrDG,cACJrM,MACN,CAEA,QAAAiM,CAASK,EAAKpN,EAAW,IACrB,GAAmB,mBAARoN,EACP,IAAM,OAAOA,EAAI3b,KAAKd,QAAUqP,CAAU,OAASqN,GAAK,OAAOrN,CAAU,CAE7E,OAAOoN,GAAOpN,CAClB,CAEA,YAAAiN,GAKI,IAAIK,EAAI7b,KAAKN,MACb,GAAiB,mBAANmc,EACP,IAAMA,EAAIA,EAAE7b,KAAKd,MAAQ,OAAS0c,GAAKC,EAAI,IAAM,CAErD,OAAIA,QAAsC,oCACzB,iBAANA,GAAwB,OAANA,GAAc,SAAUA,EAC1C7b,KAAKiS,WAAWnL,OAAO+U,EAAE/J,OAE7B9R,KAAKiS,WAAWnL,OAAO+U,GAClC,CAEA,mBAAMrJ,GAEF,SADM/U,MAAM+U,iBACPxS,KAAKiH,QAAS,OAGnB,MAAMkS,EAAOnZ,KAAKsb,SAAStb,KAAKmZ,KAAM,WAChC2C,EAAWZ,EAAY3L,IAAI4J,GAAQA,EAAO,UAChD+B,EAAY7V,WAAarF,KAAKiH,QAAQyN,UAAUP,OAAO,oBAAoB4H,MACvED,GAAyB,YAAbA,GACZ9b,KAAKiH,QAAQyN,UAAUsH,IAAI,oBAAoBF,KAG/C9b,KAAKuY,SACLvY,KAAKiH,QAAQgV,aAAa,cAAejc,KAAKuY,QACjB,WAAzBvY,KAAKiH,QAAQkJ,SAAyBnQ,KAAKiH,QAAQiV,aAAa,SAChElc,KAAKiH,QAAQgV,aAAa,OAAQ,UAG9C,CAMA,QAAAE,CAASzc,GAEL,GADAM,KAAKN,MAAQA,GACRM,KAAKiH,QAAS,OAEnB,MAAMmV,EAAUpc,KAAKiH,QAAQE,cAAc,sBAC3C,IAAKiV,EAAS,OACd,MAAMC,EAAOD,EAAQjV,cAAc,iBAC/BkV,EACAA,EAAKtI,UAAY/T,KAAKwb,eAEtBY,EAAQrI,UAAY,SAAS/T,KAAKwb,uBAE1C,CAMA,OAAAc,CAAQjB,GAEJ,GADArb,KAAKqb,KAAOA,GACPrb,KAAKiH,QAAS,OACnB,IAAIsV,EAAOvc,KAAKiH,QAAQE,cAAc,qBAClCkU,EACIkB,EACAA,EAAKnV,YAAcN,OAAOuU,IAE1BkB,EAAO1I,SAASC,cAAc,OAC9ByI,EAAKnM,UAAY,mBACjBmM,EAAKnV,YAAcN,OAAOuU,GAC1Brb,KAAKiH,QAAQuV,YAAYD,IAEtBA,GACPA,EAAKpI,QAEb,ECnHJ,MAAM+G,iBAAc,IAAInM,IAAI,CAAC,UAAW,UAAW,UAAW,OAAQ,UAAW,SAAU,cAE3F,MAAM0N,oBAAoB9M,EACtB,WAAApS,CAAYO,EAAU,IAClB,MAAMqb,KACFA,EAAW,UAAAuD,MACXA,EAAW,GAAAC,SACXA,EAAW,GAAApV,KACXA,EAAW,GAAA4K,KACXA,EAAW,KAAA7R,QACXA,EAAW,MACR4P,GACHpS,EAEJL,MAAM,CACF0S,QAAS,MACTC,UAAW,yBACRF,IAGPlQ,KAAK4c,SAAezD,EACpBnZ,KAAK6c,UAAeH,EACpB1c,KAAK8c,aAAeH,EACpB3c,KAAK+c,SAAexV,EACpBvH,KAAKgd,SAAe7K,EACpBnS,KAAKid,YAAe3c,EAEpBN,KAAKwD,SAAW,IAAMxD,KAAKyX,gBAC/B,CAIA,QAAA6D,CAASK,EAAKpN,EAAW,IACrB,MAAmB,mBAARoN,EAA2BA,EAAI3b,KAAKd,QAAUqP,EAClDoN,GAAOpN,CAClB,CAEA,YAAA2O,GACI,MAAMnB,EAAI/b,KAAKsb,SAAStb,KAAK4c,SAAU,WACvC,OAAO1B,EAAY3L,IAAIwM,GAAKA,EAAI,SACpC,CAEA,eAAAoB,GACI,MAAMC,EAAMpd,KAAKsb,SAAStb,KAAKid,YAAa,KAAO,GACnD,OAAOI,MAAMC,QAAQF,GAAOA,EAAI/S,OAAOmF,SAAW,EACtD,CAIA,cAAAiI,GACqBzX,KAAKkd,eACtB,MAAMR,EAAW5V,OAAO9G,KAAKsb,SAAStb,KAAK6c,UAAW,KAChDF,EAAW7V,OAAO9G,KAAKsb,SAAStb,KAAK8c,aAAc,KACnDvV,EAAWvH,KAAKsb,SAAStb,KAAK+c,SAAU,IACxC5K,EAAWnS,KAAKsb,SAAStb,KAAKgd,SAAU,MACxC1c,EAAWN,KAAKmd,kBAEhBxG,EAAWxE,EACX,gBAAgBnS,KAAKiS,WAAWnL,OAAOqL,+BACvC,0CAEA2G,EAAcxY,EAAQ4J,IAAI6O,IAC5B,MAAMhH,EAAUgH,EAAEhH,SAAW,UACvBwL,GAAWxL,EAAQvM,WAAW,YAC9B,OAAOuM,KAEPyL,EAAQzE,EAAE5G,KAAO,gBAAgBnS,KAAKiS,WAAW8G,EAAE5G,mBAAqB,GAC9E,MAAO,sBAAsBoL,0BAAiCvd,KAAKiS,WAAW8G,EAAER,QAAU,sBAAsBiF,IAAQxd,KAAKiS,WAAW8G,EAAEza,OAAS,iBACpJ8T,KAAK,IAER,MAAO,uEAEGsK,EAAQ,oCAAoC/F,IAAW3W,KAAKiS,WAAWyK,WAAiB,uBACxFC,EAAW,mCAAmC3c,KAAKiS,WAAW0K,WAAoB,uBAClFpV,EAAO,mCAAmCA,UAAe,uCAE7DjH,EAAQwE,OAAS,sCAAsCgU,UAAsB,eACjFzJ,MACN,CAEA,mBAAMmD,GAEF,SADM/U,MAAM+U,iBACPxS,KAAKiH,QAAS,OAInBiU,EAAY7V,WAAarF,KAAKiH,QAAQyN,UAAUP,OAAO,QAAQ4H,MAC/D,MAAM5C,EAAOnZ,KAAKkd,eACd/D,GAAiB,YAATA,GACRnZ,KAAKiH,QAAQyN,UAAUsH,IAAI,QAAQ7C,IAE3C,EC/FJ,MAAM+B,iBAAc,IAAInM,IAAI,CAAC,UAAW,UAAW,UAAW,OAAQ,UAAW,SAAU,cAE3F,MAAM0O,iBAAiB9N,EACnB,WAAApS,CAAYO,EAAU,IAClB,MAAMgM,MACFA,EAAY,GAAA4T,UACZA,EAAY,iBAAAC,MACZA,EAAY,QACTzN,GACHpS,EAEJL,MAAM,CACF0S,QAAS,KACTC,UAAW,qBACRF,IAGPlQ,KAAK4d,UAAY9T,EACjB9J,KAAK0d,UAAYA,EACjB1d,KAAK2d,MAA0B,iBAAVA,GAAsBA,EAAQ,EAAKpI,KAAKsI,MAAMF,GAAS,KAE5E3d,KAAKwD,SAAW,IAAMxD,KAAKyX,gBAC/B,CAIA,aAAAqG,GACI,MAAMxR,EAAiC,mBAAnBtM,KAAK4d,UACnB5d,KAAK4d,UAAU5d,KAAKd,OACpBc,KAAK4d,UACX,IAAKP,MAAMC,QAAQhR,SAAa,GAChC,MAAMyR,EAAWzR,EAAIjC,OAAOmF,SAC5B,OAAOxP,KAAK2d,MAAQI,EAASC,MAAM,EAAGhe,KAAK2d,OAASI,CACxD,CAEA,cAAAE,CAAe9E,GACX,OAAO+B,EAAY3L,IAAI4J,GAAQA,EAAO,IAC1C,CAIA,cAAA1B,GACI,MAAM3N,EAAQ9J,KAAK8d,gBAEnB,OAAKhU,EAAMhF,OAMJgF,EAAMI,IAAIgU,IACb,MAAM/E,EAAWnZ,KAAKie,eAAeC,EAAG/E,MAClCgF,EAAWhF,EAAO,SAASA,IAAS,GACpCwD,EAAW7V,OAAOoX,EAAGvB,UAAYuB,EAAG5f,OAAS,IAC7C8f,EAAwB,MAAbF,EAAGE,OAAiBtX,OAAOoX,EAAGE,QAAU,GACnDhG,EAAsB,MAAX8F,EAAG9F,KAAetR,OAAOoX,EAAG9F,MAAQ,GAErD,MAAO,oDAC8B+F,iGAEene,KAAKiS,WAAW0K,qCACtDyB,EAAS,uCAAuCA,UAAiB,uDAErEhG,EAAO,sCAAsCpY,KAAKiS,WAAWmG,YAAiB,4CAGzFhG,KAAK,IAnBG,0DAA0DpS,KAAKiS,WAAWjS,KAAK0d,iBAoB9F,CAMA,QAAAW,CAASvU,GAEL,GADA9J,KAAK4d,UAAY9T,GAAS,GACtB9J,KAAKiH,QACL,OAAOjH,KAAKyL,QAEpB,ECpEJ,MAAM6S,wBAAwB3O,EAC1B,WAAApS,CAAYO,EAAU,IAClB,MAAMN,KACFA,EAAc,CAAA,EAAA+gB,UACdA,EAAc,GAAAC,aACdA,GAAe,EAAAC,SACfA,EAAc,WAAAf,UACdA,EAAc,WAAAgB,QACdA,GAAc,KACXxO,GACHpS,EAEJL,MAAM,CACF0S,QAAS,MACTC,UAAW,8BACRF,IAGPlQ,KAAK2e,SAAkBnhB,EACvBwC,KAAK4e,cAAkBL,EACvBve,KAAKwe,cAAmC,IAAjBA,EACvBxe,KAAKye,SAAkBA,EACvBze,KAAK0d,UAAkBA,EACvB1d,KAAK0e,SAA8B,IAAZA,EAEvB1e,KAAKwD,SAAW,IAAMxD,KAAKyX,gBAC/B,CAIA,YAAAoH,GACI,MAAMvS,EAAgC,mBAAlBtM,KAAK2e,SACnB3e,KAAK2e,SAAS3e,KAAKd,OACnBc,KAAK2e,SACX,OAAQrS,GAAsB,iBAARA,EAAoBA,EAAM,CAAA,CACpD,CAEA,iBAAAwS,GACI,MAAMxS,EAAqC,mBAAvBtM,KAAK4e,cACnB5e,KAAK4e,cAAc5e,KAAKd,OACxBc,KAAK4e,cACX,OAAOvB,MAAMC,QAAQhR,GAAOA,EAAIjC,OAAOmF,SAAW,EACtD,CAIA,cAAAiI,GACI,MAAMja,EAAawC,KAAK6e,eAClBN,EAAave,KAAK8e,oBAGxB,IAFoBP,EAAUzZ,QAAuC,IAA7BF,OAAOC,KAAKrH,GAAMsH,OAGtD,MAAO,qCAAqC9E,KAAKiS,WAAWjS,KAAK0d,mBAGrE,MAAMqB,EAAWR,EACZrU,IAAI8U,GAAQhf,KAAKif,WAAWD,EAAMxhB,IAClC6M,OAAOmF,SACP4C,KAAK,IAUV,MAAO,GARW2M,EACZ,yCAAyCA,UACzC,KAEU/e,KAAK0e,QACf1e,KAAKkf,WAAW1hB,GAChB,IAGV,CAEA,UAAAyhB,CAAWD,EAAMxhB,GACb,IAAKwhB,IAASA,EAAK1Z,IAAK,MAAO,GAE/B,MAAM5F,EAAQM,KAAKmf,QAAQ3hB,EAAMwhB,EAAK1Z,KAChC8Z,EAAqB,MAAT1f,GAA2B,KAAVA,EAEnC,GAAI0f,GAAaJ,EAAKK,UAAW,MAAO,GAExC,MAAM/gB,EAAQwI,OAAOkY,EAAK1gB,OAAS0gB,EAAK1Z,KAClCga,EAAYF,EACZ,mDACApf,KAAKuf,aAAa7f,EAAOsf,EAAMxhB,GAErC,MAAO,mGAEsCwC,KAAKiS,WAAW3T,gEAChBghB,uCAGjD,CAEA,UAAAJ,CAAW1hB,GAEP,IAAKA,GAAqC,IAA7BoH,OAAOC,KAAKrH,GAAMsH,OAAc,MAAO,GAEpD,IAAI0a,EACJ,IACIA,EAAO/Z,KAAKkB,UAAUnJ,EAAM,KAAM,EACtC,OAAS6c,GACLmF,EAAO1Y,OAAOtJ,EAClB,CAGA,MAAO,yDADUwC,KAAKwe,aAAe,GAAK,8EAGiBxe,KAAKiS,WAAWjS,KAAKye,kFAC5Bze,KAAKiS,WAAWuN,4CAGxE,CAKA,OAAAL,CAAQ3hB,EAAM8H,GACV,IAAKA,GAAe,MAAR9H,EAAc,OAC1B,GAAIoH,OAAO4J,UAAU1I,eAAe2I,KAAKjR,EAAM8H,GAAM,OAAO9H,EAAK8H,GACjE,QAAIA,EAAIma,QAAQ,KAAa,OAC7B,MAAMjS,EAAQlI,EAAImI,MAAM,KACxB,IAAIiS,EAASliB,EACb,IAAA,MAAWmiB,KAAQnS,EAAO,CACtB,GAAc,MAAVkS,GAAoC,iBAAXA,EAAqB,OAClDA,EAASA,EAAOC,EACpB,CACA,OAAOD,CACX,CAEA,YAAAH,CAAa7f,EAAOsf,EAAMxhB,GACtB,MAAMoiB,EAAYZ,EAAKY,UAEvB,GAAyB,mBAAdA,EACP,IACI,MAAMC,EAAMD,EAAUlgB,EAAOsf,EAAK1Z,IAAK9H,GACvC,OAAc,MAAPqiB,EAAc,GAAK/Y,OAAO+Y,EACrC,OAASxF,GACL,OAAOra,KAAKiS,WAAWnL,OAAOpH,GAClC,CAGJ,GAAyB,iBAAdkgB,GAA0BA,EAAU9a,OAC3C,IACI,MAAM+a,EAAMrT,EAAcsT,MAAMF,EAAWlgB,GAC3C,OAAc,MAAPmgB,EAAc,GAAK/Y,OAAO+Y,EACrC,OAASxF,GACL,OAAOra,KAAKiS,WAAWnL,OAAOpH,GAClC,CAIJ,OAAIA,GAA0B,iBAAVA,EACT,gCAAgCM,KAAKiS,WAAWxM,KAAKkB,UAAUjH,aAEnEM,KAAKiS,WAAWnL,OAAOpH,GAClC,EChNJ,MAAMqgB,wBAAwBpQ,EAC1B,WAAApS,CAAYO,EAAU,IAClBL,MAAM,CACF2S,UAAW,kBACRtS,IAEPkC,KAAKggB,KAAOliB,EAAQkiB,MAAQ,CAAA,EAC5BhgB,KAAKigB,QAAUjgB,KAAKggB,KAAKE,cAAc1a,WAAW,UAClDxF,KAAKmgB,MAAmC,oBAA3BngB,KAAKggB,KAAKE,YAC3B,CAEA,WAAAE,GACI,MAAO,4MAIWpgB,KAAKigB,QAAU,aAAajgB,KAAKggB,KAAKK,cAAgBrgB,KAAKggB,KAAKM,8EAAgF,ykBAYtK,CAEA,sBAAMC,GACF,GAAIvgB,KAAKigB,QAAS,CAEd,MAAMO,EAAkBna,OAAOoa,MAAMC,SAASF,gBAE1CA,EACAA,EAAgBG,KAAK,CAAEC,IAAK5gB,KAAKggB,KAAKM,IAAKO,IAAK7gB,KAAKggB,KAAKc,WAG1Dza,OAAO0a,KAAK/gB,KAAKggB,KAAKM,IAAK,SAEnC,MAAA,GAAWtgB,KAAKmgB,MAAO,CAEnB,MAAMa,EAAY3a,OAAOoa,MAAMC,SAASM,UAEpCA,EACAA,EAAUC,WAAWjhB,KAAKggB,KAAKM,IAAK,CAAEpiB,MAAO8B,KAAKggB,KAAKc,WAGvDza,OAAO0a,KAAK/gB,KAAKggB,KAAKM,IAAK,SAEnC,MACIja,OAAO0a,KAAK/gB,KAAKggB,KAAKM,IAAK,SAEnC,ECjDC,MAACY,EAA2B,CAC7B,CAAExhB,MAAO,QAASpB,MAAO,SACzB,CAAEoB,MAAO,QAASpB,MAAO,SACzB,CAAEoB,MAAO,MAAOpB,MAAO,OACvB,CAAEoB,MAAO,OAAQpB,MAAO,QACxB,CAAEoB,MAAO,UAAWpB,MAAO,gBAC3B,CAAEoB,MAAO,MAAOpB,MAAO,OACvB,CAAEoB,MAAO,QAASpB,MAAO,UAGvB6iB,EAAuB,CACzB,CAAEzhB,MAAO,GAAIpB,MAAO,YACpB,CAAEoB,MAAO,UAAWpB,MAAO,WAC3B,CAAEoB,MAAO,sBAAuBpB,MAAO,wBAW3C,SAAS8iB,EAAyBC,EAAW,IACzC,MAAMC,EAAID,GAAY,CAAA,EACtB,MAAO,CACHE,SAAqBD,EAAE,aAAuB,GAC9CE,eAAqBF,EAAE,mBAAuB,GAC9CG,SAAqBH,EAAE,aAAuB,GAC9CI,aAAqBJ,EAAE,iBAAuB,GAC9CK,cAAqBL,EAAE,kBAAuB,GAC9CM,oBAAqBN,EAAE,wBAA0B,GACjDO,cAAqBP,EAAE,kBAAuB,GAEtD,CAMA,SAASQ,EAAuBC,EAAW,IACvC,MAAM7X,EAAM,CACRqX,SAAU,WACVC,eAAgB,iBAChBC,SAAU,WACVC,aAAc,eACdC,cAAe,gBACfC,oBAAqB,sBACrBC,cAAe,iBAEbR,EAAW,CAAA,EACjB,IAAA,MAAYW,EAASC,KAAcrd,OAAOQ,QAAQ8E,GAAM,CACpD,MAAM2R,EAAIkG,EAASC,GACfnG,SAAuC,KAANA,IACjCwF,EAASY,GAAapG,EAE9B,CACA,OAAOwF,CACX,CAQA,SAASa,EAAwBH,EAAW,IACxC,MAAMI,EAAW,CACb,WAAY,iBAAkB,WAC9B,eAAgB,gBAAiB,sBAAuB,iBAEtDC,EAAU,IAAKL,GACrB,IAAA,MAAW7L,KAAKiM,SAAiBC,EAAQlM,GACzC,MAAMmL,EAAWS,EAAuBC,GAIxC,OAHInd,OAAOC,KAAKwc,GAAUvc,OAAS,IAC/Bsd,EAAQf,SAAWA,GAEhBe,CACX,CAMA,MAAMC,kBAAkB/kB,EACpB,WAAAC,CAAYC,EAAO,IACfC,MAAMD,EAAM,CACRE,SAAU,uBAElB,EAOJ,MAAM4kB,sBAAsBzkB,EACxB,WAAAN,CAAYO,EAAU,IAClBL,MAAM,CACFM,WAAYskB,UACZ3kB,SAAU,yBACPI,GAEX,EAOJ,MAAMykB,uBAAuBjlB,EACzB,WAAAC,CAAYC,EAAO,IACfC,MAAMD,EAAM,CACRE,SAAU,0BAElB,EAGJ,MAAM8kB,2BAA2B3kB,EAC7B,WAAAN,CAAYO,EAAU,IAClBL,MAAM,CACFM,WAAYwkB,eACZ7kB,SAAU,4BACPI,GAEX,EAOJ,MAAM2kB,EAAyB,CAC3B,CAAErkB,KAAM,MAAOC,KAAM,MAAOC,MAAO,kBAAmBK,UAAU,EAAM+jB,YAAa,2BAA4B7jB,KAAM,IACrH,CAAET,KAAM,SAAUC,KAAM,SAAUC,MAAO,SAAUR,QAASojB,EAA0BxhB,MAAO,QAASb,KAAM,GAC5G,CAAET,KAAM,cAAeC,KAAM,SAAUC,MAAO,gBAAiBoB,MAAO,EAAGijB,IAAK,EAAG9jB,KAAM,EAAGL,KAAM,aAChG,CAAEJ,KAAM,eAAgBC,KAAM,SAAUC,MAAO,iBAAkBoB,MAAO,EAAGijB,IAAK,EAAG9jB,KAAM,GACzF,CAAET,KAAM,eAAgBC,KAAM,SAAUC,MAAO,eAAgBoB,OAAO,EAAOb,KAAM,EAAGL,KAAM,kDAC5F,CAAEJ,KAAM,kBAAmBC,KAAM,SAAUC,MAAO,qBAAsBoB,OAAO,EAAOb,KAAM,EAAGL,KAAM,gEACrG,CAAEJ,KAAM,eAAgBC,KAAM,SAAUC,MAAO,YAAaoB,OAAO,EAAOb,KAAM,EAAGL,KAAM,iCAEzF,CAAEH,KAAM,UAAWC,MAAO,+BAAgCO,KAAM,IAChE,CAAET,KAAM,WAAYC,KAAM,OAAQC,MAAO,WAAYokB,YAAa,kCAAmC7jB,KAAM,IAC3G,CAAET,KAAM,iBAAkBC,KAAM,WAAYC,MAAO,iBAAkBskB,KAAM,EAAG/jB,KAAM,IACpF,CAAET,KAAM,WAAYC,KAAM,MAAOC,MAAO,WAAYokB,YAAa,kCAAmC7jB,KAAM,KAGxGgkB,EAAiB,CACnB7W,OAAQ,CACJ9N,MAAO,mBACPQ,KAAM,KACNP,OAAQ,IACDskB,GAEPjkB,KAAM,iFAGVP,KAAM,CACFC,MAAO,iBACPQ,KAAM,KACNP,OAAQ,CACJ,CAAEC,KAAM,YAAaC,KAAM,SAAUC,MAAO,SAAUO,KAAM,MACzD4jB,EACH,CAAErkB,KAAM,eAAgBC,KAAM,SAAUC,MAAO,eAAgBR,QAASqjB,EAAsBtiB,KAAM,GACpG,CAAET,KAAM,gBAAiBC,KAAM,OAAQC,MAAO,gBAAiBO,KAAM,GACrE,CAAET,KAAM,sBAAuBC,KAAM,WAAYC,MAAO,sBAAuBskB,KAAM,EAAG/jB,KAAM,IAC9F,CAAET,KAAM,gBAAiBC,KAAM,MAAOC,MAAO,gBAAiBO,KAAM,OAKhFwjB,UAAU9iB,UAAYsjB,EAAe5kB,KChJrC,MAAM6kB,EAAgB,CAClB,UAAc,OACd,aAAc,UACd,YAAc,SACd,aAAc,UACd,aAAc,WAGlB,SAASC,EAAYC,GACjB,IAAKA,EAAG,MAAO,GACf,MAAMC,EAAMnc,OAAOkc,GACnB,OAAOC,EAAIC,OAAO,GAAGC,cAAgBF,EAAIjF,MAAM,EACnD,CAMA,MAAMoF,EAAkB,CACpBC,MAAc,CAAElR,KAAM,WAA8BmR,YAAa,QAAYC,WAAY,WACzFC,MAAc,CAAErR,KAAM,kBAA8BmR,YAAa,QAAYC,WAAY,cACzFE,MAAc,CAAEtR,KAAM,uBAA8BmR,YAAa,QAAYC,WAAY,cACzFG,IAAc,CAAEvR,KAAM,sBAA8BmR,YAAa,MAAYC,WAAY,aACzF1P,SAAc,CAAE1B,KAAM,uBAA8BmR,YAAa,WAAYC,WAAY,gBACzFI,YAAc,CAAExR,KAAM,8BAA+BmR,YAAa,WAAYC,WAAY,cAC1FK,aAAc,CAAEzR,KAAM,yBAA8BmR,YAAa,WAAYC,WAAY,cACzFM,QAAc,CAAE1R,KAAM,sBAA8BmR,YAAa,WAAYC,WAAY,WACzFO,MAAc,CAAE3R,KAAM,kBAA8BmR,YAAa,WAAYC,WAAY,iBAG7F,SAASQ,EAAkB7kB,GACvB,MAAM8kB,EAAO9kB,GAAsC,mBAAtBA,EAAM+kB,YAC7B/kB,EAAM+kB,cACL/kB,GAAOiN,MAAM,aAAe,QACnC,OAAOiX,EAAgBY,IAAQZ,EAAgBU,KACnD,CAWA,MAAMI,2BAA2BvU,EAC7B,WAAApS,CAAYO,EAAU,IAGlBL,MAAM,CACF2S,UAAW,8BACRtS,IAEPkC,KAAKmkB,eAAiBrmB,EAAQqmB,gBAAkBf,EAAgBU,KACpE,CAWA,cAAAvN,GACI,MAAMlY,EAAO2B,KAAKmkB,gBAAgBb,YACrB,UAATjlB,GAA6B,UAATA,GACpB2B,KAAKiT,aAAajT,KAAKyL,QAC/B,CAEA,WAAA2U,GACI,MAAM/hB,EAAO2B,KAAKmkB,eAAeb,YAC3BhD,EAAMtgB,KAAKd,MAAMiN,IAAI,QAAU,GAC/B2U,EAAW9gB,KAAKd,MAAMiN,IAAI,aAAe,GAE/C,GAAa,UAAT9N,EAEA,MAAO,8EAEa+lB,EAHApkB,KAAKd,MAAMmlB,iBAAmBrkB,KAAKd,MAAMmlB,mBAAsB/D,sCAI/D8D,EAAWtD,wUASnC,GAAa,UAATziB,EAAkB,CAClB,MAAMimB,EAAStkB,KAAKd,MAAMmlB,iBAAmBrkB,KAAKd,MAAMmlB,kBACxD,MAAO,wIAGeD,EAAW9D,mCAChBgE,EAAS,WAAWF,EAAWE,MAAa,uIAIjE,CAEA,GAAa,UAATjmB,EACA,MAAO,sGAEgB2B,KAAKmkB,eAAehS,6FACAF,EAAW6O,mEACTsD,EAAW9D,qDAK5D,GAAa,QAATjiB,EACA,MAAO,sGAEgB2B,KAAKmkB,eAAehS,oGACLF,EAAW6O,iiBAarD,GAAa,aAATziB,EAAqB,CACrB,MAAMkmB,EAAUvkB,KAAKd,MAAMslB,uBAAyBxkB,KAAKd,MAAMslB,wBAC/D,MAAO,yFAEGD,EACI,aAAaH,EAAWG,EAAQjE,cAAc8D,EAAWtD,wEACzD,gBAAgB9gB,KAAKmkB,eAAehS,yGACZF,EAAW6O,oiBAWrD,CAGA,MAAO,8FAEgB9gB,KAAKmkB,eAAehS,mGACLF,EAAW6O,wYASrD,CAEA,sBAAMP,GACFkE,EAAkBzkB,KAAKd,MAAOc,KAAKmkB,eACvC,CAEA,0BAAMO,GACFC,EAAa3kB,KAAKd,MACtB,EAsBJ,MAAM0lB,8BAA8BjV,EAChC,WAAApS,CAAYO,EAAU,IAClBL,MAAM,CACF2S,UAAW,iCACRtS,GAEX,CAEA,WAAAsiB,GACI,OAAIpgB,KAAKd,MAAM2lB,eAAiB7kB,KAAKd,MAAM2lB,gBAChC7kB,KAAK8kB,wBAEZ9kB,KAAKd,MAAM6lB,iBAAmB/kB,KAAKd,MAAM6lB,kBAClC/kB,KAAKglB,wBAKThlB,KAAKilB,qBAChB,CAEA,qBAAAH,GACI,MAAMI,EAAallB,KAAKd,MAAMimB,gBACxBC,EAAQF,EAAWhb,IAAII,GAAKtK,KAAKqlB,WAAW/a,IAAI8H,KAAK,IACrD9K,EAAQ4d,EAAWpgB,OACzB,MAAO,2JAGOwC,cAA4B,IAAVA,EAAc,GAAK,6oBAWxB8d,mBAE/B,CAEA,UAAAC,CAAW/a,GACP,MAAMgW,EAAMhW,GAAKA,EAAEgW,IAAMhW,EAAEgW,IAAM,GAC3BgF,EAAKhb,GAA+B,iBAAnBA,EAAE4V,aAA4B5V,EAAE4V,aAAe,GAChEqF,EAAOjb,GAAKA,EAAEib,KAAOjb,EAAEib,KAAO,YAC9BzE,EAAWxW,GAAKA,EAAEwW,SAAWxW,EAAEwW,SAAWyE,EAC1C7mB,EAAO4L,GAAKA,EAAEkb,UA04B5B,SAAqBC,GACjB,GAAa,MAATA,GAAiB9Y,MAAM8Y,GAAQ,MAAO,GAC1C,MAAMC,EAAIhZ,OAAO+Y,GACjB,OAAIC,EAAI,KAAa,GAAGA,MACpBA,EAAI,QAAoB,IAAIA,EAAI,MAAMC,QAAQ,QAC9CD,EAAI,WAA2B,IAAIA,EAAI,KAAO,MAAMC,QAAQ,QACzD,IAAID,EAAI,KAAO,KAAO,MAAMC,QAAQ,OAC/C,CAj5BwCC,CAAYtb,EAAEkb,WAAa,GACrDK,EAAcvb,GAAKA,EAAEwb,OAASxb,EAAEyb,OAAU,GAAGzb,EAAEwb,WAAWxb,EAAEyb,SAAW,GAEvEC,EAAW,CACb,+BACA,aAAa5B,EAAW9D,MACxB,YAAY8D,EAAWkB,MACvB,kBAAkBlB,EAAWtD,MAC7B,cAAcsD,EAAWmB,OAC3BnT,KAAK,KAEP,IAAImS,EAEAA,EADAe,EAAG9f,WAAW,WAAa8a,EACjB,aAAa8D,EAAW9D,YAAc8D,EAAWmB,mTAIpDD,EAAG9f,WAAW,UACX,0tBAQIyM,EAAWqT,0DAGlBA,EAAG9f,WAAW,UACX,ucAQA,kcASd,MAAMygB,EAAS3F,EAAM,mMAGH0F,uRAK2C5B,EAAW9D,4JAIrD8D,EAAW9D,iBAAmB8D,EAAWtD,qQAOxD,GAEJ,MAAO,uJAGYR,EAAM0F,EAAW,MAAM1F,EAAM,yCAA2C,gCACzEiE,iSAImFH,EAAWmB,OAAUtT,EAAWsT,0CAC/G7mB,EAAO,uFAAuFuT,EAAWvT,aAAkB,+DAE/HmnB,EAAa,6EAA6E5T,EAAW4T,WAAsB,uDAE/HI,yDAIlB,CAEA,mBAAAhB,GACI,MAAO,06BAiBX,CAEA,qBAAAD,GACI,MAAO,2VAOX,CAIA,+BAAMkB,GACF,UACUlmB,KAAKd,MAAMqJ,OACrB,OAAS8R,GACLtH,QAAQC,KAAK,6CAA8CqH,EAC/D,CACJ,CAEA,2BAAM8L,CAAsBliB,EAAOgD,GAC3BhD,IAASA,EAAM8Q,iBAAkB9Q,EAAMmiB,mBAC3C,MAAM9F,EAAMrZ,EAAQwN,QAAQ6L,IACtBgF,EAAMre,EAAQwN,QAAQ6Q,IAAM,GAClC,GAAKhF,EAAL,CAEA,GAAIgF,EAAG9f,WAAW,UAAW,CACzB,MAAM6gB,EAA6B,oBAAXhgB,OAAyBA,OAAOoa,MAAMC,SAASF,gBAAkB,KACzF,GAAI6F,GAAqC,mBAAlBA,EAAS1F,KAAqB,CACjD,MAAM2F,EAAStmB,KAAKd,MAAMimB,gBACrB9a,OAAOC,GAAKA,GAAKA,EAAEgW,KAAiC,iBAAnBhW,EAAE4V,cAA6B5V,EAAE4V,aAAa1a,WAAW,WAC1F0E,IAAII,IAAA,CAAQsW,IAAKtW,EAAEgW,IAAKO,IAAKvW,EAAEib,MAAQ,MACtCgB,EAAahR,KAAKiR,IAAI,EAAGF,EAAOG,UAAUC,GAAOA,EAAI9F,MAAQN,IAEnE,YADA+F,EAAS1F,KAAK2F,EAAQ,CAAEC,aAAYI,aAAa,GAErD,CACJ,CAIAtgB,OAAO0a,KAAKT,EAAK,SAAU,WAhBjB,CAiBd,CAEA,8BAAMsG,CAAyB3iB,EAAOgD,GAC9BhD,IAASA,EAAM8Q,iBAAkB9Q,EAAMmiB,mBAC3C,MAAM9F,EAAMrZ,EAAQwN,QAAQ6L,IAC5B,GAAKA,EACL,IACI,GAAIuG,UAAUC,WAAazgB,OAAO0gB,sBACxBF,UAAUC,UAAUE,UAAU1G,OACjC,CACH,MAAM2G,EAAWpT,SAASC,cAAc,YACxCmT,EAASvnB,MAAQ4gB,EACjBzM,SAAS5K,KAAKuT,YAAYyK,GAC1BA,EAASC,SACTrT,SAASsT,YAAY,QACrBtT,SAAS5K,KAAKme,YAAYH,EAC9B,CAEA,MAAM9U,EAAOlL,EAAQE,cAAc,KACnC,GAAIgL,EAAM,CACN,MAAMkV,EAAOlV,EAAK/B,UAClB+B,EAAK/B,UAAY,8BACjBxI,WAAW,KAAQuK,EAAK/B,UAAYiX,GAAS,KACjD,CACArnB,KAAKiG,UAAUyE,OAAOpL,UAAU,0BACpC,OAAS+a,GACLtH,QAAQvK,MAAM,gCAAiC6R,GAC/Cra,KAAKiG,UAAUyE,OAAOlC,QAAQ,qBAClC,CACJ,CAIA,mCAAM8e,GACF,IAAIC,EAAOvnB,KAAKkR,OAChB,KAAOqW,GAAM,CACT,GAAiD,mBAAtCA,EAAKC,6BACZ,OAAOD,EAAKC,+BAEhBD,EAAOA,EAAKrW,MAChB,CACJ,EAOJ,SAASuT,EAAkBvlB,EAAOilB,GAC9B,MAAM7D,EAAMphB,EAAMiN,IAAI,OACtB,IAAKmU,EAAK,OACV,MAAMjiB,EAAO8lB,EAAeb,YAE5B,GAAa,UAATjlB,EAAkB,CAClB,MAAMgoB,EAA6B,oBAAXhgB,OAAyBA,OAAOoa,MAAMC,SAASF,gBAAkB,KACnF0E,EAAahmB,EAAMiN,IAAI,eAAiB,CAAA,EACxCma,EAAS,CACX,CAAE1F,IAAKN,EAAKO,IAAK,eACdjc,OAAOiO,OAAOqS,GACZ7a,OAAOC,GAAKA,GAAKA,EAAEgW,KAAiC,iBAAnBhW,EAAE4V,cAA6B5V,EAAE4V,aAAa1a,WAAW,WAC1F0E,IAAII,IAAA,CAAQsW,IAAKtW,EAAEgW,IAAKO,IAAKvW,EAAEib,MAAQ,OAOhD,YALIc,GAAqC,mBAAlBA,EAAS1F,KAC5B0F,EAAS1F,KAAK2F,EAAQ,CAAEK,aAAa,IAErCtgB,OAAO0a,KAAKT,EAAK,UAGzB,CAEA,GAAa,QAATjiB,EAAgB,CAChB,MAAM2iB,EAA8B,oBAAX3a,OAAyBA,OAAOoa,MAAMC,SAASM,UAAY,KAMpF,YALIA,GAA6C,mBAAzBA,EAAUC,WAC9BD,EAAUC,WAAWX,EAAK,CAAEpiB,MAAOgB,EAAMiN,IAAI,cAE7C9F,OAAO0a,KAAKT,EAAK,UAGzB,CAEAja,OAAO0a,KAAKT,EAAK,SACrB,CAEA,SAASqE,EAAazlB,GAClB,MAAMohB,EAAMphB,EAAMiN,IAAI,OACtB,IAAKmU,EAAK,OACV,MAAMvH,EAAIlF,SAASC,cAAc,KACjCiF,EAAE0O,KAAOnH,EACTvH,EAAE2O,SAAWxoB,EAAMiN,IAAI,aAAe,GACtC0H,SAAS5K,KAAKuT,YAAYzD,GAC1BA,EAAE4O,QACF9T,SAAS5K,KAAKme,YAAYrO,EAC9B,CAYA,MAAM6O,0BAA0BjY,EAC5B,WAAApS,CAAYO,EAAU,IAClBL,MAAM,CACF2S,UAAW,6BACRtS,IAEPkC,KAAKwD,SAAW,qiBAYpB,CAEA,YAAME,GACF,MAAMmkB,EAAS7nB,KAAKd,MAAMiN,IAAI,MAC9B,IAAK0b,EAAQ,OAEb,MAAM3nB,EAAa,IAAIoiB,cAAc,CACjC5d,OAAQ,CACJojB,OAAQ,gBACR9H,KAAM6H,EACN5iB,KAAM,WACNvG,KAAM,MAGdsB,KAAK+nB,kBAAoB7nB,EAEzBF,KAAKgoB,YAAc,IAAInkB,EAAU,CAC7BC,YAAa,oBACb5D,aACA4B,oBAAqB,CAAC,SAAU,QAChCvD,QAAS,CACL,CACI+G,IAAK,OACLhH,MAAO,YACPkF,SAAU,kjBAYd,CAAE8B,IAAK,oBAAqBhH,MAAO,YAAashB,UAAW,gBAC3D,CAAEta,IAAK,YAAahH,MAAO,OAAQwnB,MAAO,OAAQxkB,UAAU,GAC5D,CAAEgE,IAAK,eAAgBhH,MAAO,UAAWwnB,MAAO,OAAQlG,UAAW,aACnE,CAAEta,IAAK,YAAahH,MAAO,SAAUwnB,MAAO,OAAQlG,UAAW,aAC/D,CAAEta,IAAK,aAAchH,MAAO,UAAWwnB,MAAO,QAASlG,UAAW,4BAA6Bte,UAAU,GACzG,CAAEgE,IAAK,UAAWhH,MAAO,UAAWwnB,MAAO,QAASlG,UAAW,WAAYte,UAAU,GACrF,CAAEgE,IAAK,gBAAiBhH,MAAO,OAAQshB,UAAW,4BAA6BqI,WAAY,OAE/FzmB,WAAW,EACXF,UAAU,EACVD,YAAY,EACZE,YAAY,EACZhB,YAAa,CACT,CAAEjC,MAAO,iBAAkBia,OAAQ,kBAAmBpG,KAAM,gBAC5D,CAAE+V,SAAS,GACX,CAAE5pB,MAAO,SAAUia,OAAQ,eAAgBpG,KAAM,kBAAmBgW,QAAQ,IAEhF7lB,aAAc,CACVG,OAAO,EACP/D,KAAM,KACNiE,aAAc,4DACdylB,UAAW,gBACX9nB,QAAS,MAGjBN,KAAKmE,SAASnE,KAAKgoB,YACvB,CAGA,aAAAK,GACI,OAAOroB,KAAK+nB,mBAAmBxf,OACnC,CAIA,kCAAM+f,GACF,IAAIf,EAAOvnB,KAAKkR,OAChB,KAAOqW,GAAM,CACT,GAAsC,mBAA3BA,EAAKgB,kBACZ,OAAOhB,EAAKgB,oBAEhBhB,EAAOA,EAAKrW,MAChB,CACA,OAAO,IACX,CAEA,2BAAMsX,CAAsBvkB,EAAOgD,GAC3BhD,IAASA,EAAM8Q,iBAAkB9Q,EAAMmiB,mBAC3C,MAAMqC,EAAOxhB,GAASwN,SAASgU,KAC/B,IAAKA,EAAM,OACX,MAAMnI,EA0hBd,SAAuBmI,EAAMhe,GACzB,IAAKge,EAAM,MAAO,GAClB,MAAMC,EACFje,GAAKU,QAAQwd,qBACM,oBAAXtiB,OAAyBA,OAAOC,SAASsiB,OAAS,IAC9D,MAAO,GAAG9hB,OAAO4hB,GAAMG,QAAQ,OAAQ,SAASJ,GACpD,CAhiBoBK,CAAcL,EAAMzoB,KAAKiG,YACrC,UACU4gB,UAAUC,UAAUE,UAAU1G,GACpCtgB,KAAKiG,UAAUyE,OAAOpL,UAAU,WAAWghB,IAC/C,OAASyI,GACL/oB,KAAKiG,UAAUyE,OAAOC,UAAU,yCACpC,CACJ,CAEA,yBAAMqe,CAAoB/kB,EAAOgD,GACzBhD,IAASA,EAAM8Q,iBAAkB9Q,EAAMmiB,mBAE3C,MAAM6C,EAAMhiB,GAASiiB,UAAU,iBACzBvhB,EAAKshB,GAAKxU,SAAS0U,OAASliB,GAASwN,SAAS9M,GACpD,IAAKA,EAAI,OACT,MAAMyhB,EAASppB,KAAK+nB,mBAAmB5b,MAAMxE,GAC7C,GAAKyhB,SAEmB/gB,EAAMwB,QAC1B,4FACA,eACA,CAAEwf,YAAa,SAAUC,aAAc,eAI3C,UACUF,EAAOjf,KAAK,CAAEof,WAAW,IAC/BvpB,KAAKiG,UAAUyE,OAAOpL,UAAU,uBAC1BU,KAAKqoB,eACf,OAAShO,GACLtH,QAAQvK,MAAM,0BAA2B6R,GACzCra,KAAKiG,UAAUyE,OAAOlC,QAAQ,yBAClC,CACJ,EAYJ,MAAMghB,4BAA4B7Z,EAC9B,WAAApS,CAAYO,EAAU,IAClBL,MAAM,CACF2S,UAAW,wBAGXmH,gBAAgB,EAChB/T,SAAU,86EAyCP1F,GAEX,CAEA,UAAI2rB,GAAW,QAASzpB,KAAKd,OAAOiN,MAAM,MAAQ,CAElD,YAAMzI,GACF,MAAM4d,EAAIthB,KAAKd,MACfc,KAAK0pB,QAAU,IAAIvO,WAAW,CAC1BrX,YAAa,gBACbxF,MAAO,YACPoB,MAAO8M,EAAcsT,MAAM,WAAYwB,EAAEnV,IAAI,eAAiB,MAElEnM,KAAK2pB,QAAU,IAAIxO,WAAW,CAC1BrX,YAAa,gBACbxF,MAAO,OACPoB,MAAOqjB,EAAYzB,EAAEnV,IAAI,aAAe,WAE5CnM,KAAK4pB,UAAY,IAAIzO,WAAW,CAC5BrX,YAAa,kBACbxF,MAAO,SACPoB,MAAOqjB,EAAYzB,EAAEnV,IAAI,kBAAoB,OAEjD,MAAM0d,EAAUvI,EAAEnV,IAAI,WACtBnM,KAAK8pB,YAAc,IAAI3O,WAAW,CAC9BrX,YAAa,oBACbxF,MAAO,WACPoB,MAAOmqB,GAAWrd,EAAcsT,MAAM,WAAY+J,IAAmB,MAEzE,CAAC7pB,KAAK0pB,QAAS1pB,KAAK2pB,QAAS3pB,KAAK4pB,UAAW5pB,KAAK8pB,aAC7CzkB,QAAQiN,GAAKtS,KAAKmE,SAASmO,GACpC,EAQJ,MAAMyX,iBAAiBxP,WACnB,WAAAhd,CAAYO,EAAU,IAClB,MAAMoB,EAAQpB,EAAQoB,OAAS,IAAI8qB,EAAKlsB,EAAQN,MAAQ,IAClD2mB,EAAiBJ,EAAkB7kB,GAInC+qB,EAAkB,IAAIT,oBAAoB,CAAEtqB,UAE5CgrB,EAAiB,IAAIhG,mBAAmB,CAAEhlB,QAAOilB,mBAEjDgG,EAAiB,IAAIC,EAAS,CAChClrB,QACAkR,UAAW,MACXia,iBAAiB,EACjBC,eAAgB,IAChB/rB,QAAS,EACTJ,OAAQ,CACJ,CAAEC,KAAM,KAAME,MAAO,MACrB,CAAEF,KAAM,WAAYE,MAAO,YAC3B,CAAEF,KAAM,mBAAoBE,MAAO,oBACnC,CAAEF,KAAM,eAAgBE,MAAO,gBAC/B,CAAEF,KAAM,YAAaE,MAAO,YAAagQ,OAAQ,YACjD,CAAElQ,KAAM,WAAYE,MAAO,YAC3B,CAAEF,KAAM,gBAAiBE,MAAO,SAAUgQ,OAAQ,SAClD,CAAElQ,KAAM,UAAWE,MAAO,UAAWgQ,OAAQ,YAC7C,CAAElQ,KAAM,WAAYE,MAAO,WAAYgQ,OAAQ,YAC/C,CAAElQ,KAAM,oBAAqBE,MAAO,eACpC,CAAEF,KAAM,oBAAqBE,MAAO,mBACpC,CAAEF,KAAM,oBAAqBE,MAAO,gBACpC,CAAEF,KAAM,MAAOE,MAAO,aAAcgQ,OAAQ,OAC5C,CAAElQ,KAAM,YAAaE,MAAO,YAAagQ,OAAQ,cAInDic,EAAoB,IAAI3F,sBAAsB,CAAE1lB,UAEhD0Q,EAAW,CACb,CAAEtK,IAAK,WAAchH,MAAO,WAAc6T,KAAM,cAAqBlB,KAAMgZ,GAC3E,CAAE3kB,IAAK,UAAchH,MAAO,UAAc6T,KAAMgS,EAAehS,KAAMlB,KAAMiZ,GAC3E,CAAE5kB,IAAK,UAAchH,MAAO,UAAc6T,KAAM,iBAAqBlB,KAAMkZ,GAC3E,CAAE7kB,IAAK,aAAchH,MAAO,aAAc6T,KAAM,YAAqBlB,KAAMsZ,IAK/E,IAAIC,EAAgB,KAChBtrB,EAAMiN,IAAI,QACVqe,EAAgB,IAAI5C,kBAAkB,CAAE1oB,UACxC0Q,EAASoB,KAAK,CAAE1L,IAAK,SAAUhH,MAAO,SAAU6T,KAAM,gBAAiBlB,KAAMuZ,KAIjF,MAAMnJ,EAAWniB,EAAMiN,IAAI,YAC3B,GAAIkV,GAAgC,iBAAbA,GAAyBzc,OAAOC,KAAKwc,GAAUvc,OAAQ,CAC1E,MAAM2lB,EAAe,IAAIL,EAAS,CAC9B5sB,KAAM6jB,EACNjR,UAAW,MACX7R,QAAS,EACT8rB,iBAAiB,IAErBza,EAASoB,KAAK,CAAE1L,IAAK,WAAYhH,MAAO,WAAY6T,KAAM,YAAalB,KAAMwZ,GACjF,CAMA,MAAMpK,EAAiD,mBAA1BnhB,EAAMmlB,gBAAkCnlB,EAAMmlB,kBAAoB,KACzF1N,EAAW0J,EACX,aAAa+D,EAAW/D,cACxB,KACA5J,EAAWqM,EAAcqB,EAAeZ,aAAe,KAEvDpM,EAAQ,CACV,CAAEhF,KAAM,SAAUL,QAAWtF,EAAcsT,MAAM,WAAYwB,EAAEnV,IAAI,cAAe4F,QAAS,QACzFqG,KAAMkJ,KAAOA,EAAEnV,IAAI,cACrB,CAAE2F,KAAMwP,GAAKA,EAAEnV,IAAI,iBAAmB,KAAM4F,QAAS,QACnDqG,KAAMkJ,KAAOA,EAAEnV,IAAI,iBACrB,CAAE2F,KAAMwP,GAAKyB,EAAYzB,EAAEnV,IAAI,aAAe,SAC5C4F,SAAUoS,EAAeZ,YAAc,gBAAgBsF,QAAQ,OAAQ,KACzE,CAAE/W,KAAMwP,GAAKyB,EAAYzB,EAAEnV,IAAI,kBAAoB,IAAK4F,QAAS,YAC/DqG,KAAMkJ,KAAOA,EAAEnV,IAAI,kBACrB,CAAEgG,KAAM,YAAaL,KAAM,SAAUC,QAAS,UAC5CqG,KAAMkJ,KAAOA,EAAEnV,IAAI,cACrB,CAAEgG,KAAM,UAAWL,KAAM,UAAWC,QAAS,YAC3CqG,KAAMkJ,IAAMA,EAAEnV,IAAI,eAGlBue,EAAe,CACjB,CAAEpsB,MAAO,OAAuBia,OAAQ,YAAyBpG,KAAM,UACvE,CAAE7T,MAAO,WAAuBia,OAAQ,gBAAyBpG,KAAM,eACvE,CAAE7T,MAAO,WAAuBia,OAAQ,WAAyBpG,KAAM,gBACvE,CAAE7T,MAAO,cAAuBia,OAAQ,aAAyBpG,KAAM,iBACvE,CAAE9T,KAAM,WACR,CAAEC,MAAO,eAAuBia,OAAQ,YAAyBpG,KAAM,aACvEjT,EAAMiN,IAAI,aACJ,CAAE7N,MAAO,eAAiBia,OAAQ,eAAyBpG,KAAM,WACjE,CAAE7T,MAAO,cAAiBia,OAAQ,cAAyBpG,KAAM,aACvE,CAAE7T,MAAO,sBAAuBia,OAAQ,wBAAyBpG,KAAM,mBACvE,CAAE9T,KAAM,WACR,CAAEC,MAAO,cAAuBia,OAAQ,cAAyBpG,KAAM,WAAYgW,QAAQ,IAG/F1qB,MAAM,CACF2S,UAAW,eACRtS,EACHoB,QACA8J,OAAQ,CACJmJ,KAAMgS,EAAehS,KACrBsE,WACAE,WACAE,QAASyK,GAAKA,EAAEnV,IAAI,aAAe,eACnC6K,WAAYsK,IACR,MAAMuI,EAAUvI,EAAEnV,IAAI,WACtB,IAAK0d,EAAS,MAAO,GACrB,MAAMc,EAAMne,EAAcsT,MAAM,WAAYtT,EAAcsT,MAAM,QAAS+J,IACzE,OAAOc,EAAM,YAAYA,IAAQ,IAErCxT,QACA5W,YAAa,CAAEuJ,MAAO4gB,IAE1B9a,WACAC,cAAe,WACfC,SAAU,IACVG,SAAU,IACVF,eAAgB,mBAIpB/P,KAAKiqB,gBAAkBA,EACvBjqB,KAAKkqB,eAAiBA,EACtBlqB,KAAKmqB,eAAiBA,EACtBnqB,KAAKuqB,kBAAoBA,EACzBvqB,KAAKwqB,cAAgBA,CAMzB,CAEA,kBAAAI,GACI,OAAO7G,EAAkB/jB,KAAKd,MAClC,CAEA,qBAAM2M,GACF7L,KAAK6qB,qBACT,CAIA,sBAAMtK,GACFkE,EAAkBzkB,KAAKd,MAAOc,KAAK4qB,qBACvC,CAEA,0BAAMlG,GACFC,EAAa3kB,KAAKd,MACtB,CAEA,qBAAM4rB,GACF,MAAMxK,EAAMtgB,KAAKd,MAAMiN,IAAI,OAC3B,GAAKmU,EACL,IACI,GAAIuG,UAAUC,WAAazgB,OAAO0gB,sBACxBF,UAAUC,UAAUE,UAAU1G,OACjC,CACH,MAAM2G,EAAWpT,SAASC,cAAc,YACxCmT,EAASvnB,MAAQ4gB,EACjBzM,SAAS5K,KAAKuT,YAAYyK,GAC1BA,EAASC,SACTrT,SAASsT,YAAY,QACrBtT,SAAS5K,KAAKme,YAAYH,EAC9B,CACAjnB,KAAKiG,UAAUyE,OAAOpL,UAAU,0BACpC,OAASkJ,GACLuK,QAAQvK,MAAM,sBAAuBA,GACrCxI,KAAKiG,UAAUyE,OAAOlC,QAAQ,qBAClC,CACJ,CAEA,sBAAMuiB,SACiB1iB,EAAM2iB,UAAU,CAC/B9sB,MAAO,eAAe8B,KAAKd,MAAMiN,IAAI,cACrCjN,MAAOc,KAAKd,MACZ+rB,WAAYC,EAAUjtB,QAGtB+B,KAAKyL,QAEb,CAEA,wBAAM0f,SACInrB,KAAKd,MAAMiL,KAAK,CAAEihB,WAAW,IACnCprB,KAAKyL,QACT,CAEA,yBAAM4f,SACIrrB,KAAKd,MAAMiL,KAAK,CAAEihB,WAAW,IACnCprB,KAAKyL,QACT,CAEA,uBAAM8c,GACF,IAAKvoB,KAAKd,MAAMiN,IAAI,MAEhB,YADAnM,KAAKiG,UAAUyE,OAAOC,UAAU,oCAKpC,MAAM2gB,QAAmBjjB,EAAMgD,KAAK,CAChCnN,MAAO,aACPQ,KAAM,KACNF,KAAM,4EACNL,OAAQ,CACJ,CAAEC,KAAM,cAAeC,KAAM,SAAUC,MAAO,sBAAuBoB,MAAO,GAAIijB,IAAK,EAAG9jB,KAAM,GAAIL,KAAM,wCACxG,CAAEJ,KAAM,eAAgBC,KAAM,SAAUC,MAAO,eAAgBoB,OAAO,EAAMb,KAAM,GAAIL,KAAM,0DAC5F,CAAEJ,KAAM,OAAQC,KAAM,WAAYC,MAAO,kBAAmBskB,KAAM,EAAG/jB,KAAM,GAAI0sB,UAAW,IAAK/sB,KAAM,kDAEzGgtB,WAAY,UAEhB,IAAKF,EAAY,OAGjB,MAAMld,EAAO,CAAA,EAYb,IAAIjP,OAX2B,IAA3BmsB,EAAWG,aAAwD,OAA3BH,EAAWG,aAAmD,KAA3BH,EAAWG,cACtFrd,EAAKqd,YAAc/e,OAAO4e,EAAWG,mBAET,IAA5BH,EAAWI,eACXtd,EAAKsd,eAAiBJ,EAAWI,cAEjCJ,EAAWK,OACXvd,EAAKud,KAAO7kB,OAAOwkB,EAAWK,MAAM3N,MAAM,EAAG,MAKjD,IACI7e,QAAaa,KAAKd,MAAM0sB,OAAMhnB,OAAOC,KAAKuJ,GAAMtJ,QAASsJ,EAC7D,OAASiM,GAGL,OAFAtH,QAAQvK,MAAM,gBAAiB6R,QAC/BhS,EAAMK,UAAU2R,GAAK7c,MAAMgL,OAAS6R,GAAK1R,SAAW,8BAExD,CAEA,MAAMnL,EAAO2B,GAAM3B,KACbquB,EAAWruB,GAAM8iB,IACvB,IAAKnhB,GAAMG,UAAYusB,EAEnB,YADAxjB,EAAMK,UAAUlL,GAAMgL,OAAS,+BAKnC,IAAIsjB,GAAS,EACb,UACUjF,UAAUC,WAAWE,YAAY6E,IACvCC,GAAS,CACb,OAAS/C,GACL+C,GAAS,CACb,CAGA,MAAMC,EAASvuB,EAAKwuB,WACd,IAAI1nB,KAAK9G,EAAKwuB,YAAYC,iBAC1B,QACAC,EAAU1uB,EAAKkuB,aAAe,MAAQ,KACtCS,EAAWL,EACX,6GACA,6EACAM,EAAU,+MAGiEhI,EAAWyH,yBAClFM,8IAG2Dla,EAAW8Z,uFACX9Z,EAAWia,4BACtE1uB,EAAK6uB,eAAiB,iEAAiEpa,EAAWzU,EAAK6uB,8BAAgC,wCAG3IhkB,EAAMikB,MAAMF,EAAS,qBAAsB,CAAE/tB,KAAM,YAGzD,UACU2B,KAAKwqB,eAAenC,kBAC9B,OAAShO,GACLtH,QAAQC,KAAK,oCAAqCqH,EACtD,CACJ,CAEA,kCAAMmN,GAMF,SALwBnf,EAAMwB,QAC1B,qKACA,sBACA,CAAEwf,YAAa,eAEnB,CAEA,UACUrpB,KAAKd,MAAMqtB,uBACjBvsB,KAAKiG,UAAUyE,OAAOpL,UAAU,2CACpC,OAAS+a,GAGL,OAFAtH,QAAQvK,MAAM,2CAA4C6R,QAC1Dra,KAAKiG,UAAUyE,OAAOlC,QAAQ,uCAElC,CAGAxI,KAAKwsB,0BAA0B,CAAExmB,OAAO,GAZxB,CAapB,CAMA,yBAAAwmB,CAA0B1uB,EAAU,IAChC,GAAIkC,KAAKysB,qBAAsB,OAI/B,IAAK3uB,EAAQkI,MAAO,OAGpB,IAAI0mB,EAAW,EAEf,MAAMC,EAAO,KACT3sB,KAAKysB,qBAAuB,KACvBzsB,KAAKd,QACNc,KAAKd,MAAM2lB,eAAiB7kB,KAAKd,MAAM2lB,mBACrC6H,EARU,KAUhB1sB,KAAKysB,qBAAuB7kB,WAAW5D,UACnC,UACUhE,KAAKd,MAAMqJ,OACrB,OAAS8R,GACLtH,QAAQC,KAAK,0CAA2CqH,EAC5D,CACAsS,KAfW,QAmBnBA,GACJ,CAEA,mBAAA9B,GACQ7qB,KAAKysB,uBACLG,aAAa5sB,KAAKysB,sBAClBzsB,KAAKysB,qBAAuB,KAEpC,CAEA,wBAAMI,GAMF,WALwBxkB,EAAMwB,QAC1B,6CAA6C7J,KAAKd,MAAMiN,IAAI,8CAC5D,mBACA,CAAEmd,aAAc,aAAcD,YAAa,YAE/B,OAEhB,MAAMlqB,QAAaa,KAAKd,MAAMyK,UAC1BxK,GAAQA,EAAKG,SACbU,KAAKwT,KAAK,eAAgB,CAAEtU,MAAOc,KAAKd,OAEhD,CAIA,cAAAqX,GAEA,CAEA,iBAAMzD,CAAY1U,GACV4B,KAAK8sB,mBACC9sB,KAAK8sB,YAAYha,YAAY1U,EAE3C,CAEA,gBAAAwX,GACI,OAAO5V,KAAK8sB,YAAc9sB,KAAK8sB,YAAYlX,mBAAqB,IACpE,CAEA,aAAO5J,CAAOlO,EAAU,IACpB,OAAO,IAAIisB,SAASjsB,EACxB,EAOJ,SAASmU,EAAWgR,GAChB,OAAW,MAAPA,EAAoB,GACjBnc,OAAOmc,GACT4F,QAAQ,KAAM,SACdA,QAAQ,KAAM,QACdA,QAAQ,KAAM,QACdA,QAAQ,KAAM,UACdA,QAAQ,KAAM,QACvB,CAgBA,SAASzE,EAAWnB,GAChB,OAAOhR,EAAWgR,EACtB,CAWA+G,EAAK+C,WAAahD,SAClBC,EAAKgD,UAAY,eC9pCjB,MAAMC,wBAAwBtd,EAC1B,WAAApS,CAAYO,EAAU,IAElB,MAAM6K,EAAU7K,EAAQ6K,SAAW,CAAA,EAC7BukB,EAAQpvB,EAAQovB,OAAS,UACzBC,EAAgBrvB,EAAQqvB,gBAAiB,EACzC5H,EAAO5c,EAAQ4c,OAAS4H,EAAgB,OAAS,MAEvD,IAAIC,EAAM,eACI,YAAVF,IACAE,GAAOD,EAAgB,iBAAmB,iBAEjC,cAAT5H,EAAsB6H,GAAO,qBACf,SAAT7H,IAAiB6H,GAAO,iBAEjC3vB,MAAM,CACF2S,UAAWgd,KACRtvB,IAGPkC,KAAK2I,QAAUA,EACf3I,KAAKktB,MAAQA,EACbltB,KAAKmtB,cAAgBA,EACrBntB,KAAKulB,KAAOA,CAChB,CAEA,WAAAnF,GAGI,MAA0B,iBAAtBpgB,KAAK2I,QAAQtK,KACN,qOASQ,YAAf2B,KAAKktB,MACEltB,KAAKqtB,qBAELrtB,KAAKstB,oBAEpB,CAKA,kBAAAA,GACI,MAAMC,EAAYvtB,KAAKmtB,cAAgB,aAAe,eAChDK,EAA4B,cAAdxtB,KAAKulB,KAEzB,MAAO,wFAE8BiI,EAAc,UAAYD,4BACjDC,EAAc,oJAAsJ,inBAY5JA,EAAc,OAAS,qdAQ/BxtB,KAAKytB,4EACuBztB,KAAK2I,QAAQhB,IAAM3H,KAAK2H,gIAKtE,CAKA,kBAAA0lB,GAGI,MAAO,+HAF2B,cAAdrtB,KAAKulB,KAKW,6JAA+J,kSAKrLvlB,KAAKytB,4EACuBztB,KAAK2I,QAAQhB,IAAM3H,KAAK2H,gIAKtE,CAMA,qBAAA8lB,GACI,IAAKztB,KAAK2I,QAAQ+kB,YAAiD,IAAnC1tB,KAAK2I,QAAQ+kB,WAAW5oB,OACpD,MAAO,GAEX,MAKM6oB,EAAa3tB,KAAK2I,QAAQ+kB,WAAWxjB,IAAI0jB,IAC3C,MAAMxvB,EANE,CAAC6kB,IACT,MAAM4K,EAAMha,SAASC,cAAc,OAEnC,OADA+Z,EAAIzmB,YAAc6b,EACX4K,EAAI9Z,WAGE+Z,CAAIF,EAAGxvB,MAAQwvB,EAAGG,UAAU3vB,MAAQ,QAEjD,MAAO,sBAD2B,UAAdwvB,EAAGrjB,OAAqB,YAAc,mBACRnM,aACnDgU,KAAK,IAEF4b,EAAa,SAAShuB,KAAK2I,QAAQhB,IAAM3H,KAAK2H,KACpD,MAAO,qIAEgEqmB,gGACzBhuB,KAAK2I,QAAQ+kB,WAAW5oB,mBAAmB9E,KAAK2I,QAAQ+kB,WAAW5oB,OAAS,EAAI,IAAM,uEAEhGkpB,8CACJL,+DAIpC,CAEA,mBAAMnb,GAGF,GAAIxS,KAAK2I,QAAQslB,aAAejuB,KAAK2I,QAAQslB,YAAYnpB,OAAS,EAAG,CACjE,MAAMopB,EAAuBluB,KAAKiH,QAAQE,cAAc,kCACxD,GAAI+mB,EAAsB,CACtBA,EAAqBna,UAAY,GACjC,IAAA,MAAWpM,KAAM3H,KAAKmuB,SAAU,CAC5B,MAAMC,EAAQpuB,KAAKmuB,SAASxmB,GACxBymB,aAAiBrO,iBAAiB/f,KAAKonB,YAAYgH,EAC3D,CACApuB,KAAK2I,QAAQslB,YAAY5oB,QAAQ2a,IAC7B,MAAMqO,EAAc,IAAItO,gBAAgB,CAAEC,SAC1ChgB,KAAKmE,SAASkqB,GACdA,EAAY5iB,QAAO,EAAMyiB,IAEjC,CACJ,CACJ,EC/JJ,MAAMI,sBAAsB3e,EACxB,WAAApS,CAAYO,EAAU,IAClBL,MAAM,CACF2S,UAAW,qBACRtS,IAGPkC,KAAK0iB,YAAc5kB,EAAQ4kB,aAAe,oBAC1C1iB,KAAKuuB,WAAazwB,EAAQywB,YAAc,OACxCvuB,KAAKwuB,eAA0C,IAA1B1wB,EAAQ0wB,cAC7BxuB,KAAKiuB,YAAc,GACnBjuB,KAAKyuB,kCAAqBC,GAC9B,CAEA,WAAAtO,GACI,MAAO,6TAMwBpgB,KAAK0iB,oaAO1B1iB,KAAKwuB,cAAgB,0RAOnB,kCAGhB,CAEA,mBAAMhc,GAEExS,KAAKwuB,eACLxuB,KAAK2uB,eAAe,CAChBC,iBAAkB,wBAClBC,UAAU,EACVC,cAAe,CAAC,OAChBC,gBAAgB,EAChBC,cAAe,YACfC,gBAAiB,gBAKzB,MAAMhI,EAAWjnB,KAAKiH,QAAQE,cAAc,eACxC8f,IACAA,EAAS7R,iBAAiB,QAAS,IAAMpV,KAAKkvB,mBAAmBjI,IACjEA,EAAS7R,iBAAiB,UAAYzP,GAAM3F,KAAKmvB,cAAcxpB,IAEvE,CAKA,aAAAwpB,CAAclrB,GACQ,UAAdA,EAAMqB,KAAoBrB,EAAMmrB,WAChCnrB,EAAM8Q,iBACN/U,KAAKqvB,oBAAoBprB,EAAOA,EAAMmlB,QAE9C,CAMA,gBAAMkG,CAAWC,GACb,IAAA,MAAWvP,KAAQuP,QACTvvB,KAAKwvB,WAAWxP,EAE9B,CAMA,gBAAMwP,CAAWxP,GACb,MAAMyP,EAAY,IAAIzF,EAChB0F,EAAWprB,KAAKwJ,MAAQyH,KAAKoa,SAGnC3vB,KAAK4vB,eAAeF,EAAU1P,EAAM,GACpChgB,KAAKyuB,eAAetU,IAAIuV,EAAU,CAAE1P,OAAMyP,cAE1C,UACyBA,EAAUI,OAAO,CAClC7P,OACA8P,WAAaC,IACT/vB,KAAKgwB,mBAAmBN,EAAUK,IAEtCE,WAAaC,IACTlwB,KAAKmwB,qBAAqBT,EAAUD,KAIhD,OAASjnB,GACLuK,QAAQvK,MAAM,sBAAuBA,GACrCxI,KAAKowB,kBAAkBV,EAAUlnB,EACrC,CACJ,CAQA,cAAAonB,CAAeF,EAAU1P,EAAM+P,GAC3B,MAAMtc,EAAYzT,KAAKiH,QAAQE,cAAc,kCAC7C,IAAKsM,EAAW,OAEhB,MAAM8Q,EAAU1Q,SAASC,cAAc,OACvCyQ,EAAQnU,UAAY,qBACpBmU,EAAQ9P,QAAQib,SAAWA,EAC3BnL,EAAQxQ,UAAY,kJAGoB/T,KAAKiS,WAAW+N,EAAK5hB,gEACpB4B,KAAKqwB,eAAerQ,EAAKthB,iOAIOqxB,yLAG+CL,8FAKxHjc,EAAU+I,YAAY+H,EAC1B,CAOA,kBAAAyL,CAAmBN,EAAUK,GACzB,MAAMxL,EAAUvkB,KAAKiH,QAAQE,cAAc,oBAAoBuoB,OAC/D,GAAInL,EAAS,CACT,MAAM+L,EAAc/L,EAAQpd,cAAc,iBACtCmpB,IACAA,EAAYtc,MAAM8R,MAAQ,GAAGiK,KAErC,CACJ,CAOA,oBAAAI,CAAqBT,EAAUD,GAE3BzvB,KAAKiuB,YAAYjd,KAAK,CAClBrJ,GAAI8nB,EAAU9nB,GACdvJ,KAAMqxB,EAAUtjB,IAAI,QACpBujB,aAEJ1vB,KAAKyuB,eAAe8B,OAAOb,GAE3B,MAAMnL,EAAUvkB,KAAKiH,QAAQE,cAAc,oBAAoBuoB,OAC/D,GAAInL,EAAS,CACTA,EAAQ7P,UAAUsH,IAAI,mBACtB,MAAMwU,EAAoBjM,EAAQpd,cAAc,wBAC5CqpB,GACAA,EAAkBrc,QAE1B,CACJ,CAOA,iBAAAic,CAAkBV,EAAUlnB,GACxBxI,KAAKyuB,eAAe8B,OAAOb,GAE3B,MAAMnL,EAAUvkB,KAAKiH,QAAQE,cAAc,oBAAoBuoB,OAC3DnL,IACAA,EAAQ7P,UAAUsH,IAAI,gBACtBuI,EAAQpd,cAAc,oBAAoB4M,WACtC,sDAEZ,CAKA,8BAAM0c,CAAyBxsB,EAAOgD,GAClC,MAAMyoB,EAAWzoB,EAAQwN,QAAQib,SAGjC1vB,KAAKyuB,eAAe8B,OAAOb,GAG3B,MAAMnL,EAAUvkB,KAAKiH,QAAQE,cAAc,oBAAoBuoB,OAC3DnL,GAEAA,EAAQpQ,QAEhB,CAOA,yBAAMkb,CAAoBprB,EAAOgD,GAC7B,MACM6K,EADW9R,KAAKiH,QAAQE,cAAc,eACtBzH,MAAM2P,QAGvByC,GAAoC,IAA5B9R,KAAKiuB,YAAYnpB,UAK1B9E,KAAKyuB,eAAe/vB,KAAO,IAM/BsB,KAAK0wB,SAAQ,GAGb1wB,KAAKwT,KAAK,eAAgB,CACtB1B,OACAyd,MAAOvvB,KAAKiuB,eAIpB,CAMA,UAAA0C,CAAWC,GACP,MAAM3J,EAAWjnB,KAAKiH,SAASE,cAAc,eACvC0pB,EAAS7wB,KAAKiH,SAASE,cAAc,kBACvC8f,IAAUA,EAAS/M,UAAY0W,GAC/BC,IAAQA,EAAO3W,UAAY0W,EACnC,CAMA,OAAAF,CAAQI,GACJ,MAAMD,EAAS7wB,KAAKiH,QAAQE,cAAc,kBACpCgL,EAAO0e,EAAO1pB,cAAc,iBAC5ByM,EAAUid,EAAO1pB,cAAc,mBAEjC2pB,GACAD,EAAO3W,UAAW,EAClB/H,EAAKuC,UAAUsH,IAAI,UACnBpI,EAAQc,UAAUP,OAAO,YAEzB0c,EAAO3W,UAAW,EAClB/H,EAAKuC,UAAUP,OAAO,UACtBP,EAAQc,UAAUsH,IAAI,UAE9B,CAKA,UAAA+U,GACI,MAAM9J,EAAWjnB,KAAKiH,QAAQE,cAAc,eACxC8f,IACAA,EAASvnB,MAAQ,GACjBunB,EAASjT,MAAM+R,OAAS,QAG5B,MAAMtS,EAAYzT,KAAKiH,QAAQE,cAAc,kCACzCsM,IACAA,EAAUM,UAAY,IAG1B/T,KAAKiuB,YAAc,GACnBjuB,KAAKyuB,eAAeuC,QAGpBhxB,KAAK0wB,SAAQ,EACjB,CAMA,kBAAAxB,CAAmBjI,GACfA,EAASjT,MAAM+R,OAAS,OACxBkB,EAASjT,MAAM+R,OAASxQ,KAAKoN,IAAIsE,EAASgK,aAAc,KAAO,IACnE,CAOA,cAAAZ,CAAe5K,GACX,GAAc,IAAVA,EAAa,MAAO,MACxB,MAEMyL,EAAI3b,KAAKsI,MAAMtI,KAAK4b,IAAI1L,GAASlQ,KAAK4b,IAFlC,OAGV,OAAOC,YAAY3L,EAAQlQ,KAAK8b,IAHtB,KAG6BH,IAAIvL,QAAQ,IAAM,IAF3C,CAAC,IAAK,KAAM,KAAM,MAEqCuL,EACzE,CAOA,UAAAjf,CAAWH,GACP,MAAM+b,EAAMha,SAASC,cAAc,OAEnC,OADA+Z,EAAIzmB,YAAc0K,EACX+b,EAAI9Z,SACf,EAIJud,EAAmBhD,eCjUnB,MAAMiD,iBAAiB5hB,EACnB,WAAApS,CAAYO,EAAU,IAClBL,MAAM,CACF2S,UAAW,eACRtS,IAGPkC,KAAKwxB,QAAU1zB,EAAQ0zB,QACvBxxB,KAAKktB,MAAQpvB,EAAQovB,OAAS,UAC9BltB,KAAKyxB,cAAgB3zB,EAAQ2zB,cAC7BzxB,KAAK0xB,iBAAmB5zB,EAAQ4zB,kBAAoB,oBACpD1xB,KAAK2xB,gBAAkB7zB,EAAQ6zB,iBAAmB,OAClD3xB,KAAKwuB,eAA0C,IAA1B1wB,EAAQ0wB,cAC7BxuB,KAAK4xB,WAAkC,IAAtB9zB,EAAQ8zB,UACzB5xB,KAAK6xB,iBAAmB/zB,EAAQg0B,kBAAoB7E,gBACpDjtB,KAAK+xB,SAAW,GAChB/xB,KAAKgyB,gCAAmBtD,IACxB1uB,KAAKiyB,YAAc,IACvB,CAEA,WAAA7R,GACI,MAAO,uDACqCpgB,KAAKktB,yGAEvCltB,KAAK4xB,UAAY,gEAAkE,kCAGjG,CAEA,YAAMluB,GAEF1D,KAAK+xB,eAAiB/xB,KAAKwxB,QAAQjpB,QAG/BvI,KAAK4xB,YACL5xB,KAAKkyB,UAAY,IAAI5D,cAAc,CAC/BxqB,YAAa,QACb4e,YAAa1iB,KAAK0xB,iBAClBnD,WAAYvuB,KAAK2xB,gBACjBnD,cAAexuB,KAAKwuB,gBAExBxuB,KAAKmE,SAASnE,KAAKkyB,WAGnBlyB,KAAKkyB,UAAU7tB,GAAG,eAAgBL,MAAOxG,UAC/BwC,KAAKmyB,kBAAkB30B,KAGzC,CAEA,mBAAMgV,GAEFxS,KAAKoyB,2BAGCpyB,KAAKqyB,kBAGXryB,KAAKsyB,gBACT,CAMA,qBAAMD,GAEF,MAAME,qBAAqBxjB,IAC3B/O,KAAKgyB,aAAa3sB,QAAQwW,GAAK0W,EAAevW,IAAIH,EAAElU,KAGpD,IAAA,MAAWA,KAAM3H,KAAKmuB,SAAU,CAC5B,MAAMC,EAAQpuB,KAAKmuB,SAASxmB,GACvBymB,IAASmE,EAAehjB,IAAI5H,KACjCymB,EAAMld,OAASlR,WACTgK,QAAQwoB,QAAQpE,EAAM3iB,UAAUwP,SAClClI,QAAQC,KAAK,gCAAgCrL,KAAO0S,IAE5D,CAGA,MAAMoY,EAAoBzyB,KAAKiH,QAAQE,cAAc,+BAChDsrB,GAELzyB,KAAKgyB,aAAa3sB,QAASqtB,IACvBD,EAAkBjW,YAAYkW,EAAYzrB,SAC1CyrB,EAAYjnB,QAAO,IAE3B,CAMA,kBAAA2mB,GACSpyB,KAAK+xB,UAAqC,IAAzB/xB,KAAK+xB,SAASjtB,QAEpC9E,KAAK+xB,SAAS1sB,QAAQsD,IACb3I,KAAKgyB,aAAaziB,IAAI5G,EAAQhB,KAC/B3H,KAAK2yB,mBAAmBhqB,IAGpC,CAMA,kBAAAgqB,CAAmBhqB,GACf,GAAI3I,KAAKgyB,aAAaziB,IAAI5G,EAAQhB,IAAK,OAEvC,MAAMwlB,EAAgBxkB,EAAQiqB,QAAUjqB,EAAQiqB,OAAOjrB,KAAO3H,KAAKyxB,cAE7DiB,EAAc,IAAI1yB,KAAK6xB,iBAAiB,CAC1ClpB,UACAukB,MAAOltB,KAAKktB,MACZC,kBAMJ,OAHAntB,KAAKmE,SAASuuB,GACd1yB,KAAKgyB,aAAa7X,IAAIxR,EAAQhB,GAAI+qB,GAE3BA,CACX,CAOA,UAAAG,CAAWlqB,EAASmqB,GAAS,GACzB,GAAI9yB,KAAKgyB,aAAaziB,IAAI5G,EAAQhB,IAAK,OAEvC,MAAM+qB,EAAc1yB,KAAK2yB,mBAAmBhqB,GAG5C,GAAI3I,KAAKiT,YAAa,CAClB,MAAMwf,EAAoBzyB,KAAKiH,QAAQE,cAAc,+BACjDsrB,IACAA,EAAkBjW,YAAYkW,EAAYzrB,SAC1CyrB,EAAYjnB,QAAO,GAE3B,CAEIqnB,GACA9yB,KAAKsyB,gBAEb,CAOA,uBAAMH,CAAkB30B,GACpB,IAEI,GAAIA,EAAKsU,MAAQtU,EAAKsU,KAAKzC,gBACFrP,KAAKwxB,QAAQuB,QAAQ,CACtCjhB,KAAMtU,EAAKsU,KACXyd,MAAO/xB,EAAK+xB,OAAS/xB,EAAK+xB,MAAMzqB,OAAS,EAAI,CAACtH,EAAK+xB,MAAM,IAAM,MAGvDjwB,QACR,MAAM,IAAI8a,MAAM,0BAOxB,IAAA,IAAS8W,EAFW1zB,EAAKsU,MAAQtU,EAAKsU,KAAKzC,QAAU7R,EAAK+xB,OAAOzqB,OAAS,EAAK,EAAI,EAE1DosB,GAAK1zB,EAAK+xB,OAAOzqB,QAAU,GAAIosB,IAAK,CACzD,MAAMlR,EAAOxiB,EAAK+xB,MAAM2B,UACHlxB,KAAKwxB,QAAQuB,QAAQ,CACtCjhB,KAAM,GACNyd,MAAO,CAACvP,MAGA1gB,SACRyT,QAAQvK,MAAM,yBAA0BwX,EAEhD,CAGAhgB,KAAK+xB,eAAiB/xB,KAAKwxB,QAAQjpB,QAGnCvI,KAAK+xB,SAAS1sB,QAAQsD,IACb3I,KAAKgyB,aAAaziB,IAAI5G,EAAQhB,KAC/B3H,KAAK6yB,WAAWlqB,GAAS,KAKjC3I,KAAKkyB,UAAUnB,YAEnB,OAASvoB,GACLuK,QAAQvK,MAAM,0BAA2BA,GAEzCxI,KAAKkyB,UAAUxB,SAAQ,GACvB,IACI1wB,KAAKiG,SAASyE,MAAMlC,MAAM,yBAC9B,OAAS7C,GAET,CACJ,CACJ,CAOA,YAAAqtB,CAAalhB,EAAO,eAChB,MAAM2B,EAAYzT,KAAKiH,SAASE,cAAc,+BACzCsM,IAEAzT,KAAKiyB,cACNjyB,KAAKiyB,YAAcpe,SAASC,cAAc,OAC1C9T,KAAKiyB,YAAY7hB,UAAY,gBAC7BpQ,KAAKiyB,YAAYle,UAAY,2XAS7BN,EAAU+I,YAAYxc,KAAKiyB,aAE3BjyB,KAAKizB,eAAiB3uB,KAAKwJ,MAC3B9N,KAAKkzB,kBAAoBC,YAAY,KACjC,MAAMC,EAAU7d,KAAKsI,OAAOvZ,KAAKwJ,MAAQ9N,KAAKizB,gBAAkB,KAC1DI,EAAO9d,KAAKsI,MAAMuV,EAAU,IAC5BE,EAAOF,EAAU,GACjBG,EAAUvzB,KAAKiyB,aAAa9qB,cAAc,wBAC5CosB,IACAA,EAAQnsB,YAAcisB,EAAO,EACvB,GAAGA,MAASvsB,OAAOwsB,GAAMnmB,SAAS,EAAG,QACrC,GAAGmmB,OAEd,MAGPtzB,KAAKiyB,YAAY9qB,cAAc,uBAAuBC,YAAc0K,EACpE9R,KAAKsyB,iBACT,CAKA,YAAAkB,GACQxzB,KAAKkzB,oBACLO,cAAczzB,KAAKkzB,mBACnBlzB,KAAKkzB,kBAAoB,MAEzBlzB,KAAKiyB,cACLjyB,KAAKiyB,YAAY9d,SACjBnU,KAAKiyB,YAAc,KAE3B,CAMA,eAAAyB,CAAgB9C,GACR5wB,KAAKkyB,WAAWvB,YAChB3wB,KAAKkyB,UAAUvB,WAAWC,EAElC,CAKA,cAAA0B,GACI,MAAM7e,EAAYzT,KAAKiH,QAAQE,cAAc,kBACzCsM,GACAkgB,sBAAsB,KAClBlgB,EAAUmgB,UAAYngB,EAAUwd,cAG5C,CAKA,aAAA4C,GACI7zB,KAAKgyB,aAAa3sB,QAAS4L,WAEhBjR,KAAKmuB,SAASld,EAAKtJ,IAC1BsJ,EAAKtH,YAET3J,KAAKgyB,aAAahB,QAClBhxB,KAAK+xB,SAAW,GAEhB,MAAMte,EAAYzT,KAAKiH,SAASE,cAAc,+BAC1CsM,IACAA,EAAUM,UAAY,GAE9B,CAKA,aAAMxK,GACFvJ,KAAK6zB,gBACL7zB,KAAK+xB,eAAiB/xB,KAAKwxB,QAAQjpB,QACnCvI,KAAKoyB,qBAEDpyB,KAAKiT,oBACCjT,KAAKqyB,kBACXryB,KAAKsyB,iBAEb"}
1
+ {"version":3,"file":"ChatView-CPaBPG5C.js","sources":["../../src/core/models/Metrics.js","../../src/core/models/System.js","../../src/core/pages/TablePage.js","../../src/core/views/list/grouping.js","../../src/core/views/navigation/SideNavView.js","../../src/core/views/data/DetailView.js","../../src/core/views/data/MetricCard.js","../../src/core/views/data/StatusPanel.js","../../src/core/views/data/Timeline.js","../../src/core/views/data/KnownFieldsCard.js","../../src/core/views/data/FilePreviewView.js","../../src/core/models/ShortLink.js","../../src/core/views/data/FileView.js","../../src/core/views/chat/ChatMessageView.js","../../src/core/views/chat/ChatInputView.js","../../src/core/views/chat/ChatView.js"],"sourcesContent":["import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\nclass MetricsPermission extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/metrics/permissions',\n id_key: 'account'\n });\n }\n}\n\nclass MetricsPermissionList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: MetricsPermission,\n endpoint: '/api/metrics/permissions',\n ...options,\n });\n }\n\n}\n\nconst MetricsForms = {\n edit: {\n title: 'Edit Metrics Permissions',\n fields: [\n { name: 'account', type: 'text', label: 'Account', columns:12 },\n { name: 'view_permissions', type: 'tags', label: 'View Permissions', help: 'Enter permissions or \"public\"', columns:12 },\n { name: 'write_permissions', type: 'tags', label: 'Write Permissions', help: 'Enter permissions', columns:12 },\n ]\n }\n};\n\nexport { MetricsPermission, MetricsPermissionList, MetricsForms };\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/* =========================\n * GeoLocatedIP\n * ========================= */\n\nconst GeoIPForms = {\n editLocation: {\n title: 'Edit Location',\n size: 'lg',\n fields: [\n { name: 'ip_address', label: 'IP Address', type: 'text', required: true, readonly: true, cols: 6 },\n { name: 'subnet', label: 'Subnet', type: 'text', cols: 6 },\n { name: 'country_name', label: 'Country', type: 'text', cols: 6 },\n { name: 'country_code', label: 'Country Code', type: 'text', cols: 6 },\n { name: 'region', label: 'Region', type: 'text', cols: 6 },\n { name: 'city', label: 'City', type: 'text', cols: 6 },\n { name: 'postal_code', label: 'Postal Code', type: 'text', cols: 6 },\n { name: 'timezone', label: 'Timezone', type: 'text', cols: 6 },\n { name: 'latitude', label: 'Latitude', type: 'number', step: 'any', cols: 6 },\n { name: 'longitude', label: 'Longitude', type: 'number', step: 'any', cols: 6 },\n ]\n },\n editSecurity: {\n title: 'Edit Security',\n size: 'md',\n fields: [\n { \n name: 'threat_level', \n label: 'Threat Level', \n type: 'select', \n cols: 12,\n options: [\n { value: '', label: 'None' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'critical', label: 'Critical' }\n ]\n },\n { name: 'is_threat', label: 'Threat', type: 'switch', cols: 6 },\n { name: 'is_suspicious', label: 'Suspicious', type: 'switch', cols: 6 },\n { name: 'is_known_attacker', label: 'Known Attacker', type: 'switch', cols: 6 },\n { name: 'is_known_abuser', label: 'Known Abuser', type: 'switch', cols: 6 },\n { name: 'risk_score', label: 'Risk Score', type: 'number', cols: 6 },\n { name: 'is_tor', label: 'TOR Exit Node', type: 'switch', cols: 6 },\n { name: 'is_vpn', label: 'VPN', type: 'switch', cols: 6 },\n { name: 'is_proxy', label: 'Proxy', type: 'switch', cols: 6 },\n { name: 'is_cloud', label: 'Cloud Provider', type: 'switch', cols: 6 },\n { name: 'is_datacenter', label: 'Datacenter', type: 'switch', cols: 6 }\n ]\n },\n editNetwork: {\n title: 'Edit Network',\n size: 'md',\n fields: [\n { name: 'asn', label: 'ASN', type: 'text', cols: 6 },\n { name: 'asn_org', label: 'ASN Organization', type: 'text', cols: 6 },\n { name: 'isp', label: 'ISP', type: 'text', cols: 12 },\n { name: 'connection_type', label: 'Connection Type', type: 'text', cols: 6 },\n { name: 'provider', label: 'Provider', type: 'text', cols: 6 },\n { name: 'is_mobile', label: 'Mobile Connection', type: 'switch', cols: 6 },\n { name: 'mobile_carrier', label: 'Mobile Carrier', type: 'text', cols: 6 },\n { name: 'last_seen', label: 'Last Seen', type: 'datetime', cols: 12 }\n ]\n }\n};\n\nclass GeoLocatedIP extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/system/geoip',\n });\n }\n\n static async lookup(ip) {\n const model = new GeoLocatedIP();\n const resp = await model.rest.GET('/api/system/geoip/lookup', { ip });\n if (resp.success && resp.data && resp.data.data) {\n return new GeoLocatedIP(resp.data.data);\n }\n return null;\n }\n}\n\n// Attach forms to model (use Location as default EDIT_FORM for TableView)\nGeoLocatedIP.EDIT_FORM = GeoIPForms.editLocation;\nGeoLocatedIP.EDIT_LOCATION_FORM = GeoIPForms.editLocation;\nGeoLocatedIP.EDIT_SECURITY_FORM = GeoIPForms.editSecurity;\nGeoLocatedIP.EDIT_NETWORK_FORM = GeoIPForms.editNetwork;\n\nclass GeoLocatedIPList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: GeoLocatedIP,\n endpoint: '/api/system/geoip',\n ...options,\n });\n }\n}\n\nexport { GeoLocatedIP, GeoLocatedIPList };\n","/**\n * TablePage - Page component that manages a TableView with URL parameter synchronization\n *\n * A clean, simplified implementation using the new TableView component.\n * Automatically syncs pagination, sorting, and filtering with URL parameters.\n *\n * @example\n * const usersPage = new TablePage({\n * pageName: 'users',\n * title: 'User Management',\n * collection: userCollection,\n * columns: [\n * { key: 'name', label: 'Name', sortable: true },\n * { key: 'email', label: 'Email' },\n * { key: 'role', label: 'Role', type: 'badge' }\n * ],\n * actions: ['view', 'edit', 'delete']\n * });\n */\n\nimport Page from '@core/Page.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport TableView from '@core/views/table/TableView.js';\nimport Collection from '@core/Collection.js';\nimport { parseFilterKey } from '@core/utils/DjangoLookups.js';\n\nclass TablePage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n pageName: options.pageName || options.name || 'table'\n });\n\n // Page configuration\n this.title = options.title || this.pageName;\n this.description = options.description || '';\n\n // Collection setup\n this.Collection = options.Collection || null;\n this.collection = options.collection || null;\n\n // our default collection query\n this.defaultQuery = options.defaultQuery || {};\n\n // Group field configuration - defaults to \"group\"\n this.groupField = options.groupField || 'group';\n\n // Store configuration for TableView\n // Map legacy property names to new ones\n this.tableViewConfig = {\n // Core table properties\n columns: options.columns || [],\n actions: options.actions || null,\n contextMenu: options.contextMenu || null,\n batchActions: options.batchActions || null,\n batchBarLocation: options.batchBarLocation || 'top',\n clickAction: options.clickAction || 'view',\n // Map legacy form properties to new names\n addForm: options.addForm || options.formFields || options.formCreate,\n editForm: options.editForm || options.formEdit || options.formFields,\n\n // Model operation configurations\n itemView: options.itemView || options.itemViewClass,\n deleteTemplate: options.deleteTemplate,\n formDialogConfig: options.formDialogConfig,\n viewDialogOptions: options.viewDialogOptions,\n\n // Features\n searchable: options.searchable !== false,\n sortable: options.sortable !== false,\n filterable: options.filterable !== false,\n paginated: options.paginated !== false,\n\n // Selection mode\n selectionMode: options.selectionMode || (options.selectable ? 'multiple' : 'none'),\n\n // Filter configuration\n filters: options.filters || options.additionalFilters || [],\n hideActivePills: options.hideActivePills || false,\n hideActivePillNames: options.hideActivePillNames || [],\n searchPlacement: options.searchPlacement || 'toolbar',\n\n // Toolbar day-range segment + chronological grouping. ListView /\n // TableView own the implementation; TablePage just forwards.\n dayRangeFilter: options.dayRangeFilter,\n groupBy: options.groupBy,\n groupHeaderTemplate: options.groupHeaderTemplate,\n groupHeaderLabel: options.groupHeaderLabel,\n groupHeaderStyle: options.groupHeaderStyle,\n\n // Row stripe — same forwarding pattern.\n rowStripe: options.rowStripe,\n\n // Display options for the HTML table element\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false,\n ...options.tableOptions\n },\n\n // Additional options\n emptyMessage: options.emptyMessage || 'No data available',\n searchPlaceholder: options.searchPlaceholder || 'Search...',\n showAdd: options.showAdd !== false,\n showExport: options.showExport !== false,\n\n // Custom handlers\n onItemView: options.onItemView,\n onItemEdit: options.onItemEdit,\n onItemDelete: options.onItemDelete,\n onAdd: options.onAdd,\n onExport: options.onExport,\n\n // Override with tableViewOptions if provided\n ...options.tableViewOptions\n };\n\n // URL synchronization\n this.urlSyncEnabled = options.urlSyncEnabled !== false;\n\n // Status tracking\n this.lastUpdated = null;\n this.isLoading = false;\n\n // Set up template\n this.template = options.template || this.buildTemplate();\n }\n\n /**\n * Build the page template\n */\n buildTemplate() {\n return `\n <div class=\"table-page-container\">\n\n <div class=\"table-container\" data-container=\"table\"></div>\n\n {{#showStatus}}\n <div class=\"table-status-bar table-status-top\">\n <div class=\"status-info\">\n <div class=\"d-flex justify-content-between w-100\">\n <span class=\"text-muted\">\n <i class=\"bi bi-clock me-1\"></i>\n Last updated: <span data-status=\"last-updated\">{{lastUpdated}}</span>\n </span>\n <span class=\"text-muted\">\n <i class=\"bi bi-list-ol me-1\"></i>\n Total records: <span data-status=\"record-count\">0</span>\n </span>\n </div>\n </div>\n </div>\n {{/showStatus}}\n\n </div>\n `;\n }\n\n /**\n * Initialize the page\n */\n async onInit() {\n await super.onInit();\n\n // Create collection if needed\n if (!this.collection) {\n if (this.Collection) {\n this.collection = new this.Collection();\n } else {\n this.collection = new Collection();\n }\n }\n\n // Apply URL query parameters to collection\n this.applyQueryToCollection();\n\n // Create TableView instance with all configuration\n // Intercept item view to add deep-link URL support\n this.tableView = new TableView({\n collection: this.collection,\n containerId: 'table',\n fetchOnMount: true,\n ...this.tableViewConfig,\n onItemView: async (model, event) => {\n // Allow subclass onItemView to fully override\n if (this.tableViewConfig.onItemView) {\n return this.tableViewConfig.onItemView(model, event);\n }\n await this.showItemDialog(model);\n }\n });\n\n // Add as child view\n this.addChild(this.tableView);\n\n // Set up event listeners\n this.setupEventListeners();\n }\n\n /**\n * Set up event listeners\n */\n setupEventListeners() {\n // Listen for collection changes to sync URL\n if (this.urlSyncEnabled && this.collection) {\n // Sync URL when collection params change\n this.collection.on('fetch:start', () => {\n this.isLoading = true;\n });\n\n this.collection.on('fetch:end', () => {\n this.isLoading = false;\n this.lastUpdated = new Date().toLocaleTimeString();\n this.updateStatusDisplay();\n });\n }\n\n // Listen for params-changed event from TableView to sync URL\n this.tableView.on('params-changed', () => {\n if (this.urlSyncEnabled) {\n this.syncUrl();\n }\n });\n\n // // Listen for table events (these also emit params-changed, but keep for backwards compatibility)\n // this.tableView.on('table:search', ({ searchTerm }) => {\n // // params-changed will handle URL sync\n // });\n\n // this.tableView.on('table:sort', ({ field }) => {\n // // params-changed will handle URL sync\n // });\n\n // this.tableView.on('table:page', ({ page }) => {\n // // params-changed will handle URL sync\n // });\n\n // Filter:edit handling lives entirely on ListView/TableView itself\n // (it owns the edit-modal flow). We do NOT listen for `filter:edit`\n // here anymore — doing so opened a second racing Modal.form. Filter\n // changes still arrive via the `params-changed` event, which drives\n // the URL sync below.\n\n // Row action events\n this.tableView.on('row:view', async ({ model }) => {\n if (this.onItemView) {\n await this.onItemView(model);\n }\n });\n\n this.tableView.on('row:edit', async ({ model }) => {\n if (this.onItemEdit) {\n await this.onItemEdit(model);\n }\n });\n\n this.tableView.on('row:delete', async ({ model }) => {\n if (this.onItemDelete) {\n await this.onItemDelete(model);\n }\n });\n\n // Table action events\n // Note: TableView will call options.onAdd if provided, but we still listen to the event\n // for backwards compatibility and to support event-based patterns\n this.tableView.on('table:add', async ({ event }) => {\n // The handler was already called by TableView if options.onAdd is set,\n // but we keep this listener for external code that might listen to 'table:add'\n // We don't call this.tableViewConfig.onAdd here to avoid double execution\n });\n\n this.tableView.on('table:export', async ({ data }) => {\n if (this.tableViewConfig.onExport) {\n await this.tableViewConfig.onExport(data);\n }\n });\n }\n\n /**\n * Apply URL query parameters to collection\n */\n applyQueryToCollection() {\n const params = {};\n const query = { ...this.defaultQuery, ...this.query };\n if (!query || Object.keys(query).length === 0) {\n return;\n }\n // Pagination\n if (query.start !== undefined) params.start = parseInt(query.start) || 0;\n if (query.size !== undefined) params.size = parseInt(query.size) || 10;\n\n // Sorting\n if (query.sort !== undefined) params.sort = query.sort;\n\n // Search\n if (query.search !== undefined) params.search = query.search;\n\n // Process all other params as potential filters\n const reservedParams = ['start', 'size', 'sort', 'search', 'page', '_item'];\n Object.entries(query).forEach(([key, value]) => {\n if (!reservedParams.includes(key) && value !== undefined && value !== '') {\n // Parse value if it looks like JSON\n if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {\n try {\n params[key] = JSON.parse(value);\n } catch (e) {\n params[key] = value;\n }\n } else {\n params[key] = value;\n }\n }\n });\n\n // Update collection params\n if (Object.keys(params).length > 0) {\n // Deduplicate simple + __in filters (prefer __in / not_in when both are present)\n Object.keys(params).forEach(key => {\n const { field, lookup } = parseFilterKey(key);\n if ((lookup === 'in' || lookup === 'not_in') && params.hasOwnProperty(field)) {\n delete params[field];\n }\n });\n\n this.collection.setParams({\n ...this.collection.params,\n ...params\n });\n }\n }\n\n /**\n * Sync URL with current table state\n */\n syncUrl(force = true) {\n if (!this.urlSyncEnabled || !this.collection || !this.getApp()?.router) {\n return;\n }\n\n // Get current URL params\n const currentUrl = new URL(window.location);\n const currentParams = {};\n for (const [key, value] of currentUrl.searchParams) {\n if (key !== 'page') {\n currentParams[key] = value;\n }\n }\n\n // Get desired params from collection\n const desiredParams = {};\n const collectionParams = this.collection.params || {};\n\n // Only include non-default values\n if (collectionParams.start) {\n desiredParams.start = collectionParams.start;\n }\n if (collectionParams.size) {\n desiredParams.size = collectionParams.size;\n }\n if (collectionParams.sort) {\n desiredParams.sort = collectionParams.sort;\n }\n if (collectionParams.search) {\n desiredParams.search = collectionParams.search;\n }\n\n // Include other filters\n Object.entries(collectionParams).forEach(([key, value]) => {\n if (!['start', 'size', 'sort', 'search'].includes(key) && value !== undefined && value !== '') {\n // Stringify complex values for URL\n if (typeof value === 'object') {\n desiredParams[key] = JSON.stringify(value);\n } else {\n desiredParams[key] = value;\n }\n }\n });\n\n // Check if there are any changes\n const hasChanges =\n Object.keys(desiredParams).some(key =>\n String(currentParams[key] || '') !== String(desiredParams[key] || '')\n ) ||\n Object.keys(currentParams).some(key =>\n !(key in desiredParams)\n );\n\n // Preserve _item param if currently set (deep-link, not a collection param)\n if (this.query._item) {\n desiredParams._item = this.query._item;\n }\n\n this.query = desiredParams;\n if (!hasChanges && !force) return;\n\n // Update URL\n this.updateBrowserUrl(desiredParams, true, false);\n }\n\n /**\n * Update status display\n */\n updateStatusDisplay() {\n if (!this.element) return;\n\n // Update last updated time\n const updatedElement = this.element.querySelector('[data-status=\"last-updated\"]');\n if (updatedElement) {\n updatedElement.textContent = this.lastUpdated || 'Never';\n }\n\n // Update record count\n const countElement = this.element.querySelector('[data-status=\"record-count\"]');\n if (countElement && this.collection) {\n const count = this.collection.meta?.count || this.collection.length();\n countElement.textContent = count;\n }\n }\n\n /**\n * Called when entering this page\n */\n async onEnter() {\n await super.onEnter();\n\n if (this.options.requiresGroup && !this.query[this.groupField] && this.getApp().activeGroup) {\n this.query[this.groupField] = this.getApp().activeGroup.id;\n }\n\n this.applyQueryToCollection();\n\n // Ensure filter pills are shown if there are active filters from URL\n if (this.tableView && this.tableView.element) {\n setTimeout(() => {\n this.tableView.updateFilterPills();\n this.tableView.updateSortIcons();\n }, 100);\n }\n\n // Deep-link: auto-open item dialog if _item param is present\n if (this.query._item) {\n this._openDeepLinkedItem(this.query._item);\n }\n }\n\n /**\n * Open a deep-linked item dialog by fetching the model and showing it\n */\n async _openDeepLinkedItem(itemId) {\n try {\n const model = await this.collection.fetchOne(itemId);\n if (model) {\n await this.showItemDialog(model);\n }\n } catch (e) {\n // Item not found or fetch failed — silently clear the param\n this._clearItemParam();\n }\n }\n\n /**\n * Show the item view dialog with deep-link URL support\n */\n async showItemDialog(model) {\n // Update URL with _item param\n this._setItemParam(model.id);\n\n // Fetch latest model data before showing the dialog\n if (this.tableView.fetchOnView) {\n try {\n Modal.loading();\n await model.fetch();\n } catch (error) {\n Modal.hideLoading(true);\n Modal.showError(error?.data?.error || error?.message || 'Failed to load item details');\n this._clearItemParam();\n return;\n } finally {\n Modal.hideLoading(true);\n }\n }\n\n const ViewClass = this.tableView.getItemViewClass(model);\n\n if (ViewClass) {\n const viewInstance = new ViewClass({ model, collection: this.collection });\n await Modal.dialog({\n header: false,\n body: viewInstance,\n size: 'lg',\n centered: false,\n ...this.tableView.getFormDialogConfig(this.tableView.getModelClass(model)),\n // View class declares its own modal presentation via static DIALOG_OPTIONS.\n ...ViewClass.DIALOG_OPTIONS,\n ...this.tableView.viewDialogOptions\n });\n } else {\n await Modal.data({\n title: `View ${this.tableView.getModelName(model)} #${model.id}`,\n model\n });\n }\n\n // Dialog closed — remove _item from URL\n this._clearItemParam();\n }\n\n /**\n * Add _item param to URL\n */\n _setItemParam(itemId) {\n const params = { ...this.query, _item: itemId };\n this.query = params;\n this.updateBrowserUrl(params, true, false);\n }\n\n /**\n * Remove _item param from URL\n */\n _clearItemParam() {\n delete this.query._item;\n this.updateBrowserUrl(this.query, true, false);\n }\n\n /**\n * Public method to refresh the table\n */\n async refresh() {\n await this.tableView.refresh();\n }\n\n /**\n * Public method to get selected items\n */\n getSelectedItems() {\n return this.tableView.getSelectedItems();\n }\n\n /**\n * Public method to clear selection\n */\n clearSelection() {\n this.tableView.clearSelection();\n }\n\n /**\n * Batch-action helper — encapsulates the confirm → save/destroy → toast →\n * refresh pattern shared by every admin batch handler.\n *\n * Pass one of three modes:\n * - { field, value, label, message? } → save({ [field]: value }) on each\n * - { destroy: true, label, message? } → destroy() each\n * - { handler: async (model) => …, label, … } → run handler(model) per item\n *\n * Behavior:\n * - Resolves to 0 (no-op) when the selection is empty.\n * - Confirms via Modal.confirm unless `confirm: false` is passed.\n * - Runs items in parallel via Promise.allSettled so one failure\n * doesn't abort the rest.\n * - Surfaces a ToastService message — success-only or \"N succeeded,\n * M failed\" — based on the settled outcomes.\n * - Always clears selection and refreshes the table on a non-cancel\n * path so the user sees the partial state on failure.\n *\n * @param {object} options\n * @param {string} [options.field] - Field name to set on each model (save mode).\n * @param {*} [options.value] - Value to assign when in save mode.\n * @param {boolean}[options.destroy=false] - Destroy each model instead of saving.\n * @param {Function}[options.handler] - Custom async (model) => * handler.\n * @param {string} [options.label='Action'] - Verb used in the confirm + toast.\n * @param {string} [options.message] - Override the confirm message.\n * @param {boolean}[options.confirm=true] - Skip the confirm prompt when false.\n * @returns {Promise<number>} The count of successful operations.\n */\n async batchAction({\n field,\n value,\n destroy = false,\n handler = null,\n label = 'Action',\n message,\n confirm = true\n } = {}) {\n const items = this.tableView ? this.tableView.getSelectedItems() : [];\n if (!items.length) return 0;\n\n if (confirm) {\n const ok = await Modal.confirm(\n message || `${label} ${items.length} item(s)?`\n );\n if (!ok) return 0;\n }\n\n const results = await Promise.allSettled(items.map(({ model }) => {\n if (handler) return handler(model);\n if (destroy) return model.destroy();\n return model.save({ [field]: value });\n }));\n\n const successes = results.filter(r => r.status === 'fulfilled').length;\n const failures = results.length - successes;\n\n const app = this.getApp ? this.getApp() : null;\n if (failures === 0) {\n app?.toast?.success?.(`${label}: ${successes} item(s) updated`);\n } else if (successes === 0) {\n app?.toast?.error?.(`${label} failed for all ${failures} item(s)`);\n } else {\n app?.toast?.warning?.(\n `${label}: ${successes} succeeded, ${failures} failed`\n );\n }\n\n this.tableView.clearSelection();\n await this.tableView.refresh();\n return successes;\n }\n\n /**\n * Handle filter edit dialog\n */\n async handleFilterEdit(filterKey) {\n const filterConfig = this.tableView.getAllAvailableFilters().find(f => f.key === filterKey);\n const currentValue = this.collection.params[filterKey];\n\n if (!filterConfig) return;\n\n // Build form field for the filter\n const field = {\n name: 'filter_value',\n label: filterConfig.label || filterKey,\n value: currentValue,\n ...filterConfig.config\n };\n\n const result = await Modal.form({\n title: `Edit ${field.label} Filter`,\n size: 'md',\n fields: [field]\n });\n\n if (result && result.filter_value !== undefined) {\n this.tableView.setFilter(filterKey, result.filter_value);\n\n if (this.collection.restEnabled) {\n this.collection.fetch();\n }\n await this.tableView.render();\n this.syncUrl();\n }\n }\n\n /**\n * Clear all filters\n */\n clearAllFilters() {\n if (!this.collection) return;\n\n // Keep only pagination and sort params\n const { start, size, sort } = this.collection.params;\n this.collection.params = { start, size };\n if (sort) this.collection.params.sort = sort;\n\n this.syncUrl();\n\n if (this.collection.restEnabled) {\n this.collection.fetch();\n } else {\n this.tableView.render();\n }\n }\n\n async onGroupChange(group) {\n if (!group || !this.collection || !this.options.requiresGroup) return;\n this.query[this.groupField] = group.id;\n this.applyQueryToCollection();\n if (this.collection && this.collection.restEnabled) {\n this.collection.fetch();\n }\n }\n\n /**\n * Cleanup on destroy\n */\n async onBeforeDestroy() {\n // Remove event listeners\n if (this.collection) {\n this.collection.off('fetch:start');\n this.collection.off('fetch:end');\n }\n\n if (this.tableView) {\n this.tableView.off('params-changed');\n this.tableView.off('table:search');\n this.tableView.off('table:sort');\n this.tableView.off('table:page');\n this.tableView.off('row:view');\n this.tableView.off('row:edit');\n this.tableView.off('row:delete');\n this.tableView.off('table:add');\n this.tableView.off('table:export');\n }\n\n await super.onBeforeDestroy();\n }\n\n /**\n * Show/hide status display\n */\n get showStatus() {\n return this.options.showStatus === true;\n }\n\n /**\n * Static factory method\n */\n static create(options = {}) {\n return new this(options);\n }\n}\n\nexport default TablePage;\n","/**\n * Built-in `groupBy*` helpers for ListView.\n *\n * Each helper returns an object `{ groupBy, groupHeaderLabel }` ready to\n * spread into the ListView constructor:\n *\n * import { groupByDay } from '@core/views/list/grouping.js';\n *\n * new ListView({\n * collection: loginEvents,\n * itemTemplate: '...',\n * ...groupByDay('created')\n * });\n *\n * Helpers always produce a stable bucket key (deterministic equality\n * regardless of input format) and a separate display formatter for the\n * `{{key}}` slot in the header template.\n *\n * Shipped helpers:\n * - `groupByDay` — chronological feeds (Today / Yesterday / May 5 / May 5, 2025)\n * - `groupByField` — categorical bucketing with explicit label maps\n * - `groupByRecency` — six fixed buckets (Today / Yesterday / This week / This month / Earlier this year / Older)\n * - `groupByBoolean` — binary on/off split with consumer-supplied labels\n *\n * Additional helpers (`groupByMonth`, `groupByYear`, `groupByLetter`, etc.)\n * are tracked in `planning/requests/listview-grouping-helpers.md` and ship\n * when a real consumer asks.\n */\n\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\n/**\n * Resolve the raw value off a model — accept either a field-name string\n * (resolved via `model.get(field)`) or an accessor function. Shared by\n * every helper in this module — do not duplicate when adding new ones.\n * @private\n */\nfunction resolveAccessor(fieldOrAccessor) {\n if (typeof fieldOrAccessor === 'function') return fieldOrAccessor;\n if (typeof fieldOrAccessor === 'string') {\n return (model) => (model && typeof model.get === 'function' ? model.get(fieldOrAccessor) : null);\n }\n throw new TypeError('grouping helper expects a field name string or an accessor function');\n}\n\n/**\n * Convert a raw date-ish value (epoch seconds / epoch ms / ISO string /\n * Date instance) into a JavaScript Date in local time, or null when the\n * value is missing / unparseable. Uses `dataFormatter.normalizeEpoch`\n * (matches the existing AssistantConversationListView convention).\n * Shared by all date-bucket helpers — do not duplicate when adding new ones.\n * @private\n */\nfunction toDate(raw) {\n if (raw == null || raw === '') return null;\n try {\n const ms = dataFormatter.normalizeEpoch(raw);\n if (ms === '' || ms == null || Number.isNaN(ms)) return null;\n const d = ms instanceof Date ? ms : new Date(ms);\n return Number.isNaN(d.getTime()) ? null : d;\n } catch (_err) {\n return null;\n }\n}\n\n/**\n * Format a YYYY-MM-DD bucket key into a human display label, relative to\n * the local \"today\" / \"yesterday\" anchor:\n * - 2026-05-09 → \"Today\" (when today is 2026-05-09)\n * - 2026-05-08 → \"Yesterday\"\n * - 2026-04-25 → \"Apr 25\" (current year)\n * - 2025-12-19 → \"Dec 19, 2025\" (prior year)\n * @private\n */\nfunction formatDayLabel(bucketKey) {\n if (!bucketKey || typeof bucketKey !== 'string') return '';\n\n const parts = bucketKey.split('-');\n if (parts.length !== 3) return bucketKey;\n const year = Number(parts[0]);\n const month = Number(parts[1]);\n const day = Number(parts[2]);\n if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return bucketKey;\n\n const now = new Date();\n const todayKey = isoDayKey(now);\n const yesterdayDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n const yesterdayKey = isoDayKey(yesterdayDate);\n\n if (bucketKey === todayKey) return 'Today';\n if (bucketKey === yesterdayKey) return 'Yesterday';\n\n const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n const monthLabel = months[month - 1] || '';\n if (year === now.getFullYear()) {\n return `${monthLabel} ${day}`;\n }\n return `${monthLabel} ${day}, ${year}`;\n}\n\n/**\n * Build a stable YYYY-MM-DD bucket key from a Date — local time, so the\n * bucket aligns with the user's idea of \"the day this happened\" rather\n * than UTC.\n * @private\n */\nfunction isoDayKey(date) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\n/**\n * Day-bucketing helper for chronological feeds.\n *\n * Buckets each model into its local-day ISO key (e.g. `'2026-05-09'`).\n * Stable keys make equality deterministic regardless of input format\n * (epoch / ISO / Date). The label formatter renders 'Today' / 'Yesterday'\n * / 'May 5' / 'May 5, 2025' depending on how recent the bucket is.\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: loginEvents,\n * itemTemplate: '...',\n * ...groupByDay('created')\n * });\n *\n * @example\n * // Custom accessor for fallback fields\n * ...groupByDay((m) => m.get('updated') || m.get('created'))\n */\nexport function groupByDay(fieldOrAccessor) {\n const access = resolveAccessor(fieldOrAccessor);\n return {\n groupBy: (model) => {\n const date = toDate(access(model));\n return date ? isoDayKey(date) : null;\n },\n groupHeaderLabel: (key) => formatDayLabel(key)\n };\n}\n\n/**\n * Categorical-field bucketing helper.\n *\n * Buckets each model on the raw value at `fieldOrAccessor`, coerced to a\n * string (`String(raw)`) for deterministic equality. Three optional\n * formatting controls drive the displayed header:\n *\n * - `labels` — explicit map: `{ active: 'Active', resolved: 'Resolved' }`\n * - `format` — fallback transform applied when no `labels` entry matches\n * - `fallback` — bucket key used when raw is `null` / `undefined` / `''`\n * (omitted by default → ungrouped tail)\n *\n * `labels` wins over `format` when both are passed.\n *\n * **Falsy-but-stringable raw values** (`0`, `false`) coerce to non-empty\n * strings (`'0'`, `'false'`) and DO produce buckets — only `null` /\n * `undefined` / `''` go to the fallback / null-bucket path. If you want\n * `0` collapsed into the ungrouped tail, pass a custom accessor that\n * returns `null` for those values.\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @param {object} [opts]\n * @param {Object<string,string>} [opts.labels] - Explicit `rawKey → display` map\n * @param {Function} [opts.format] - `(rawKey) => display` fallback formatter\n * @param {string} [opts.fallback] - Bucket key when raw is null/undefined/''\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: incidents,\n * itemTemplate: '...',\n * ...groupByField('status', {\n * labels: { active: 'Active', resolved: 'Resolved', closed: 'Closed' },\n * fallback: 'Other'\n * })\n * });\n */\nexport function groupByField(fieldOrAccessor, opts = {}) {\n const access = resolveAccessor(fieldOrAccessor);\n const { labels, format, fallback } = opts;\n\n return {\n groupBy: (model) => {\n const raw = access(model);\n if (raw == null || raw === '') {\n return fallback != null ? String(fallback) : null;\n }\n return String(raw);\n },\n groupHeaderLabel: (key) => {\n if (labels && Object.prototype.hasOwnProperty.call(labels, key)) {\n return labels[key];\n }\n if (typeof format === 'function') {\n return format(key);\n }\n return key;\n }\n };\n}\n\n// Sort-ordered bucket keys so descending-by-date sort renders buckets in\n// natural reading order (Today on top, Older on bottom).\nconst RECENCY_LABELS = {\n 'recency-0-today': 'Today',\n 'recency-1-yesterday': 'Yesterday',\n 'recency-2-this-week': 'This week',\n 'recency-3-this-month': 'This month',\n 'recency-4-this-year': 'Earlier this year',\n 'recency-5-older': 'Older'\n};\n\n/**\n * Map a Date into one of six fixed recency buckets relative to local \"now\".\n * @private\n */\nfunction recencyBucketKey(date) {\n if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;\n\n const now = new Date();\n const dateKey = isoDayKey(date);\n const todayKey = isoDayKey(now);\n if (dateKey === todayKey) return 'recency-0-today';\n\n const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n if (dateKey === isoDayKey(yesterday)) return 'recency-1-yesterday';\n\n const sevenDaysAgo = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);\n if (date >= sevenDaysAgo) return 'recency-2-this-week';\n\n if (date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth()) {\n return 'recency-3-this-month';\n }\n\n if (date.getFullYear() === now.getFullYear()) {\n return 'recency-4-this-year';\n }\n\n return 'recency-5-older';\n}\n\n/**\n * Recency-bucketing helper for chronological feeds.\n *\n * Buckets each model into one of six fixed buckets relative to the local\n * current time:\n *\n * - 'Today' — same local calendar day as now\n * - 'Yesterday' — day before today\n * - 'This week' — within the previous 7 calendar days\n * - 'This month' — earlier in the current calendar month\n * - 'Earlier this year' — earlier in the current calendar year\n * - 'Older' — prior calendar years\n *\n * Bucket keys are sort-ordered (`'recency-0-today'`, `'recency-1-yesterday'`, …)\n * so a descending-by-date sort renders buckets in natural reading order.\n *\n * V1 is opinionated — no `opts` parameter. If you need different bucket\n * thresholds, override `groupHeaderLabel` after the spread (label-only\n * customization) or write an inline `groupBy` (bucket-set customization).\n *\n * **Future dates** (rare) bucket as 'Today' if same calendar day,\n * otherwise 'This week' (because `date >= sevenDaysAgo` trivially holds\n * for any future date).\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: notifications,\n * itemTemplate: '...',\n * ...groupByRecency('created')\n * });\n */\nexport function groupByRecency(fieldOrAccessor) {\n const access = resolveAccessor(fieldOrAccessor);\n return {\n groupBy: (model) => recencyBucketKey(toDate(access(model))),\n groupHeaderLabel: (key) => RECENCY_LABELS[key] || key\n };\n}\n\n// Common JSON-string-as-boolean forms. Lower-cased + trimmed before match.\nconst STRING_FALSE_VALUES = new Set(['false', '0', 'no', 'off']);\n\n/**\n * Coerce a raw value into a boolean for `groupByBoolean` bucketing.\n * Returns `null` for missing / empty inputs so they fall into the\n * ungrouped tail rather than a misleading \"false\" bucket.\n * @private\n */\nfunction coerceBoolean(raw) {\n if (raw == null) return null;\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'number') return raw !== 0;\n if (typeof raw === 'string') {\n const lower = raw.trim().toLowerCase();\n if (lower === '') return null;\n return !STRING_FALSE_VALUES.has(lower);\n }\n return Boolean(raw);\n}\n\n/**\n * Binary-flag bucketing helper.\n *\n * Buckets each model into `'true'` / `'false'` based on the raw value at\n * `fieldOrAccessor`. Missing / empty raw values (`null`, `undefined`,\n * empty string) drop into the ungrouped tail rather than misleadingly\n * collapsing to \"false\".\n *\n * **String-false carve-out:** raw string values `'false'`, `'0'`, `'no'`,\n * `'off'` (case-insensitive, trimmed) coerce to `false`. Catches the\n * common backend pattern of returning JSON booleans as strings, which\n * pure JS truthy-coercion would mis-bucket.\n *\n * Defaults to `'Yes'` / `'No'` labels — most admin-UI cases (Active /\n * Inactive, Verified / Unverified, Paid / Unpaid) will override.\n *\n * @param {string|Function} fieldOrAccessor - Model field name or accessor\n * @param {object} [opts]\n * @param {string} [opts.trueLabel='Yes'] - Display label for the `true` bucket\n * @param {string} [opts.falseLabel='No'] - Display label for the `false` bucket\n * @returns {{ groupBy: Function, groupHeaderLabel: Function }}\n *\n * @example\n * new ListView({\n * collection: users,\n * itemTemplate: '...',\n * ...groupByBoolean('is_active', { trueLabel: 'Active', falseLabel: 'Inactive' })\n * });\n */\nexport function groupByBoolean(fieldOrAccessor, opts = {}) {\n const access = resolveAccessor(fieldOrAccessor);\n const trueLabel = opts.trueLabel != null ? opts.trueLabel : 'Yes';\n const falseLabel = opts.falseLabel != null ? opts.falseLabel : 'No';\n\n return {\n groupBy: (model) => {\n const b = coerceBoolean(access(model));\n if (b === null) return null;\n return b ? 'true' : 'false';\n },\n groupHeaderLabel: (key) => {\n if (key === 'true') return trueLabel;\n if (key === 'false') return falseLabel;\n return key;\n }\n };\n}\n\n// Default export is the helper bag — convenient for environments that\n// can't resolve the named export directly (test simple-module-loader,\n// non-ESM consumers, etc.). Keep both forms in sync as helpers are added.\nexport default {\n groupByDay,\n groupByField,\n groupByRecency,\n groupByBoolean\n};\n","/**\n * SideNavView - Left sidebar navigation with content panel\n *\n * A reusable navigation component that displays a vertical sidebar with\n * nav links, optional group labels, and icons. The content panel mounts\n * one child view at a time, switching on nav click.\n *\n * Features:\n * - Left sidebar with nav links, icons, and group dividers\n * - Active state with accent border\n * - Mount/unmount child views on section switch\n * - Responsive: collapses to dropdown on narrow containers\n * - Permission-aware: skips sections the user lacks permission for\n * - Configurable nav width and content padding\n * - Smooth fade transitions between sections\n *\n * Example Usage:\n * ```javascript\n * const sideNav = new SideNavView({\n * sections: [\n * { key: 'profile', label: 'Profile', icon: 'bi-person', view: profileView },\n * { key: 'security', label: 'Security', icon: 'bi-shield-lock', view: securityView },\n * { type: 'divider', label: 'Activity' },\n * { key: 'sessions', label: 'Sessions', icon: 'bi-clock-history', view: sessionsView },\n * ],\n * activeSection: 'profile',\n * navWidth: 200,\n * contentPadding: '1.5rem 2.5rem',\n * enableResponsive: true\n * });\n * ```\n */\n\nimport View from '@core/View.js';\n\nclass SideNavView extends View {\n constructor(options = {}) {\n const {\n sections = [],\n activeSection,\n navWidth,\n contentPadding,\n enableResponsive,\n minWidth,\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'side-nav-view',\n ...viewOptions\n });\n\n // Configuration\n this.navWidth = navWidth || 200;\n this.contentPadding = contentPadding || '1.5rem 2.5rem';\n this.enableResponsive = enableResponsive !== false;\n this.minWidth = minWidth || 500;\n\n // State\n this.sectionConfigs = []; // Full config array (including dividers)\n this.sectionViews = {}; // key → view instance\n this.sectionKeys = []; // Ordered navigable section keys\n this.activeSection = null;\n this.currentMode = 'sidebar'; // 'sidebar' or 'dropdown'\n this.resizeObserver = null;\n this.lastContainerWidth = 0;\n\n // Process sections config\n for (const config of sections) {\n this._addSectionConfig(config);\n }\n\n // Set initial active section\n this.activeSection = activeSection || this.sectionKeys[0] || null;\n\n // Bind resize handler\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Process and store a section config entry\n * @param {object} config - Section config (navigable or divider)\n * @private\n */\n _addSectionConfig(config) {\n if (config.type === 'divider') {\n this.sectionConfigs.push({ type: 'divider', label: config.label });\n return;\n }\n\n // Skip if user lacks required permission\n if (config.permissions && !this._hasPermission(config.permissions)) {\n return;\n }\n\n this.sectionConfigs.push(config);\n this.sectionKeys.push(config.key);\n\n if (config.view) {\n this.sectionViews[config.key] = config.view;\n config.view.parent = this;\n }\n }\n\n /**\n * Check if the current user has a permission\n * @param {string} perm - Permission string\n * @returns {boolean}\n * @private\n */\n _hasPermission(perm) {\n try {\n return this.getApp().activeUser.hasPerm(perm);\n } catch {\n return true; // If app isn't available yet, allow — will be checked at render\n }\n }\n\n // ───────────────────────────────────────────────\n // Template\n // ───────────────────────────────────────────────\n\n async renderTemplate() {\n // Stylesheet lives in src/core/css/core.css under \"SideNavView\".\n // Two instance-specific values (`navWidth`, `contentPadding`) are\n // applied as inline `style=\"...\"` attributes below.\n const nav = this.currentMode === 'dropdown'\n ? this._buildDropdownNav()\n : this._buildSidebarNav();\n\n const navWidthStyle = `width: ${this.navWidth}px`;\n const contentPaddingStyle = `padding: ${this.contentPadding}`;\n\n return `\n ${this.currentMode === 'dropdown' ? `\n <div class=\"snv-dropdown\">${nav}</div>\n <div class=\"snv-content\" data-container=\"snv-content\" style=\"${contentPaddingStyle}\"></div>\n ` : `\n <div class=\"snv-layout\">\n <nav class=\"snv-nav\" style=\"${navWidthStyle}\">${nav}</nav>\n <div class=\"snv-content\" data-container=\"snv-content\" style=\"${contentPaddingStyle}\"></div>\n </div>\n `}\n `;\n }\n\n /**\n * Normalize a badge config value into { text, variant } or null.\n * Accepts: number, string, { text, variant }, or falsy → null.\n * @param {*} badge\n * @returns {{text: string, variant: string} | null}\n * @private\n */\n _normalizeBadge(badge) {\n if (badge === null || badge === undefined || badge === false || badge === '') return null;\n if (typeof badge === 'number' || typeof badge === 'string') {\n return { text: String(badge), variant: 'muted' };\n }\n if (typeof badge === 'object' && badge.text !== undefined && badge.text !== null && badge.text !== '') {\n const variant = badge.variant || 'muted';\n return { text: String(badge.text), variant };\n }\n return null;\n }\n\n /**\n * Render a badge HTML fragment for a section config.\n * @param {object} config\n * @returns {string}\n * @private\n */\n _renderBadge(config) {\n const badge = this._normalizeBadge(config.badge);\n if (!badge) return '';\n return `<span class=\"snv-badge snv-badge-${this.escapeHtml(badge.variant)}\">${this.escapeHtml(badge.text)}</span>`;\n }\n\n /**\n * Build sidebar navigation HTML\n * @returns {string}\n * @private\n */\n _buildSidebarNav() {\n return this.sectionConfigs.map(config => {\n if (config.type === 'divider') {\n return `<div class=\"snv-nav-label\">${this.escapeHtml(config.label)}</div>`;\n }\n const isActive = config.key === this.activeSection;\n const icon = config.icon ? `<i class=\"bi ${this.escapeHtml(config.icon)}\"></i>` : '';\n const badge = this._renderBadge(config);\n return `<a role=\"button\" class=\"${isActive ? 'active' : ''}\" data-action=\"navigate\" data-section=\"${this.escapeHtml(config.key)}\">${icon} ${this.escapeHtml(config.label)}${badge}</a>`;\n }).join('');\n }\n\n /**\n * Build dropdown navigation HTML (responsive mode)\n * @returns {string}\n * @private\n */\n _buildDropdownNav() {\n const activeConfig = this.sectionConfigs.find(c => c.key === this.activeSection);\n const activeLabel = activeConfig ? activeConfig.label : this.sectionKeys[0];\n\n const items = this.sectionConfigs\n .filter(c => c.type !== 'divider')\n .map(config => {\n const isActive = config.key === this.activeSection;\n const badge = this._renderBadge(config);\n return `\n <li>\n <button class=\"dropdown-item ${isActive ? 'active' : ''}\"\n data-action=\"navigate\"\n data-section=\"${this.escapeHtml(config.key)}\"\n type=\"button\">\n ${config.icon ? `<i class=\"bi ${this.escapeHtml(config.icon)} me-2\"></i>` : ''}\n ${this.escapeHtml(config.label)}\n ${badge}\n ${isActive ? '<i class=\"bi bi-check-lg ms-2\"></i>' : ''}\n </button>\n </li>\n `;\n }).join('');\n\n return `\n <div class=\"dropdown\">\n <button class=\"snv-select-btn\" type=\"button\"\n data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n ${activeConfig?.icon ? `<i class=\"bi ${activeConfig.icon}\"></i>` : ''}\n <span>${this.escapeHtml(activeLabel)}</span>\n </button>\n <ul class=\"dropdown-menu w-100\">${items}</ul>\n </div>\n `;\n }\n\n // ───────────────────────────────────────────────\n // Lifecycle\n // ───────────────────────────────────────────────\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Mount the active section\n if (this.activeSection) {\n await this._mountSection(this.activeSection);\n }\n\n // Set up responsive behavior\n if (this.enableResponsive) {\n this._setupResponsive();\n }\n }\n\n async onBeforeDestroy() {\n await super.onBeforeDestroy();\n\n // Clean up resize observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('resize', this.handleResize);\n }\n\n // Destroy all section views\n for (const view of Object.values(this.sectionViews)) {\n if (view && typeof view.destroy === 'function') {\n await view.destroy();\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Section switching\n // ───────────────────────────────────────────────\n\n /**\n * Navigate to a section\n * @param {string} key - Section key\n * @returns {Promise<boolean>}\n */\n async showSection(key) {\n if (!this.sectionViews[key]) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n if (key === this.activeSection) {\n // Already active — but ensure it's mounted\n const view = this.sectionViews[key];\n if (view && view.isMounted() && this.element?.contains(view.element)) {\n return true;\n }\n }\n\n const previousSection = this.activeSection;\n this.activeSection = key;\n\n // Unmount previous section\n if (previousSection && previousSection !== key) {\n await this._unmountSection(previousSection);\n }\n\n // Mount new section\n await this._mountSection(key);\n\n // Call onSectionActivated hook after the view is mounted and visible\n const activeView = this.sectionViews[key];\n if (activeView?.onSectionActivated) {\n await activeView.onSectionActivated();\n }\n\n // Update nav visual state\n this._updateNavState(key);\n\n this.emit('section:changed', {\n activeSection: key,\n previousSection\n });\n\n return true;\n }\n\n /**\n * Mount a section view into the content area\n * @param {string} key - Section key\n * @private\n */\n async _mountSection(key) {\n const view = this.sectionViews[key];\n if (!view) return;\n\n const container = this.element?.querySelector('[data-container=\"snv-content\"]');\n if (!container) return;\n\n if (!view.isMounted()) {\n this._showContentLoading(container);\n try {\n await view.render(true, container);\n } finally {\n this._hideContentLoading(container);\n }\n }\n }\n\n /**\n * Show a lightweight spinner in the content panel\n * @param {HTMLElement} container\n * @private\n */\n _showContentLoading(container) {\n if (!container) return;\n let spinner = container.querySelector('.snv-loading');\n if (!spinner) {\n spinner = document.createElement('div');\n spinner.className = 'snv-loading';\n spinner.innerHTML = '<div class=\"spinner-border spinner-border-sm text-secondary\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div>';\n spinner.style.cssText = 'display:flex;align-items:center;justify-content:center;padding:3rem;';\n container.prepend(spinner);\n }\n }\n\n /**\n * Remove the content panel spinner\n * @param {HTMLElement} container\n * @private\n */\n _hideContentLoading(container) {\n if (!container) return;\n const spinner = container.querySelector('.snv-loading');\n if (spinner) spinner.remove();\n }\n\n /**\n * Unmount a section view\n * @param {string} key - Section key\n * @private\n */\n async _unmountSection(key) {\n const view = this.sectionViews[key];\n if (!view || !view.isMounted()) return;\n\n await view.unmount();\n }\n\n /**\n * Update nav link active state\n * @param {string} activeKey - Active section key\n * @private\n */\n _updateNavState(activeKey) {\n if (!this.element) return;\n\n // Update sidebar links\n this.element.querySelectorAll('.snv-nav a, .dropdown-item').forEach(link => {\n const section = link.dataset.section;\n if (section) {\n link.classList.toggle('active', section === activeKey);\n }\n });\n\n // Update dropdown button label\n const selectBtn = this.element.querySelector('.snv-select-btn span');\n if (selectBtn) {\n const config = this.sectionConfigs.find(c => c.key === activeKey);\n if (config) {\n selectBtn.textContent = config.label;\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Action handlers\n // ───────────────────────────────────────────────\n\n async onActionNavigate(event, el) {\n event.preventDefault();\n const section = el.dataset.section;\n if (section) {\n await this.showSection(section);\n }\n return true;\n }\n\n // ───────────────────────────────────────────────\n // Responsive\n // ───────────────────────────────────────────────\n\n /**\n * Set up responsive width detection\n * @private\n */\n _setupResponsive() {\n if (!this.element || !this.enableResponsive) return;\n\n this._updateMode();\n\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n this.handleResize();\n });\n const container = this.element.parentElement || this.element;\n this.resizeObserver.observe(container);\n } else {\n window.addEventListener('resize', this.handleResize);\n }\n }\n\n /**\n * Handle resize events\n */\n async handleResize() {\n const containerWidth = this._getContainerWidth();\n if (Math.abs(containerWidth - this.lastContainerWidth) > 50) {\n this.lastContainerWidth = containerWidth;\n await this._updateMode();\n }\n }\n\n /**\n * Get the container width\n * @returns {number}\n * @private\n */\n _getContainerWidth() {\n if (!this.element) return this.minWidth;\n const container = this.element.parentElement || this.element;\n return container.offsetWidth || this.minWidth;\n }\n\n /**\n * Check and switch between sidebar and dropdown modes\n * @private\n */\n async _updateMode() {\n const containerWidth = this._getContainerWidth();\n const newMode = containerWidth < this.minWidth ? 'dropdown' : 'sidebar';\n\n if (newMode !== this.currentMode) {\n this.currentMode = newMode;\n if (this.isMounted()) {\n await this.render();\n }\n this.emit('navigation:modeChanged', {\n mode: this.currentMode,\n containerWidth\n });\n }\n }\n\n // ───────────────────────────────────────────────\n // Public API\n // ───────────────────────────────────────────────\n\n /**\n * Get the active section key\n * @returns {string|null}\n */\n getActiveSection() {\n return this.activeSection;\n }\n\n /**\n * Get all navigable section keys\n * @returns {string[]}\n */\n getSectionKeys() {\n return [...this.sectionKeys];\n }\n\n /**\n * Get a section's view by key\n * @param {string} key - Section key\n * @returns {View|null}\n */\n getSection(key) {\n return this.sectionViews[key] || null;\n }\n\n /**\n * Add a section dynamically\n * @param {object} config - Section config\n * @param {boolean} makeActive - Whether to activate the section\n * @returns {Promise<boolean>}\n */\n async addSection(config, makeActive = false) {\n if (config.key && this.sectionViews[config.key]) {\n console.warn(`SideNavView: Section \"${config.key}\" already exists`);\n return false;\n }\n\n this._addSectionConfig(config);\n\n if (this.isMounted()) {\n await this.render();\n if (makeActive && config.key) {\n await this.showSection(config.key);\n }\n }\n\n this.emit('section:added', { config });\n return true;\n }\n\n /**\n * Remove a section dynamically\n * @param {string} key - Section key to remove\n * @returns {Promise<boolean>}\n */\n async removeSection(key) {\n const view = this.sectionViews[key];\n if (!view) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n // Destroy the view\n if (typeof view.destroy === 'function') {\n await view.destroy();\n }\n\n // Remove from data structures\n delete this.sectionViews[key];\n this.sectionKeys = this.sectionKeys.filter(k => k !== key);\n this.sectionConfigs = this.sectionConfigs.filter(c => c.key !== key);\n\n // Handle active section removal\n if (this.activeSection === key) {\n this.activeSection = this.sectionKeys[0] || null;\n }\n\n if (this.isMounted()) {\n await this.render();\n }\n\n this.emit('section:removed', { key });\n return true;\n }\n\n /**\n * Update a section's badge dynamically without re-rendering the whole nav.\n * Accepts the same shapes as the schema: number, string, { text, variant }, or falsy to clear.\n * @param {string} key - Section key\n * @param {*} value - Badge value\n * @returns {boolean} true if the section exists and was updated\n */\n setBadge(key, value) {\n const config = this.sectionConfigs.find(c => c.key === key);\n if (!config) return false;\n\n config.badge = value;\n\n if (!this.element) return true; // not rendered yet — schema update is enough\n\n // Update both sidebar (<a>) and dropdown-item (<button>) instances\n const links = this.element.querySelectorAll(`[data-section=\"${key}\"]`);\n links.forEach(link => {\n const existing = link.querySelector('.snv-badge');\n const html = this._renderBadge(config);\n if (existing) existing.remove();\n if (html) link.insertAdjacentHTML('beforeend', html);\n });\n return true;\n }\n\n /**\n * Prevent model changes from triggering a full re-render.\n * Section views manage their own model reactivity.\n */\n _onModelChange() {\n // no-op — same pattern as UserView\n }\n\n static create(options = {}) {\n return new SideNavView(options);\n }\n}\n\nexport default SideNavView;\n","/**\n * DetailView - Reusable record-detail layout\n *\n * The standard \"record viewer in a modal\" shape used across web-mojo:\n *\n * ┌──────────────────────────────────────────────────────────┐\n * │ icon Name [active] [edit] x│ ← DetailHeaderView\n * │ Optional reasoning / subtitle │\n * │ [chips] [chips] [chips] │\n * ├────────┬─────────────────────────────────────────────────┤\n * │ Rail │ │\n * │ link │ Active section content │ ← SideNavView\n * │ link │ │\n * │ link │ │\n * └────────┴─────────────────────────────────────────────────┘\n *\n * Subclass `DetailView` and pass config; override action handlers as needed.\n * Pair with `Modal.detail(view)` to get the right modal envelope (no body\n * padding, no footer, X close handled by the header).\n *\n * @example\n * class RuleSetView extends DetailView {\n * constructor(options = {}) {\n * super({\n * ...options,\n * header: {\n * icon: 'bi-gear-wide-connected',\n * titleField: 'name',\n * subtitlePath: 'metadata.reasoning',\n * chips: [\n * { icon: 'bi-tag-fill', textPath: 'category', variant: 'primary' },\n * { icon: 'bi-flag', text: m => `Priority ${m.get('priority')}` },\n * { icon: 'bi-stars', text: 'AI-proposed', variant: 'warning',\n * when: m => m.get('metadata')?.assistant_proposed }\n * ],\n * activeField: 'is_active',\n * actions: [\n * { label: 'Edit', icon: 'bi-pencil', action: 'edit-header' }\n * ],\n * contextMenu: { items: [...] }\n * },\n * sections: [ ... ], // SideNavView shape\n * activeSection: 'Overview'\n * });\n * }\n *\n * async onActionEditHeader() { ... }\n * }\n *\n * await Modal.detail(new RuleSetView({ model }));\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport MOJOUtils from '@core/utils/MOJOUtils.js';\n\n// ── DetailHeaderView ──────────────────────────────────────\n\n/**\n * The flat record-header card used at the top of `DetailView`.\n * Exported separately for cases where you want this header without the\n * full DetailView (e.g. on a Page rather than a Modal).\n *\n * The right-side action cluster reads, left-to-right:\n * [auxFn output] · [active switch] · [actions[]] · | · [⋮ context] · [✕]\n *\n * `auxFn(model) -> htmlString` is the slot for inline state read-outs\n * that don't fit the chip/badge model — presence dots, \"Last seen 4m\n * ago\" lines, attempt counters, etc. The string is rendered as TRUSTED\n * HTML (it comes from source code, not user input). Returning falsy\n * omits the wrapper entirely.\n *\n * Security note: if your `auxFn` interpolates model fields, free-text\n * user data, or any other untrusted value, you MUST escape it\n * yourself (e.g. via `MOJOUtils.escapeHtml(...)`) before composing\n * the returned string. The framework does not escape the result.\n */\nclass DetailHeaderView extends View {\n constructor(options = {}) {\n const {\n icon,\n iconTone, // optional: primary | success | warning | danger | info — toned dh-icon\n iconToneFn, // optional: (model) => tone, for state-driven icons\n iconHtml, // optional: string | (model) => htmlString — render trusted HTML in the icon slot (avatar img, custom badge, etc.) instead of the Bootstrap icon\n titleField,\n titleFn,\n titleAffix, // optional: string | (model) => htmlString — trusted HTML slot rendered next to the title (copy-button, etc.)\n subtitlePath,\n subtitleFn, // optional: (model) => string, takes precedence over subtitlePath\n subtitlePlaceholder, // optional muted text shown when subtitle is empty\n subtitleEditAction, // action name for click-to-edit on the empty placeholder\n chips = [],\n activeField,\n auxFn, // optional: (model) => htmlString — right-gutter aux block (presence, last-seen, etc.)\n actions = [],\n closable = true,\n contextMenu, // { items: [...] } | null\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'detail-header',\n // The header emits several `data-bs-toggle=\"tooltip\"` triggers\n // (active toggle, action buttons, close button). Auto-init them.\n enableTooltips: true,\n ...viewOptions\n });\n\n this.icon = icon || 'bi-file-earmark';\n this.iconTone = iconTone || null;\n this.iconToneFn = iconToneFn || null;\n this.iconHtml = iconHtml || null;\n this.titleField = titleField || null;\n this.titleFn = titleFn || null;\n this.titleAffix = titleAffix || null;\n this.subtitlePath = subtitlePath || null;\n this.subtitleFn = subtitleFn || null;\n this.subtitlePlaceholder = subtitlePlaceholder || null;\n this.subtitleEditAction = subtitleEditAction || null;\n this.chips = chips;\n this.activeField = activeField || null;\n this.auxFn = typeof auxFn === 'function' ? auxFn : null;\n this.actions = actions;\n this.closable = closable;\n this.contextMenuConfig = contextMenu || null;\n\n this.template = () => this._buildTemplate();\n }\n\n _resolveTitle() {\n if (this.titleFn) return this.titleFn(this.model) || '';\n if (this.titleField) return this.model?.get?.(this.titleField) || '';\n return '';\n }\n\n _resolveTitleAffix() {\n if (typeof this.titleAffix === 'function') return this.titleAffix(this.model) || '';\n return this.titleAffix || '';\n }\n\n _resolveSubtitle() {\n if (this.subtitleFn) return this.subtitleFn(this.model) || '';\n if (!this.subtitlePath) return '';\n return MOJOUtils.getNestedValue(this.model?.attributes || {}, this.subtitlePath) || '';\n }\n\n _resolveIconTone() {\n if (this.iconToneFn) return this.iconToneFn(this.model) || null;\n return this.iconTone;\n }\n\n _resolveIconHtml() {\n if (typeof this.iconHtml === 'function') return this.iconHtml(this.model) || '';\n return this.iconHtml || '';\n }\n\n _resolveChips() {\n return (this.chips || [])\n .filter(chip => !chip.when || chip.when(this.model))\n .map(chip => {\n let text;\n if (typeof chip.text === 'function') text = chip.text(this.model);\n else if (chip.textPath) text = this.model?.get?.(chip.textPath);\n else text = chip.text;\n\n let tooltip = '';\n if (typeof chip.tooltip === 'function') tooltip = chip.tooltip(this.model) || '';\n else if (chip.tooltip) tooltip = String(chip.tooltip);\n\n return {\n icon: chip.icon || null,\n text: text != null ? String(text) : '',\n variant: chip.variant || 'light',\n tooltip,\n // Optional kebab-cased action — when set, the chip\n // renders as a click-through <button> carrying\n // data-action so the parent view's onActionKebabName\n // fires on click. Used by UserView for the org chip.\n action: chip.action || null\n };\n })\n .filter(c => c.text !== '');\n }\n\n _buildTemplate() {\n const title = this._resolveTitle();\n const subtitle = this._resolveSubtitle();\n const chips = this._resolveChips();\n const isActive = this.activeField ? !!this.model?.get?.(this.activeField) : null;\n\n const chipsHtml = chips.map(c => {\n const iconHtml = c.icon ? `<i class=\"bi ${this.escapeHtml(c.icon)} me-1\"></i>` : '';\n const tooltipAttrs = c.tooltip\n ? ` data-bs-toggle=\"tooltip\" title=\"${this.escapeHtml(c.tooltip)}\"`\n : '';\n // Click-through chips render as buttons so the parent's\n // EventDelegate picks up `data-action`. Visually identical\n // to the static-span variant (same Bootstrap badge classes).\n if (c.action) {\n return `<button type=\"button\" class=\"badge bg-${this.escapeHtml(c.variant)} dh-chip-action border-0\" data-action=\"${this.escapeHtml(c.action)}\" style=\"cursor:pointer;\"${tooltipAttrs}>${iconHtml}${this.escapeHtml(c.text)}</button>`;\n }\n return `<span class=\"badge bg-${this.escapeHtml(c.variant)}\"${tooltipAttrs}>${iconHtml}${this.escapeHtml(c.text)}</span>`;\n }).join('');\n\n // Right-gutter aux slot — trusted HTML. Falsy result omits the wrapper.\n const auxResult = this.auxFn ? (this.auxFn(this.model) || '') : '';\n const auxHtml = auxResult ? `<div class=\"dh-aux\">${auxResult}</div>` : '';\n\n const switchHtml = this.activeField ? `\n <label class=\"dh-active-switch me-1\" data-bs-toggle=\"tooltip\" title=\"Toggle ${this.escapeHtml(this.activeField)}\">\n <input type=\"checkbox\" data-action=\"toggle-active\" ${isActive ? 'checked' : ''}>\n <span class=\"dh-track\"></span>\n <span class=\"dh-track-label\">${isActive ? 'Active' : 'Inactive'}</span>\n </label>\n ` : '';\n\n const actionsHtml = (this.actions || []).map(a => {\n const iconHtml = a.icon ? `<i class=\"bi ${this.escapeHtml(a.icon)}\"></i>` : '';\n return `<button class=\"dh-action\" data-action=\"${this.escapeHtml(a.action)}\" data-bs-toggle=\"tooltip\" title=\"${this.escapeHtml(a.title || a.label)}\">${iconHtml} ${this.escapeHtml(a.label)}</button>`;\n }).join('');\n\n const closeHtml = this.closable ? `\n <button class=\"dh-action dh-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\" data-bs-toggle=\"tooltip\" title=\"Close\">\n <i class=\"bi bi-x-lg\"></i>\n </button>\n ` : '';\n\n const contextMenuHtml = this.contextMenuConfig ? `<div data-container=\"detail-context-menu\"></div>` : '';\n\n // Group 2 (overflow + close) renders only when at least one of those is present.\n const hasGroup2 = !!this.contextMenuConfig || this.closable;\n\n const tone = this._resolveIconTone();\n const iconHtmlSlot = this._resolveIconHtml();\n\n // When iconHtml is provided (avatar img, custom badge, etc.) drop the\n // tone-tinted background/border so the slot is a frame for the\n // caller's content rather than a colored square. The caller's HTML\n // controls its own visual identity.\n const iconClass = iconHtmlSlot\n ? 'dh-icon dh-icon-image'\n : (tone ? `dh-icon dh-icon-tone-${this.escapeHtml(tone)}` : 'dh-icon');\n const iconBody = iconHtmlSlot || `<i class=\"bi ${this.escapeHtml(this.icon)}\"></i>`;\n\n // Trusted-HTML slot rendered inline next to the title (copy-button,\n // edit-pencil, etc.). Caller is responsible for escaping anything\n // that comes from user input — same contract as `auxFn`.\n const titleAffixHtml = this._resolveTitleAffix();\n\n // Stylesheet lives in src/core/css/core.css under \"DetailHeaderView\".\n return `\n <div class=\"d-flex align-items-start gap-3\">\n <div class=\"${iconClass}\">${iconBody}</div>\n <div class=\"dh-meta\" style=\"min-width: 0; flex: 1;\">\n <div class=\"dh-name-row\">\n <h2 class=\"dh-name\">${this.escapeHtml(title)}</h2>\n ${titleAffixHtml ? `<span class=\"dh-title-affix\">${titleAffixHtml}</span>` : ''}\n </div>\n ${subtitle\n ? `<p class=\"dh-subtitle\">${this.escapeHtml(subtitle)}</p>`\n : (this.subtitlePlaceholder\n ? `<p class=\"dh-subtitle dh-subtitle-empty\"${this.subtitleEditAction ? ` data-action=\"${this.escapeHtml(this.subtitleEditAction)}\" role=\"button\"` : ''}>${this.escapeHtml(this.subtitlePlaceholder)}</p>`\n : '')\n }\n ${chips.length ? `<div class=\"dh-chips\">${chipsHtml}</div>` : ''}\n </div>\n </div>\n <div class=\"dh-actions d-flex align-items-center gap-1\">\n <!-- Group 0 — aux slot (presence, last-seen, attempt counter, …) -->\n ${auxHtml}\n <!-- Group 1 — record actions (state + primary edits) -->\n ${switchHtml}\n ${actionsHtml}\n ${hasGroup2 ? `\n <span class=\"dh-group-sep\" aria-hidden=\"true\"></span>\n <!-- Group 2 — modal chrome (overflow + close) -->\n ${contextMenuHtml}\n ${closeHtml}\n ` : ''}\n </div>\n `;\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n // Mount context menu if configured\n if (this.contextMenuConfig && !this._contextMenuMounted) {\n const cm = new ContextMenu({\n containerId: 'detail-context-menu',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n ...this.contextMenuConfig\n }\n });\n this.addChild(cm);\n await cm.render();\n this._contextMenuMounted = true;\n }\n }\n\n /**\n * Re-dispatch unhandled actions to the parent DetailView so subclasses\n * can keep their handlers in one place.\n *\n * Always re-dispatches programmatically. A previous \"skip when target\n * is contained in the parent\" optimization was wrong for ContextMenu\n * items: the natural DOM bubble carries `data-action=\"menu-item-click\"`,\n * NOT the menu item's action (e.g. `edit-user`). The item action only\n * travels via the programmatic dispatch from `ContextMenu.onActionMenuItemClick`,\n * so short-circuiting based on bubble path dropped it on the floor.\n *\n * Re-dispatching unconditionally is safe: when the parent handles the\n * action and returns truthy, EventDelegate calls `stopPropagation`, so\n * the natural DOM bubble can't fire it a second time.\n */\n async onActionDefault(action, event, el) {\n const dest = this.parent;\n if (!dest || !dest.events || typeof dest.events.dispatch !== 'function') {\n return false;\n }\n return await dest.events.dispatch(action, event, el);\n }\n\n /**\n * Default implementation of the active toggle. Overridable by subclasses.\n *\n * Round-trips `model.save({ [activeField]: checked })`. The visual\n * feedback is the switch itself — no toasts:\n * - Optimistic update: `model.set` fires a change event; the\n * parent DetailView's `_onModelChange` re-renders the header\n * and the switch reflects the new state.\n * - On success: the switch stays in the new position.\n * - On failure: the revert `model.set` fires another change\n * event, the header re-renders, and the switch bounces back.\n * The bounce IS the feedback — no shouting at the user.\n */\n async onActionToggleActive(event, element) {\n if (!this.activeField) return;\n const checked = !!element.checked;\n element.disabled = true;\n try {\n this.model.set(this.activeField, checked);\n const resp = await this.model.save({ [this.activeField]: checked });\n if (resp && resp.status && resp.status >= 400) {\n throw new Error('Save failed');\n }\n this.emit('detail:updated');\n } catch (err) {\n // Revert silently — the bounce IS the feedback.\n this.model.set(this.activeField, !checked);\n } finally {\n // The element may have been replaced by a re-render between\n // the optimistic `model.set` and here; this is best-effort.\n if (element && element.isConnected) element.disabled = false;\n }\n }\n}\n\n\n// ── DetailView ────────────────────────────────────────────\n\nclass DetailView extends View {\n constructor(options = {}) {\n const {\n header = {},\n sections = [],\n activeSection,\n navWidth,\n minWidth,\n contentPadding,\n ...viewOptions\n } = options;\n\n // Always include `detail-view` as one of the classes so the framework's\n // DetailView-scoped CSS applies, even when subclasses pass their own\n // className (e.g. `user-view`, `runner-details-view`).\n const subclassClassName = viewOptions.className || '';\n const mergedClassName = subclassClassName.includes('detail-view')\n ? subclassClassName\n : (subclassClassName ? `detail-view ${subclassClassName}` : 'detail-view');\n\n super({\n ...viewOptions,\n className: mergedClassName\n });\n\n if (!this.model && options.model) {\n this.model = options.model;\n }\n\n this.headerConfig = header;\n this.sectionsConfig = sections;\n this.initialActiveSection = activeSection;\n this.navWidth = navWidth ?? 200;\n this.minWidth = minWidth ?? 600;\n // Bootstrap p-4 (1.5rem) breathing room around every section by default.\n // The padding is on the SideNavView's snv-content wrapper, so each\n // section gets uniform inset on all four sides without needing its\n // own padding utility class. Pass `contentPadding: '0'` to opt out.\n this.contentPadding = contentPadding ?? '1.5rem';\n\n this.template = `\n <div class=\"detail-view-shell\">\n <div data-container=\"detail-header\"></div>\n <div data-container=\"detail-sidenav\" class=\"detail-view-body\"></div>\n </div>\n `;\n }\n\n async onInit() {\n await this.onBeforeBuild();\n\n this.headerView = new DetailHeaderView({\n containerId: 'detail-header',\n model: this.model,\n ...this.headerConfig\n });\n // Bubble header events up so subclasses can listen via `this.on(...)`\n this.headerView.on('detail:updated', () => this.emit('detail:updated'));\n this.addChild(this.headerView);\n\n this.sideNav = new SideNavView({\n containerId: 'detail-sidenav',\n sections: this.sectionsConfig,\n activeSection: this.initialActiveSection,\n navWidth: this.navWidth,\n minWidth: this.minWidth,\n contentPadding: this.contentPadding,\n enableResponsive: true\n });\n this.addChild(this.sideNav);\n\n await this.onAfterBuild();\n }\n\n /**\n * Hook for subclasses to do work BEFORE the header + sidenav are built\n * (e.g. pre-create shared collections, instantiate section views).\n */\n async onBeforeBuild() {}\n\n /**\n * Hook for subclasses to do work AFTER the header + sidenav are built\n * (e.g. wire cross-section listeners, register sidenav badges).\n */\n async onAfterBuild() {}\n\n /** Expose the SideNavView's setBadge for subclass convenience */\n setBadge(key, value) {\n return this.sideNav?.setBadge(key, value);\n }\n\n /** Switch to a section programmatically */\n showSection(key) {\n return this.sideNav?.showSection(key);\n }\n\n /**\n * Prevent a model `change` event from triggering a full re-render of the\n * shell. Section views and the header manage their own model reactivity\n * (re-rendering themselves directly when needed). Without this, any\n * `model.set` would wipe the SideNav's mounted section out of the DOM.\n */\n _onModelChange() {\n // Update the header so chips / switch / subtitle reflect the new state\n if (this.headerView?.isMounted()) {\n this.headerView.render().catch(() => {});\n }\n }\n}\n\n// Stylesheet for DetailView + DetailHeaderView lives in\n// src/core/css/core.css under \"DetailView / DetailHeaderView\".\n\nexport { DetailView, DetailHeaderView };\nexport default DetailView;\n","/**\n * MetricCard - At-a-glance KPI card.\n *\n * A compact label / value / hint card with an optional icon and\n * an optional left-border tone accent. Designed to compose in a\n * row of 3-4 cards for \"overview\" sections (RuleSetView Overview,\n * IncidentView Overview, security dashboard).\n *\n * Example:\n * const card = new MetricCard({\n * label: 'Incidents (30d)',\n * value: 42,\n * icon: 'bi-shield-exclamation',\n * tone: 'warning',\n * hint: '14 minutes ago',\n * action: 'view-incidents' // optional — emits via parent's onActionViewIncidents\n * });\n * this.addChild(card, { containerId: 'kpi-1' });\n *\n * Tones map to Bootstrap CSS variables — `default` | `success` |\n * `warning` | `danger` | `info`. Theme-aware via tokens.\n */\n\nimport View from '@core/View.js';\n\nconst VALID_TONES = new Set(['default', 'success', 'warning', 'danger', 'info', 'primary']);\n\nclass MetricCard extends View {\n constructor(options = {}) {\n const {\n label,\n value,\n icon = null,\n valueIcon = null,\n tone = 'default',\n hint = null,\n action = null,\n ...viewOptions\n } = options;\n\n super({\n tagName: action ? 'button' : 'div',\n className: 'metric-card',\n ...viewOptions\n });\n\n this.label = label || '';\n this.value = value;\n this.icon = icon;\n this.valueIcon = valueIcon; // optional Bootstrap Icons class shown left of value\n this.tone = VALID_TONES.has(tone) ? tone : 'default';\n this.hint = hint;\n this.action = action;\n\n this.template = () => this._buildTemplate();\n }\n\n _buildTemplate() {\n const label = this._resolve(this.label, '');\n const hint = this._resolve(this.hint, null);\n const valueText = this._renderValue();\n const iconHtml = this.icon\n ? `<i class=\"bi ${this.escapeHtml(this.icon)} metric-card-icon\"></i>`\n : '';\n const valueIconHtml = this.valueIcon\n ? `<i class=\"bi ${this.escapeHtml(this.valueIcon)} metric-card-value-icon\"></i>`\n : '';\n const hintHtml = hint\n ? `<div class=\"metric-card-hint\">${this.escapeHtml(String(hint))}</div>`\n : '';\n\n // Outer element styling is handled by `className` + the tone class on the root.\n // We toggle the tone via classList in onAfterRender to avoid stomping\n // the user's className override.\n // Stylesheet is injected once at module load (see bottom of file) so\n // multiple MetricCard instances don't duplicate `<style>` blocks that\n // would fight in the cascade.\n return `\n <div class=\"metric-card-label\">${iconHtml}<span>${this.escapeHtml(String(label))}</span></div>\n <div class=\"metric-card-value\">${valueIconHtml}<span>${valueText}</span></div>\n ${hintHtml}\n `.trim();\n }\n\n _resolve(opt, fallback = '') {\n if (typeof opt === 'function') {\n try { return opt(this.model) ?? fallback; } catch (_) { return fallback; }\n }\n return opt ?? fallback;\n }\n\n _renderValue() {\n // Resolve function-valued options against the current model — same\n // pattern as `StatusPanel._resolve()` so callers can pass\n // `value: () => this._someCount()` and have it re-evaluate on every\n // render() rather than rendering the function source.\n let v = this.value;\n if (typeof v === 'function') {\n try { v = v(this.model); } catch (_) { v = null; }\n }\n if (v === null || v === undefined) return '<span class=\"text-muted\">—</span>';\n if (typeof v === 'object' && v !== null && 'text' in v) {\n return this.escapeHtml(String(v.text));\n }\n return this.escapeHtml(String(v));\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n if (!this.element) return;\n // Apply the tone class — resolve function-valued tone against the\n // current model so consumers can vary tone with state.\n const tone = this._resolve(this.tone, 'default');\n const safeTone = VALID_TONES.has(tone) ? tone : 'default';\n VALID_TONES.forEach(t => this.element.classList.remove(`metric-card-tone-${t}`));\n if (safeTone && safeTone !== 'default') {\n this.element.classList.add(`metric-card-tone-${safeTone}`);\n }\n // Apply data-action / type=\"button\" on the root when this is a clickable card\n if (this.action) {\n this.element.setAttribute('data-action', this.action);\n if (this.element.tagName === 'BUTTON' && !this.element.hasAttribute('type')) {\n this.element.setAttribute('type', 'button');\n }\n }\n }\n\n /**\n * Update the displayed value without a full re-render.\n * @param {*} value - New value (string|number|{text})\n */\n setValue(value) {\n this.value = value;\n if (!this.element) return;\n // Update only the text span; preserve the optional valueIcon.\n const valueEl = this.element.querySelector('.metric-card-value');\n if (!valueEl) return;\n const span = valueEl.querySelector(':scope > span');\n if (span) {\n span.innerHTML = this._renderValue();\n } else {\n valueEl.innerHTML = `<span>${this._renderValue()}</span>`;\n }\n }\n\n /**\n * Update the hint line without re-render.\n * @param {string|null} hint\n */\n setHint(hint) {\n this.hint = hint;\n if (!this.element) return;\n let slot = this.element.querySelector('.metric-card-hint');\n if (hint) {\n if (slot) {\n slot.textContent = String(hint);\n } else {\n slot = document.createElement('div');\n slot.className = 'metric-card-hint';\n slot.textContent = String(hint);\n this.element.appendChild(slot);\n }\n } else if (slot) {\n slot.remove();\n }\n }\n}\n\n// Stylesheet for MetricCard lives in src/core/css/core.css under \"MetricCard\".\n\nexport default MetricCard;\n","/**\n * StatusPanel - Hero \"current state\" panel for record-detail views.\n *\n * The big colored panel that opens an Overview section with a\n * dot+state read-out, a primary headline, a secondary meta line, and\n * a row of action buttons. Used in JobDetailsView (job lifecycle),\n * IncidentView (incident triage), RunnerDetailsView (alive/dead),\n * and any other record where the operator's first question is\n * \"what state is this in and what do I do next.\"\n *\n * Security note: `meta` is rendered as TRUSTED HTML so callers can\n * compose `<code>`, `<strong>`, etc. inline. The trust contract is\n * \"the string was assembled in source code, not pulled from user\n * input.\" If you interpolate model fields or other user-controlled\n * data into `meta`, you MUST escape them yourself (e.g. via\n * `MOJOUtils.escapeHtml(...)`). `state`, `headline`, and action\n * `label` are escaped automatically.\n *\n * ┌───────────────────────────────────────────────────────────┐\n * │ ● State label [ primary ] [ alt ] │\n * │ Headline line │\n * │ Optional supporting meta line · with <code>fragments</code>\n * └───────────────────────────────────────────────────────────┘\n *\n * Each option may be a static value OR a function of `model` (the\n * standard DetailHeaderView pattern) so the panel re-renders with the\n * current model state.\n *\n * @example\n * const panel = new StatusPanel({\n * model,\n * tone: m => m.get('status') === 'failed' ? 'danger' : 'info',\n * state: m => m.get('status') === 'running' ? 'Running' : 'Idle',\n * headline: m => `On ${m.get('runner_id') || '—'}`,\n * meta: m => `Last attempt ${m.get('attempt')} of ${m.get('max_retries')}`,\n * actions: m => [\n * m.canRetry?.() && { label: 'Retry now', action: 'retry-job', icon: 'bi-arrow-clockwise', variant: 'primary' },\n * m.canCancel?.() && { label: 'Cancel', action: 'cancel-job', icon: 'bi-x-circle', variant: 'outline-danger' }\n * ].filter(Boolean)\n * });\n * parent.addChild(panel, { containerId: 'status' });\n *\n * Action buttons render with `data-action=\"<action>\"` and dispatch via\n * the standard MOJO action pipeline — handlers live on whichever\n * ancestor wants to react (typically the parent section view or the\n * containing DetailView subclass).\n */\n\nimport View from '@core/View.js';\n\nconst VALID_TONES = new Set(['default', 'primary', 'success', 'info', 'warning', 'danger', 'secondary']);\n\nclass StatusPanel extends View {\n constructor(options = {}) {\n const {\n tone = 'default',\n state = '',\n headline = '',\n meta = '',\n icon = null,\n actions = [],\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'detail-status-panel',\n ...viewOptions\n });\n\n this._toneOpt = tone;\n this._stateOpt = state;\n this._headlineOpt = headline;\n this._metaOpt = meta;\n this._iconOpt = icon;\n this._actionsOpt = actions;\n\n this.template = () => this._buildTemplate();\n }\n\n // ── Resolvers ──────────────────────────────────────────────\n\n _resolve(opt, fallback = '') {\n if (typeof opt === 'function') return opt(this.model) ?? fallback;\n return opt ?? fallback;\n }\n\n _resolveTone() {\n const t = this._resolve(this._toneOpt, 'default');\n return VALID_TONES.has(t) ? t : 'default';\n }\n\n _resolveActions() {\n const arr = this._resolve(this._actionsOpt, []) || [];\n return Array.isArray(arr) ? arr.filter(Boolean) : [];\n }\n\n // ── Rendering ──────────────────────────────────────────────\n\n _buildTemplate() {\n const tone = this._resolveTone();\n const state = String(this._resolve(this._stateOpt, ''));\n const headline = String(this._resolve(this._headlineOpt, ''));\n const meta = this._resolve(this._metaOpt, ''); // trusted HTML\n const icon = this._resolve(this._iconOpt, null);\n const actions = this._resolveActions();\n\n const iconHtml = icon\n ? `<i class=\"bi ${this.escapeHtml(String(icon))} detail-status-icon\"></i>`\n : '<span class=\"detail-status-dot\"></span>';\n\n const actionsHtml = actions.map(a => {\n const variant = a.variant || 'primary';\n const btnClass = variant.startsWith('outline-')\n ? `btn-${variant}`\n : `btn-${variant}`;\n const aIcon = a.icon ? `<i class=\"bi ${this.escapeHtml(a.icon)} me-1\"></i>` : '';\n return `<button class=\"btn ${btnClass} btn-sm\" data-action=\"${this.escapeHtml(a.action || '')}\" type=\"button\">${aIcon}${this.escapeHtml(a.label || '')}</button>`;\n }).join('');\n\n return `\n <div class=\"detail-status-headline\">\n ${state ? `<div class=\"detail-status-state\">${iconHtml}${this.escapeHtml(state)}</div>` : ''}\n ${headline ? `<div class=\"detail-status-line\">${this.escapeHtml(headline)}</div>` : ''}\n ${meta ? `<div class=\"detail-status-meta\">${meta}</div>` : ''}\n </div>\n ${actions.length ? `<div class=\"detail-status-actions\">${actionsHtml}</div>` : ''}\n `.trim();\n }\n\n async onAfterRender() {\n await super.onAfterRender();\n if (!this.element) return;\n\n // Apply tone class fresh on every render (clear any stale tone-* class\n // first) so a state change that flips tone shows up correctly.\n VALID_TONES.forEach(t => this.element.classList.remove(`tone-${t}`));\n const tone = this._resolveTone();\n if (tone && tone !== 'default') {\n this.element.classList.add(`tone-${tone}`);\n }\n }\n}\n\n// Stylesheet for StatusPanel lives in src/core/css/core.css under \"StatusPanel\".\n\nexport default StatusPanel;\nexport { StatusPanel };\n","/**\n * Timeline - Vertical event-feed primitive.\n *\n * A vertical activity feed with a hairline connector and tone-colored\n * dots. Used for incident history, job lifecycle events, recent\n * activity in user / group overviews, audit trails — anywhere the\n * record's story is \"this happened, then this, then this.\"\n *\n * Security note: each item's `detail` field is rendered as TRUSTED\n * HTML so callers can interpolate `<code>`, `<a>`, `<strong>`, etc.\n * The trust contract is \"the string was assembled in source code, not\n * pulled from user input.\" If you build `detail` from event payloads,\n * user IDs, free-text fields, or any other user-controlled data, you\n * MUST escape them yourself (e.g. via `MOJOUtils.escapeHtml(...)`).\n * `headline`, `when`, and `emptyText` are escaped automatically.\n *\n * ┌──────────────────────────────────────────────────┐\n * │ ● Headline line 4m ago │\n * │ │ Optional supporting detail │\n * │ │\n * │ ● Headline line 1h ago │\n * │ │ Detail line │\n * └──────────────────────────────────────────────────┘\n *\n * @example\n * const tl = new Timeline({\n * items: model.getEvents().map(ev => ({\n * tone: EVENT_TONE[ev.event] || 'default',\n * headline: ev.label || ev.event,\n * detail: ev.details || '',\n * when: formatRelative(ev.at)\n * }))\n * });\n * parent.addChild(tl, { containerId: 'lifecycle' });\n *\n * `items` may be an array OR a function of `model` (the standard\n * primitive pattern). When function-valued, the items list\n * re-resolves on every render so the feed reflects the latest model\n * state.\n *\n * Empty timeline renders the `emptyText` (defaults to a muted\n * \"No events yet\" placeholder).\n */\n\nimport View from '@core/View.js';\n\nconst VALID_TONES = new Set(['default', 'primary', 'success', 'info', 'warning', 'danger', 'secondary']);\n\nclass Timeline extends View {\n constructor(options = {}) {\n const {\n items = [],\n emptyText = 'No events yet.',\n limit = null, // optional max items rendered\n ...viewOptions\n } = options;\n\n super({\n tagName: 'ol',\n className: 'detail-timeline',\n ...viewOptions\n });\n\n this._itemsOpt = items;\n this.emptyText = emptyText;\n this.limit = (typeof limit === 'number' && limit > 0) ? Math.floor(limit) : null;\n\n this.template = () => this._buildTemplate();\n }\n\n // ── Resolvers ──────────────────────────────────────────────\n\n _resolveItems() {\n const raw = (typeof this._itemsOpt === 'function')\n ? this._itemsOpt(this.model)\n : this._itemsOpt;\n if (!Array.isArray(raw)) return [];\n const filtered = raw.filter(Boolean);\n return this.limit ? filtered.slice(0, this.limit) : filtered;\n }\n\n _normalizeTone(tone) {\n return VALID_TONES.has(tone) ? tone : null;\n }\n\n // ── Rendering ──────────────────────────────────────────────\n\n _buildTemplate() {\n const items = this._resolveItems();\n\n if (!items.length) {\n // The class on the root <ol> is fine; we render a fallback <li>\n // so the empty state still uses the timeline rail visually.\n return `<li class=\"detail-timeline-empty text-secondary small\">${this.escapeHtml(this.emptyText)}</li>`;\n }\n\n return items.map(it => {\n const tone = this._normalizeTone(it.tone);\n const toneCls = tone ? ` tone-${tone}` : '';\n const headline = String(it.headline ?? it.label ?? '');\n const detail = it.detail != null ? String(it.detail) : ''; // trusted HTML\n const when = it.when != null ? String(it.when) : '';\n\n return `\n <li class=\"detail-timeline-item${toneCls}\">\n <div>\n <div class=\"detail-timeline-headline\">${this.escapeHtml(headline)}</div>\n ${detail ? `<div class=\"detail-timeline-detail\">${detail}</div>` : ''}\n </div>\n ${when ? `<span class=\"detail-timeline-when\">${this.escapeHtml(when)}</span>` : ''}\n </li>\n `;\n }).join('');\n }\n\n /**\n * Replace the items source and re-render. Accepts the same shape\n * as the constructor's `items` option (array or function).\n */\n setItems(items) {\n this._itemsOpt = items ?? [];\n if (this.element) {\n return this.render();\n }\n }\n}\n\n// Stylesheet for Timeline lives in src/core/css/core.css under \"Timeline\".\n\nexport default Timeline;\nexport { Timeline };\n","/**\n * KnownFieldsCard - \"Known JSON keys promoted, raw blob below\" pattern.\n *\n * Many records carry blob-shaped JSON fields — `metadata`, `ip_info`,\n * `device_info`, `payload`, `og_metadata` — that contain a few keys\n * the framework knows about plus an open-ended bag of extras. The\n * Detail / Metadata sections of admin views typically want to:\n *\n * 1. Promote the known keys to a clean 2-column label/value layout.\n * 2. Keep the raw JSON accessible but visually subordinated.\n *\n * That's the pattern this primitive captures. Built on the existing\n * `.detail-flat-row` family for the known-keys grid and a native\n * `<details>` for the collapsible raw blob.\n *\n * ┌──────────────────────────────────────────────┐\n * │ Created by ian@example.com │\n * │ Reasoning brute-force from same /24 │\n * │ Last resolved 2026-04-21 11:42 │\n * │ │\n * │ ▶ Raw metadata │\n * └──────────────────────────────────────────────┘\n *\n * @example\n * const card = new KnownFieldsCard({\n * data: model.get('metadata') || {}, // OR (model) => object\n * knownKeys: [\n * { key: 'created_by', label: 'Created by' },\n * { key: 'last_resolved', label: 'Resolved', formatter: 'datetime' },\n * { key: 'reasoning', label: 'Reasoning' },\n * { key: 'agent_prompt', label: 'Agent prompt',\n * formatter: (v) => `<code>${escapeHtml(v)}</code>` }\n * ],\n * rawLabel: 'Raw metadata',\n * rawCollapsed: true\n * });\n * parent.addChild(card, { containerId: 'metadata-card' });\n *\n * `data` may be a plain object OR a function of `model`. `knownKeys`\n * may be a plain array OR a function of `model` for state-dependent\n * key sets.\n *\n * `formatter` per known-key may be:\n * - a string — looked up via DataFormatter.apply (e.g. 'datetime',\n * 'relative', 'filesize', 'phone'). Returns trusted HTML.\n * - a function `(value, key, data) => string` — returns trusted HTML.\n * - omitted — the value is rendered as escaped text via String(value).\n *\n * Missing keys render with the muted \"—\" placeholder so the row grid\n * stays visually consistent. Pass `hideEmpty: true` on a key to omit\n * the row entirely when the value is null/undefined/''.\n */\n\nimport View from '@core/View.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\nclass KnownFieldsCard extends View {\n constructor(options = {}) {\n const {\n data = {},\n knownKeys = [],\n rawCollapsed = true,\n rawLabel = 'Raw JSON',\n emptyText = 'No data.',\n showRaw = true,\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'detail-known-fields-card',\n ...viewOptions\n });\n\n this._dataOpt = data;\n this._knownKeysOpt = knownKeys;\n this.rawCollapsed = rawCollapsed !== false;\n this.rawLabel = rawLabel;\n this.emptyText = emptyText;\n this.showRaw = showRaw !== false;\n\n this.template = () => this._buildTemplate();\n }\n\n // ── Resolvers ──────────────────────────────────────────────\n\n _resolveData() {\n const raw = (typeof this._dataOpt === 'function')\n ? this._dataOpt(this.model)\n : this._dataOpt;\n return (raw && typeof raw === 'object') ? raw : {};\n }\n\n _resolveKnownKeys() {\n const raw = (typeof this._knownKeysOpt === 'function')\n ? this._knownKeysOpt(this.model)\n : this._knownKeysOpt;\n return Array.isArray(raw) ? raw.filter(Boolean) : [];\n }\n\n // ── Rendering ──────────────────────────────────────────────\n\n _buildTemplate() {\n const data = this._resolveData();\n const knownKeys = this._resolveKnownKeys();\n const isEmpty = !knownKeys.length && Object.keys(data).length === 0;\n\n if (isEmpty) {\n return `<div class=\"text-secondary small\">${this.escapeHtml(this.emptyText)}</div>`;\n }\n\n const rowsHtml = knownKeys\n .map(spec => this._renderRow(spec, data))\n .filter(Boolean)\n .join('');\n\n const knownHtml = rowsHtml\n ? `<div class=\"detail-known-fields-grid\">${rowsHtml}</div>`\n : '';\n\n const rawHtml = this.showRaw\n ? this._renderRaw(data)\n : '';\n\n return `${knownHtml}${rawHtml}`;\n }\n\n _renderRow(spec, data) {\n if (!spec || !spec.key) return '';\n\n const value = this._lookup(data, spec.key);\n const isMissing = value == null || value === '';\n\n if (isMissing && spec.hideEmpty) return '';\n\n const label = String(spec.label ?? spec.key);\n const valueHtml = isMissing\n ? '<span class=\"text-secondary fst-italic\">—</span>'\n : this._formatValue(value, spec, data);\n\n return `\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">${this.escapeHtml(label)}</div>\n <div class=\"detail-flat-row-value\">${valueHtml}</div>\n </div>\n `;\n }\n\n _renderRaw(data) {\n // Empty object — skip raw block entirely.\n if (!data || Object.keys(data).length === 0) return '';\n\n let json;\n try {\n json = JSON.stringify(data, null, 2);\n } catch (err) {\n json = String(data);\n }\n\n const openAttr = this.rawCollapsed ? '' : ' open';\n return `\n <details class=\"detail-known-fields-raw\"${openAttr}>\n <summary class=\"detail-known-fields-raw-summary\">${this.escapeHtml(this.rawLabel)}</summary>\n <pre class=\"detail-known-fields-raw-body\">${this.escapeHtml(json)}</pre>\n </details>\n `;\n }\n\n // Looks up a dotted path on the data object so knownKeys can reference\n // nested keys like `os.family`. Falls back to a flat lookup when the\n // path doesn't traverse anything.\n _lookup(data, key) {\n if (!key || data == null) return undefined;\n if (Object.prototype.hasOwnProperty.call(data, key)) return data[key];\n if (key.indexOf('.') === -1) return undefined;\n const parts = key.split('.');\n let cursor = data;\n for (const part of parts) {\n if (cursor == null || typeof cursor !== 'object') return undefined;\n cursor = cursor[part];\n }\n return cursor;\n }\n\n _formatValue(value, spec, data) {\n const formatter = spec.formatter;\n\n if (typeof formatter === 'function') {\n try {\n const out = formatter(value, spec.key, data);\n return out == null ? '' : String(out);\n } catch (err) {\n return this.escapeHtml(String(value));\n }\n }\n\n if (typeof formatter === 'string' && formatter.length) {\n try {\n const out = dataFormatter.apply(formatter, value);\n return out == null ? '' : String(out);\n } catch (err) {\n return this.escapeHtml(String(value));\n }\n }\n\n // Default — escaped scalar / JSON.\n if (value && typeof value === 'object') {\n return `<code class=\"text-secondary\">${this.escapeHtml(JSON.stringify(value))}</code>`;\n }\n return this.escapeHtml(String(value));\n }\n}\n\n// Stylesheet for KnownFieldsCard lives in src/core/css/core.css under \"KnownFieldsCard\".\n\nexport default KnownFieldsCard;\nexport { KnownFieldsCard };\n","import View from '@core/View.js';\n\nclass FilePreviewView extends View {\n constructor(options = {}) {\n super({\n className: 'file-preview',\n ...options\n });\n this.file = options.file || {};\n this.isImage = this.file.content_type?.startsWith('image/');\n this.isPdf = this.file.content_type === 'application/pdf';\n }\n\n getTemplate() {\n return `\n <div class=\"file-preview-item card card-body p-2 mt-2\">\n <div class=\"d-flex align-items-center\">\n <div class=\"flex-shrink-0\">\n ${this.isImage ? `<img src=\"${this.file.thumbnailUrl || this.file.url}\" class=\"rounded\" style=\"width: 40px; height: 40px; object-fit: cover;\">` : `<i class=\"bi bi-file-earmark-text fs-2 text-secondary\"></i>`}\n </div>\n <div class=\"flex-grow-1 ms-3\">\n <div class=\"fw-bold text-truncate\">{{file.filename}}</div>\n <div class=\"small text-muted\">{{file.file_size|filesize}}</div>\n </div>\n <div class=\"flex-shrink-0\">\n <button class=\"btn btn-sm btn-outline-primary\" data-action=\"view-file\">View</button>\n </div>\n </div>\n </div>\n `;\n }\n\n async onActionViewFile() {\n if (this.isImage) {\n // Check if lightbox extension is available\n const LightboxGallery = window.MOJO?.plugins?.LightboxGallery;\n \n if (LightboxGallery) {\n LightboxGallery.show({ src: this.file.url, alt: this.file.filename });\n } else {\n // Fallback: open in new tab\n window.open(this.file.url, '_blank');\n }\n } else if (this.isPdf) {\n // Check if lightbox extension is available\n const PDFViewer = window.MOJO?.plugins?.PDFViewer;\n \n if (PDFViewer) {\n PDFViewer.showDialog(this.file.url, { title: this.file.filename });\n } else {\n // Fallback: open in new tab\n window.open(this.file.url, '_blank');\n }\n } else {\n window.open(this.file.url, '_blank');\n }\n }\n}\n\nexport default FilePreviewView;\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/* =========================\n * Constants\n * ========================= */\n\nconst SHORTLINK_SOURCE_OPTIONS = [\n { value: 'admin', label: 'Admin' },\n { value: 'email', label: 'Email' },\n { value: 'sms', label: 'SMS' },\n { value: 'push', label: 'Push' },\n { value: 'fileman', label: 'File Manager' },\n { value: 'api', label: 'API' },\n { value: 'other', label: 'Other' },\n];\n\nconst TWITTER_CARD_OPTIONS = [\n { value: '', label: '— None —' },\n { value: 'summary', label: 'summary' },\n { value: 'summary_large_image', label: 'summary_large_image' },\n];\n\n/* =========================\n * Metadata helpers\n * ========================= */\n\n/**\n * Flatten a ShortLink metadata object (keys like \"og:title\") into sibling form\n * fields (og_title, twitter_card, ...). Used to seed edit dialogs.\n */\nfunction flattenShortLinkMetadata(metadata = {}) {\n const m = metadata || {};\n return {\n og_title: m['og:title'] || '',\n og_description: m['og:description'] || '',\n og_image: m['og:image'] || '',\n twitter_card: m['twitter:card'] || '',\n twitter_title: m['twitter:title'] || '',\n twitter_description: m['twitter:description'] || '',\n twitter_image: m['twitter:image'] || '',\n };\n}\n\n/**\n * Inverse of flattenShortLinkMetadata — combines og_ and twitter_ form fields\n * into a flat metadata object with colon-keyed keys. Empty values are dropped.\n */\nfunction buildShortLinkMetadata(formData = {}) {\n const map = {\n og_title: 'og:title',\n og_description: 'og:description',\n og_image: 'og:image',\n twitter_card: 'twitter:card',\n twitter_title: 'twitter:title',\n twitter_description: 'twitter:description',\n twitter_image: 'twitter:image',\n };\n const metadata = {};\n for (const [flatKey, targetKey] of Object.entries(map)) {\n const v = formData[flatKey];\n if (v !== undefined && v !== null && v !== '') {\n metadata[targetKey] = v;\n }\n }\n return metadata;\n}\n\n/**\n * Pull og_ and twitter_ fields out of a form-data object and collapse them into\n * `metadata`. Returns a new object safe to send to the REST API. If no OG or\n * Twitter fields are set, `metadata` is omitted entirely so the backend's\n * auto-scraper can fill in the gaps.\n */\nfunction extractShortLinkPayload(formData = {}) {\n const flatKeys = [\n 'og_title', 'og_description', 'og_image',\n 'twitter_card', 'twitter_title', 'twitter_description', 'twitter_image',\n ];\n const payload = { ...formData };\n for (const k of flatKeys) delete payload[k];\n const metadata = buildShortLinkMetadata(formData);\n if (Object.keys(metadata).length > 0) {\n payload.metadata = metadata;\n }\n return payload;\n}\n\n/* =========================\n * ShortLink Model\n * ========================= */\n\nclass ShortLink extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/shortlink/link',\n });\n }\n}\n\n/* =========================\n * ShortLink Collection\n * ========================= */\n\nclass ShortLinkList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ShortLink,\n endpoint: '/api/shortlink/link',\n ...options,\n });\n }\n}\n\n/* =========================\n * ShortLinkClick (read-only)\n * ========================= */\n\nclass ShortLinkClick extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/shortlink/history',\n });\n }\n}\n\nclass ShortLinkClickList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ShortLinkClick,\n endpoint: '/api/shortlink/history',\n ...options,\n });\n }\n}\n\n/* =========================\n * Forms\n * ========================= */\n\nconst _shortLinkSharedFields = [\n { name: 'url', type: 'url', label: 'Destination URL', required: true, placeholder: 'https://example.com/page', cols: 12 },\n { name: 'source', type: 'select', label: 'Source', options: SHORTLINK_SOURCE_OPTIONS, value: 'admin', cols: 6 },\n { name: 'expire_days', type: 'number', label: 'Expire (days)', value: 3, min: 0, cols: 3, help: '0 = never' },\n { name: 'expire_hours', type: 'number', label: 'Expire (hours)', value: 0, min: 0, cols: 3 },\n { name: 'track_clicks', type: 'switch', label: 'Track clicks', value: false, cols: 4, help: 'Record per-click history and per-link metrics.' },\n { name: 'bot_passthrough', type: 'switch', label: 'Bypass bot preview', value: false, cols: 4, help: 'Bots receive a plain redirect (use for transactional links).' },\n { name: 'is_protected', type: 'switch', label: 'Protected', value: false, cols: 4, help: 'Prevents accidental deletion.' },\n // ── OG Metadata (optional) ──\n { type: 'heading', label: 'OpenGraph Preview (optional)', cols: 12 },\n { name: 'og_title', type: 'text', label: 'og:title', placeholder: 'Shown in Slack/iMessage preview', cols: 12 },\n { name: 'og_description', type: 'textarea', label: 'og:description', rows: 2, cols: 12 },\n { name: 'og_image', type: 'url', label: 'og:image', placeholder: 'https://example.com/preview.jpg', cols: 12 },\n];\n\nconst ShortLinkForms = {\n create: {\n title: 'Create Shortlink',\n size: 'md',\n fields: [\n ..._shortLinkSharedFields,\n ],\n help: 'Leave OG fields blank to let the server scrape the destination automatically.',\n },\n\n edit: {\n title: 'Edit Shortlink',\n size: 'md',\n fields: [\n { name: 'is_active', type: 'switch', label: 'Active', cols: 4 },\n ..._shortLinkSharedFields,\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 },\n};\n\nShortLink.EDIT_FORM = ShortLinkForms.edit;\n\nexport {\n ShortLink,\n ShortLinkList,\n ShortLinkClick,\n ShortLinkClickList,\n ShortLinkForms,\n SHORTLINK_SOURCE_OPTIONS,\n TWITTER_CARD_OPTIONS,\n flattenShortLinkMetadata,\n buildShortLinkMetadata,\n extractShortLinkPayload,\n};\n","/**\n * FileView - Unified viewer for fileman File records\n *\n * Canonical viewer for `File` models returned by /api/fileman/file. Mirrors the\n * UserView pattern: a header block with a ContextMenu, plus a SideNavView that\n * switches between Preview / Details / Renditions / Metadata sections.\n *\n * Preview is driven by the backend `category` field (image, video, audio, pdf,\n * document, spreadsheet, presentation, archive, other). Each category has a\n * defined preview experience. Image and PDF previews delegate to the optional\n * lightbox extension (`window.MOJO.plugins.LightboxGallery` / `PDFViewer`);\n * when that extension isn't loaded, actions fall back to `window.open`.\n *\n * Features:\n * - Category-aware Preview section (inline <video>/<audio>, lightbox, pdf viewer)\n * - DataView-powered Details with all file metadata\n * - TableView-powered Renditions (hidden when there are no renditions)\n * - Auto-generated Metadata DataView from `model.metadata` (hidden when empty)\n * - ContextMenu actions: View, Download, Edit Details, Make Public/Private,\n * Copy URL, Delete\n * - Emits `file:deleted` after successful delete\n */\n\nimport View from '@core/View.js';\nimport DetailView from '@core/views/data/DetailView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport MetricCard from '@core/views/data/MetricCard.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\nimport { File, FileForms } from '@core/models/Files.js';\nimport { ShortLinkList } from '@core/models/ShortLink.js';\n\n// CATEGORY_CONFIG.badgeClass → DetailHeaderView iconTone mapping. Anything\n// not in here renders without tone tinting (the regular dh-icon background).\nconst CATEGORY_TONE = {\n 'bg-info': 'info',\n 'bg-primary': 'primary',\n 'bg-danger': 'danger',\n 'bg-success': 'success',\n 'bg-warning': 'warning'\n};\n\nfunction _capitalize(s) {\n if (!s) return '';\n const str = String(s);\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Category → preview config\n// ──────────────────────────────────────────────────────────────────────────\n\nconst CATEGORY_CONFIG = {\n image: { icon: 'bi-image', previewType: 'image', badgeClass: 'bg-info' },\n video: { icon: 'bi-camera-video', previewType: 'video', badgeClass: 'bg-primary' },\n audio: { icon: 'bi-music-note-beamed', previewType: 'audio', badgeClass: 'bg-primary' },\n pdf: { icon: 'bi-file-earmark-pdf', previewType: 'pdf', badgeClass: 'bg-danger' },\n document: { icon: 'bi-file-earmark-text', previewType: 'document', badgeClass: 'bg-secondary' },\n spreadsheet: { icon: 'bi-file-earmark-spreadsheet', previewType: 'document', badgeClass: 'bg-success' },\n presentation: { icon: 'bi-file-earmark-slides', previewType: 'document', badgeClass: 'bg-warning' },\n archive: { icon: 'bi-file-earmark-zip', previewType: 'download', badgeClass: 'bg-dark' },\n other: { icon: 'bi-file-earmark', previewType: 'download', badgeClass: 'bg-secondary' }\n};\n\nfunction getCategoryConfig(model) {\n const cat = (model && typeof model.getCategory === 'function')\n ? model.getCategory()\n : (model?.get?.('category') || 'other');\n return CATEGORY_CONFIG[cat] || CATEGORY_CONFIG.other;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FilePreviewSection — category-aware preview (inside SideNavView content)\n//\n// NOTE: SideNavView assigns `view.parent = this` to its section views\n// (SideNavView.js:102), so events bubble to SideNavView, not FileView.\n// Action handlers are therefore duplicated here; both touch only\n// `this.model` + `window.MOJO.plugins`, so there is no cross-talk risk.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FilePreviewSection extends View {\n constructor(options = {}) {\n // Pass `model` through — View.setModel wires up the 'change'\n // listener automatically, no need to duplicate it here.\n super({\n className: 'file-preview-section p-3',\n ...options\n });\n this.categoryConfig = options.categoryConfig || CATEGORY_CONFIG.other;\n }\n\n /**\n * Override the base re-render-on-model-change behavior.\n * - video/audio elements must NOT re-render: the rendition poll fires\n * model `change` every 5s while transcoding, and re-rendering would\n * destroy and recreate the media element on every tick (visible\n * reload cycle, lost playback state).\n * - image/pdf/document/archive previews are idempotent — re-render\n * safely to pick up new rendition URLs as they arrive.\n */\n _onModelChange() {\n const type = this.categoryConfig?.previewType;\n if (type === 'video' || type === 'audio') return;\n if (this.isMounted()) this.render();\n }\n\n getTemplate() {\n const type = this.categoryConfig.previewType;\n const url = this.model.get('url') || '';\n const filename = this.model.get('filename') || '';\n\n if (type === 'image') {\n const previewUrl = (this.model.getThumbnailUrl && this.model.getThumbnailUrl()) || url;\n return `\n <div class=\"text-center\">\n <img src=\"${escapeAttr(previewUrl)}\"\n alt=\"${escapeAttr(filename)}\"\n class=\"img-fluid rounded shadow-sm\"\n style=\"max-height: 70vh; cursor: zoom-in;\"\n data-action=\"view-file\" role=\"button\">\n <div class=\"text-muted small mt-2\">Click image for full view</div>\n </div>\n `;\n }\n\n if (type === 'video') {\n const poster = this.model.getThumbnailUrl && this.model.getThumbnailUrl();\n return `\n <div class=\"text-center\">\n <video controls preload=\"metadata\"\n src=\"${escapeAttr(url)}\"\n ${poster ? `poster=\"${escapeAttr(poster)}\"` : ''}\n style=\"width: 100%; max-height: 70vh; background:#000;\"></video>\n </div>\n `;\n }\n\n if (type === 'audio') {\n return `\n <div class=\"p-4 bg-light rounded text-center\">\n <i class=\"bi ${this.categoryConfig.icon} display-4 text-secondary\"></i>\n <h5 class=\"mt-3 mb-3 text-break\">${escapeHtml(filename)}</h5>\n <audio controls class=\"w-100\" src=\"${escapeAttr(url)}\"></audio>\n </div>\n `;\n }\n\n if (type === 'pdf') {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi ${this.categoryConfig.icon} text-danger\" style=\"font-size: 5rem;\"></i>\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <div class=\"mt-4\">\n <button type=\"button\" class=\"btn btn-primary me-2\" data-action=\"view-file\">\n <i class=\"bi bi-eye me-1\"></i>Open PDF Viewer\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n if (type === 'document') {\n const preview = this.model.getBestImageRendition && this.model.getBestImageRendition();\n return `\n <div class=\"text-center p-4 bg-light rounded\">\n ${preview\n ? `<img src=\"${escapeAttr(preview.url)}\" alt=\"${escapeAttr(filename)} preview\" class=\"img-fluid rounded mb-3\" style=\"max-height: 50vh;\">`\n : `<i class=\"bi ${this.categoryConfig.icon} text-secondary\" style=\"font-size: 5rem;\"></i>`}\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary me-2\" data-action=\"view-file\">\n <i class=\"bi bi-box-arrow-up-right me-1\"></i>Open\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n // archive / other / download fallback\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi ${this.categoryConfig.icon} text-secondary\" style=\"font-size: 5rem;\"></i>\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <p class=\"text-muted small\">No inline preview available for this file type.</p>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n async onActionViewFile() {\n openFileInPreview(this.model, this.categoryConfig);\n }\n\n async onActionDownloadFile() {\n downloadFile(this.model);\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileRenditionsSection — Renditions gallery\n//\n// Renders each rendition as a card tile: inline preview (image thumbnail,\n// video poster with play overlay, or category icon), role badge, dimensions\n// + size, and three actions (Preview, Copy URL, Download).\n//\n// Three states:\n// - Gallery grid when renditions exist\n// - Processing placeholder when upload_status === 'completed' and the\n// backend worker hasn't populated renditions yet\n// - \"Upload in progress\" placeholder otherwise\n//\n// `model:` flows through to View.setModel which wires the built-in\n// 'change' → _onModelChange → render() listener (guarded by isMounted).\n// That's exactly what we want: swap placeholder for the grid, or add new\n// rendition cards, as the rendition poll brings them in.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileRenditionsSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-renditions-section p-3',\n ...options\n });\n }\n\n getTemplate() {\n if (this.model.hasRenditions && this.model.hasRenditions()) {\n return this._buildGalleryTemplate();\n }\n if (this.model.isUploadPending && this.model.isUploadPending()) {\n return this._buildWaitingTemplate();\n }\n // Upload finished, but the rendition map is empty — no work is in\n // progress (per the backend: completed === done). Show a clean empty\n // state with a Regenerate button instead of an indefinite spinner.\n return this._buildEmptyTemplate();\n }\n\n _buildGalleryTemplate() {\n const renditions = this.model.getRenditions();\n const cards = renditions.map(r => this._buildCard(r)).join('');\n const count = renditions.length;\n return `\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"text-muted small\">\n ${count} rendition${count === 1 ? '' : 's'}\n </div>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"refresh-renditions\" title=\"Refresh list\">\n <i class=\"bi bi-arrow-clockwise\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"regenerate-from-section\" title=\"Rebuild all previews\">\n <i class=\"bi bi-arrow-repeat me-1\"></i>Regenerate\n </button>\n </div>\n </div>\n <div class=\"row g-3\">${cards}</div>\n `;\n }\n\n _buildCard(r) {\n const url = r && r.url ? r.url : '';\n const ct = r && typeof r.content_type === 'string' ? r.content_type : '';\n const role = r && r.role ? r.role : 'rendition';\n const filename = r && r.filename ? r.filename : role;\n const size = r && r.file_size ? formatBytes(r.file_size) : '';\n const dimensions = (r && r.width && r.height) ? `${r.width} × ${r.height}` : '';\n\n const viewData = [\n `data-action=\"view-rendition\"`,\n `data-url=\"${escapeAttr(url)}\"`,\n `data-ct=\"${escapeAttr(ct)}\"`,\n `data-filename=\"${escapeAttr(filename)}\"`,\n `data-role=\"${escapeAttr(role)}\"`\n ].join(' ');\n\n let preview;\n if (ct.startsWith('image/') && url) {\n preview = `<img src=\"${escapeAttr(url)}\" alt=\"${escapeAttr(role)}\"\n loading=\"lazy\"\n class=\"w-100\"\n style=\"height: 140px; object-fit: cover; background: #f8f9fa; border-top-left-radius: var(--bs-card-inner-border-radius); border-top-right-radius: var(--bs-card-inner-border-radius);\">`;\n } else if (ct.startsWith('video/')) {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center position-relative\"\n style=\"height: 140px; background: linear-gradient(135deg, #212529 0%, #343a40 100%); color: #fff;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-play-circle-fill\" style=\"font-size: 2.75rem; opacity: 0.9;\"></i>\n <span class=\"position-absolute bottom-0 start-0 end-0 text-center small py-1\"\n style=\"background: rgba(0,0,0,0.35); font-variant-numeric: tabular-nums;\">\n ${escapeHtml(ct)}\n </span>\n </div>`;\n } else if (ct.startsWith('audio/')) {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center\"\n style=\"height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-music-note-beamed text-secondary\" style=\"font-size: 2.5rem;\"></i>\n </div>`;\n } else {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center\"\n style=\"height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-file-earmark text-secondary\" style=\"font-size: 2.5rem;\"></i>\n </div>`;\n }\n\n const footer = url ? `\n <div class=\"card-footer p-1 d-flex gap-1 bg-white border-top-0\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary flex-fill\"\n ${viewData}\n title=\"Preview\">\n <i class=\"bi bi-eye\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary flex-fill\"\n data-action=\"copy-rendition-url\" data-url=\"${escapeAttr(url)}\"\n title=\"Copy URL\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n <a href=\"${escapeAttr(url)}\" download=\"${escapeAttr(filename)}\"\n class=\"btn btn-sm btn-outline-secondary flex-fill\"\n title=\"Download\"\n data-stop-propagation>\n <i class=\"bi bi-download\"></i>\n </a>\n </div>\n ` : '';\n\n return `\n <div class=\"col-sm-6 col-md-4 col-lg-3\">\n <div class=\"card h-100 shadow-sm rendition-card\">\n <div ${url ? viewData : ''} ${url ? 'role=\"button\" style=\"cursor: pointer;\"' : ''}>\n ${preview}\n </div>\n <div class=\"card-body p-2\">\n <div class=\"d-flex justify-content-between align-items-start gap-2 mb-1\">\n <span class=\"badge bg-secondary text-truncate\" style=\"max-width: 100%;\" title=\"${escapeAttr(role)}\">${escapeHtml(role)}</span>\n ${size ? `<small class=\"text-muted flex-shrink-0\" style=\"font-variant-numeric: tabular-nums;\">${escapeHtml(size)}</small>` : ''}\n </div>\n ${dimensions ? `<div class=\"small text-muted\" style=\"font-variant-numeric: tabular-nums;\">${escapeHtml(dimensions)}</div>` : ''}\n </div>\n ${footer}\n </div>\n </div>\n `;\n }\n\n _buildEmptyTemplate() {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi bi-layers display-6 text-muted\"></i>\n <h6 class=\"mt-3 mb-1\">No renditions for this file</h6>\n <p class=\"text-muted small mb-3\">\n Click <strong>Regenerate</strong> to (re)build thumbnails and previews on the backend.\n </p>\n <div class=\"d-inline-flex gap-2\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" data-action=\"refresh-renditions\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Refresh\n </button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" data-action=\"regenerate-from-section\">\n <i class=\"bi bi-arrow-repeat me-1\"></i>Regenerate\n </button>\n </div>\n </div>\n `;\n }\n\n _buildWaitingTemplate() {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi bi-hourglass-split display-6 text-muted\"></i>\n <h6 class=\"mt-3 mb-1\">Upload still in progress</h6>\n <p class=\"text-muted small mb-0\">Renditions will be generated once the upload completes.</p>\n </div>\n `;\n }\n\n // ── Actions ─────────────────────────────────────\n\n async onActionRefreshRenditions() {\n try {\n await this.model.fetch();\n } catch (err) {\n console.warn('FileView: refresh-renditions fetch failed:', err);\n }\n }\n\n async onActionViewRendition(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const url = element.dataset.url;\n const ct = element.dataset.ct || '';\n if (!url) return;\n\n if (ct.startsWith('image/')) {\n const Lightbox = typeof window !== 'undefined' ? window.MOJO?.plugins?.LightboxGallery : null;\n if (Lightbox && typeof Lightbox.show === 'function') {\n const images = this.model.getRenditions()\n .filter(r => r && r.url && typeof r.content_type === 'string' && r.content_type.startsWith('image/'))\n .map(r => ({ src: r.url, alt: r.role || '' }));\n const startIndex = Math.max(0, images.findIndex(img => img.src === url));\n Lightbox.show(images, { startIndex, fitToScreen: false });\n return;\n }\n }\n // Non-image renditions (video, audio, anything else) open in a new tab\n // — browsers have native players for these and the lightbox plugin\n // only handles images.\n window.open(url, '_blank', 'noopener');\n }\n\n async onActionCopyRenditionUrl(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const url = element.dataset.url;\n if (!url) return;\n try {\n if (navigator.clipboard && window.isSecureContext) {\n await navigator.clipboard.writeText(url);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = url;\n document.body.appendChild(textarea);\n textarea.select();\n document.execCommand('copy');\n document.body.removeChild(textarea);\n }\n // Short-lived visual confirmation on the clicked button\n const icon = element.querySelector('i');\n if (icon) {\n const orig = icon.className;\n icon.className = 'bi bi-check-lg text-success';\n setTimeout(() => { icon.className = orig; }, 1200);\n }\n this.getApp()?.toast?.success?.('URL copied to clipboard');\n } catch (err) {\n console.error('Failed to copy rendition URL:', err);\n this.getApp()?.toast?.error?.('Failed to copy URL');\n }\n }\n\n // Delegates up the parent chain to FileView.onActionRegenerateRenditions\n // so the section button behaves identically to the ContextMenu item.\n async onActionRegenerateFromSection() {\n let node = this.parent;\n while (node) {\n if (typeof node.onActionRegenerateRenditions === 'function') {\n return node.onActionRegenerateRenditions();\n }\n node = node.parent;\n }\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Shared action helpers (also used by FileView's ContextMenu-triggered actions)\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction openFileInPreview(model, categoryConfig) {\n const url = model.get('url');\n if (!url) return;\n const type = categoryConfig.previewType;\n\n if (type === 'image') {\n const Lightbox = typeof window !== 'undefined' ? window.MOJO?.plugins?.LightboxGallery : null;\n const renditions = model.get('renditions') || {};\n const images = [\n { src: url, alt: 'Original' },\n ...Object.values(renditions)\n .filter(r => r && r.url && typeof r.content_type === 'string' && r.content_type.startsWith('image/'))\n .map(r => ({ src: r.url, alt: r.role || '' }))\n ];\n if (Lightbox && typeof Lightbox.show === 'function') {\n Lightbox.show(images, { fitToScreen: false });\n } else {\n window.open(url, '_blank');\n }\n return;\n }\n\n if (type === 'pdf') {\n const PDFViewer = typeof window !== 'undefined' ? window.MOJO?.plugins?.PDFViewer : null;\n if (PDFViewer && typeof PDFViewer.showDialog === 'function') {\n PDFViewer.showDialog(url, { title: model.get('filename') });\n } else {\n window.open(url, '_blank');\n }\n return;\n }\n\n window.open(url, '_blank');\n}\n\nfunction downloadFile(model) {\n const url = model.get('url');\n if (!url) return;\n const a = document.createElement('a');\n a.href = url;\n a.download = model.get('filename') || '';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileSharesSection — Active shortlink shares for this file\n//\n// Lists ShortLink rows where source=fileman-share and file=<this.id>. Each\n// share is a distinct shortlink minted by `model.share({...})` (see\n// Files.js). Revoking a share flips `is_active=false`; the audit row is\n// preserved per the backend contract — never DELETE the row, that just\n// causes a fresh shortlink to be minted on next read.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileSharesSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-shares-section p-3',\n ...options\n });\n this.template = `\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"text-muted small\">\n Active and historical shareable links for this file.\n </div>\n <button type=\"button\" class=\"btn btn-sm btn-primary\"\n data-action=\"share-file-from-section\">\n <i class=\"bi bi-link-45deg me-1\"></i>Share new\n </button>\n </div>\n <div data-container=\"file-shares-table\"></div>\n `;\n }\n\n async onInit() {\n const fileId = this.model.get('id');\n if (!fileId) return;\n\n const collection = new ShortLinkList({\n params: {\n source: 'fileman-share',\n file: fileId,\n sort: '-created',\n size: 25,\n },\n });\n this._sharesCollection = collection;\n\n this.sharesTable = new TableView({\n containerId: 'file-shares-table',\n collection,\n hideActivePillNames: ['source', 'file'],\n columns: [\n {\n key: 'code',\n label: 'Short URL',\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-share-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 { key: 'user.display_name', label: 'Shared By', formatter: \"default('—')\" },\n { key: 'hit_count', label: 'Hits', width: '80px', sortable: true },\n { key: 'track_clicks', label: 'Tracked', width: '90px', formatter: 'yesnoicon' },\n { key: 'is_active', label: 'Active', width: '80px', formatter: 'yesnoicon' },\n { key: 'expires_at', label: 'Expires', width: '160px', formatter: \"datetime|default('Never')\", sortable: true },\n { key: 'created', label: 'Created', width: '160px', formatter: 'datetime', sortable: true },\n { key: 'metadata.note', label: 'Note', formatter: \"truncate(40)|default('—')\", visibility: 'lg' },\n ],\n paginated: true,\n sortable: true,\n searchable: false,\n filterable: false,\n contextMenu: [\n { label: 'Copy Short URL', action: 'copy-share-code', icon: 'bi-clipboard' },\n { divider: true },\n { label: 'Revoke', action: 'revoke-share', icon: 'bi-slash-circle', danger: true },\n ],\n tableOptions: {\n hover: true,\n size: 'sm',\n emptyMessage: 'No shares yet — click “Share new” to mint a tracked link.',\n emptyIcon: 'bi-link-45deg',\n actions: [],\n },\n });\n this.addChild(this.sharesTable);\n }\n\n /** Re-fetch the shares list (called after a new share is minted). */\n refreshShares() {\n return this._sharesCollection?.fetch();\n }\n\n // Section bubbles section-scoped actions up to FileView. Mirrors the\n // FileRenditionsSection.onActionRegenerateFromSection pattern.\n async onActionShareFileFromSection() {\n let node = this.parent;\n while (node) {\n if (typeof node.onActionShareFile === 'function') {\n return node.onActionShareFile();\n }\n node = node.parent;\n }\n return null;\n }\n\n async onActionCopyShareCode(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const code = element?.dataset?.code;\n if (!code) return;\n const url = buildShortUrl(code, 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 async onActionRevokeShare(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n // The contextMenu wires `data-id` on the row; resolve from the table.\n const row = element?.closest?.('[data-row-id]');\n const id = row?.dataset?.rowId || element?.dataset?.id;\n if (!id) return;\n const target = this._sharesCollection?.get?.(id);\n if (!target) return;\n\n const confirmed = await Modal.confirm(\n 'Revoke this share? Anyone with the short URL will get a 404 — the audit row is preserved.',\n 'Revoke Share',\n { confirmText: 'Revoke', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n try {\n await target.save({ is_active: false });\n this.getApp()?.toast?.success?.('Share revoked');\n await this.refreshShares();\n } catch (err) {\n console.error('Failed to revoke share:', err);\n this.getApp()?.toast?.error?.('Failed to revoke share');\n }\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileOverviewSection — canonical DetailView landing surface\n//\n// Mirrors the round-1 KPIs + flat-row identity card + recent-activity shape\n// every other DetailView consumer leads with (UserView, GroupView,\n// IncidentView, ShortLinkView). Preview / Renditions / Details remain as\n// dedicated sections; the user lands here first.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileOverviewSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-overview-section',\n // The Public-URL row uses the `clipboard` formatter which emits a\n // [data-bs-toggle=\"tooltip\"] copy button.\n enableTooltips: true,\n template: `\n <div class=\"detail-section-eyebrow\">Snapshot</div>\n <div class=\"detail-kpi-grid\">\n <div data-container=\"file-kpi-size\"></div>\n <div data-container=\"file-kpi-type\"></div>\n <div data-container=\"file-kpi-status\"></div>\n <div data-container=\"file-kpi-uploaded\"></div>\n </div>\n\n <div class=\"detail-section-eyebrow\">Identity</div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Filename</div>\n <div class=\"detail-flat-row-value text-break\">{{model.filename|default:'Unnamed file'}}</div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Content type</div>\n <div class=\"detail-flat-row-value\"><code>{{model.content_type|default:'unknown'}}</code></div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Uploaded by</div>\n <div class=\"detail-flat-row-value\">{{model.user.display_name|default:'—'}}</div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Storage</div>\n <div class=\"detail-flat-row-value\">{{model.file_manager.name|default:'—'}}</div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Visibility</div>\n <div class=\"detail-flat-row-value\">\n {{#model.is_public|bool}}<span class=\"badge bg-success\"><i class=\"bi bi-unlock me-1\"></i>Public</span>{{/model.is_public|bool}}\n {{^model.is_public|bool}}<span class=\"badge bg-secondary\"><i class=\"bi bi-lock me-1\"></i>Private</span>{{/model.is_public|bool}}\n </div>\n </div>\n <div class=\"detail-flat-row\">\n <div class=\"detail-flat-row-label\">Public URL</div>\n <div class=\"detail-flat-row-value detail-flat-row-value--url\">\n {{#hasUrl|bool}}{{{model.url|clipboard}}}{{/hasUrl|bool}}\n {{^hasUrl|bool}}<span class=\"text-secondary\">—</span>{{/hasUrl|bool}}\n </div>\n </div>\n `,\n ...options\n });\n }\n\n get hasUrl() { return !!this.model?.get?.('url'); }\n\n async onInit() {\n const m = this.model;\n this.kpiSize = new MetricCard({\n containerId: 'file-kpi-size',\n label: 'File size',\n value: dataFormatter.apply('filesize', m.get('file_size')) || '—'\n });\n this.kpiType = new MetricCard({\n containerId: 'file-kpi-type',\n label: 'Type',\n value: _capitalize(m.get('category') || 'other')\n });\n this.kpiStatus = new MetricCard({\n containerId: 'file-kpi-status',\n label: 'Status',\n value: _capitalize(m.get('upload_status') || '—')\n });\n const created = m.get('created');\n this.kpiUploaded = new MetricCard({\n containerId: 'file-kpi-uploaded',\n label: 'Uploaded',\n value: created ? (dataFormatter.apply('relative', created) || '—') : '—'\n });\n [this.kpiSize, this.kpiType, this.kpiStatus, this.kpiUploaded]\n .forEach(c => this.addChild(c));\n }\n}\n\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileView (main component)\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileView extends DetailView {\n constructor(options = {}) {\n const model = options.model || new File(options.data || {});\n const categoryConfig = getCategoryConfig(model);\n\n // ── Section views (built before super() so SideNavView mounts them) ──\n\n const overviewSection = new FileOverviewSection({ model });\n\n const previewSection = new FilePreviewSection({ model, categoryConfig });\n\n const detailsSection = new DataView({\n model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'id', label: 'ID' },\n { name: 'filename', label: 'Filename' },\n { name: 'storage_filename', label: 'Storage Filename' },\n { name: 'content_type', label: 'Content Type' },\n { name: 'file_size', label: 'File Size', format: 'filesize' },\n { name: 'category', label: 'Category' },\n { name: 'upload_status', label: 'Status', format: 'badge' },\n { name: 'created', label: 'Created', format: 'datetime' },\n { name: 'modified', label: 'Modified', format: 'datetime' },\n { name: 'user.display_name', label: 'Uploaded By' },\n { name: 'file_manager.name', label: 'Storage Backend' },\n { name: 'storage_file_path', label: 'Storage Path' },\n { name: 'url', label: 'Public URL', format: 'url' },\n { name: 'is_public', label: 'Is Public', format: 'boolean' }\n ]\n });\n\n const renditionsSection = new FileRenditionsSection({ model });\n\n const sections = [\n { key: 'overview', label: 'Overview', icon: 'bi-grid-1x2', view: overviewSection },\n { key: 'preview', label: 'Preview', icon: categoryConfig.icon, view: previewSection },\n { key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsSection },\n { key: 'renditions', label: 'Renditions', icon: 'bi-layers', view: renditionsSection }\n ];\n\n // Shares — list of shortlinks (source=fileman-share) for this file.\n // Hidden for unsaved models — the section needs an `id` to scope.\n let sharesSection = null;\n if (model.get('id')) {\n sharesSection = new FileSharesSection({ model });\n sections.push({ key: 'shares', label: 'Shares', icon: 'bi-link-45deg', view: sharesSection });\n }\n\n // Metadata — only when backend returned a non-empty metadata object\n const metadata = model.get('metadata');\n if (metadata && typeof metadata === 'object' && Object.keys(metadata).length) {\n const metadataView = new DataView({\n data: metadata,\n className: 'p-3',\n columns: 2,\n showEmptyValues: false\n });\n sections.push({ key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: metadataView });\n }\n\n // ── Header config ───────────────────────────────────\n\n // Thumbnail — render real preview if the model exposes one; otherwise\n // fall back to the category Bootstrap icon (DetailView default).\n const thumbnailUrl = (typeof model.getThumbnailUrl === 'function') ? model.getThumbnailUrl() : null;\n const iconHtml = thumbnailUrl\n ? `<img src=\"${escapeAttr(thumbnailUrl)}\" alt=\"\">`\n : null;\n const iconTone = CATEGORY_TONE[categoryConfig.badgeClass] || null;\n\n const chips = [\n { icon: 'bi-hdd', text: m => dataFormatter.apply('filesize', m.get('file_size')), variant: 'light',\n when: m => !!m.get('file_size') },\n { text: m => m.get('content_type') || null, variant: 'light',\n when: m => !!m.get('content_type') },\n { text: m => _capitalize(m.get('category') || 'other'),\n variant: (categoryConfig.badgeClass || 'bg-secondary').replace(/^bg-/, '') },\n { text: m => _capitalize(m.get('upload_status') || ''), variant: 'secondary',\n when: m => !!m.get('upload_status') },\n { icon: 'bi-unlock', text: 'Public', variant: 'success',\n when: m => !!m.get('is_public') },\n { icon: 'bi-lock', text: 'Private', variant: 'secondary',\n when: m => !m.get('is_public') }\n ];\n\n const contextItems = [\n { label: 'View', action: 'view-file', icon: 'bi-eye' },\n { label: 'Download', action: 'download-file', icon: 'bi-download' },\n { label: 'Copy URL', action: 'copy-url', icon: 'bi-clipboard' },\n { label: 'Share Link…', action: 'share-file', icon: 'bi-link-45deg' },\n { type: 'divider' },\n { label: 'Edit Details', action: 'edit-file', icon: 'bi-pencil' },\n model.get('is_public')\n ? { label: 'Make Private', action: 'make-private', icon: 'bi-lock' }\n : { label: 'Make Public', action: 'make-public', icon: 'bi-unlock' },\n { label: 'Regenerate Previews', action: 'regenerate-renditions', icon: 'bi-arrow-repeat' },\n { type: 'divider' },\n { label: 'Delete File', action: 'delete-file', icon: 'bi-trash', danger: true }\n ];\n\n super({\n className: 'file-view',\n ...options,\n model,\n header: {\n icon: categoryConfig.icon,\n iconTone,\n iconHtml,\n titleFn: m => m.get('filename') || 'Unnamed file',\n subtitleFn: m => {\n const created = m.get('created');\n if (!created) return '';\n const fmt = dataFormatter.apply('datetime', dataFormatter.apply('epoch', created));\n return fmt ? `Uploaded ${fmt}` : '';\n },\n chips,\n contextMenu: { items: contextItems }\n },\n sections,\n activeSection: 'overview',\n navWidth: 200,\n minWidth: 500,\n contentPadding: '1.25rem 1.5rem'\n });\n\n // Stash references for action handlers + cross-section wiring\n this.overviewSection = overviewSection;\n this.previewSection = previewSection;\n this.detailsSection = detailsSection;\n this.renditionsSection = renditionsSection;\n this.sharesSection = sharesSection;\n\n // No auto-poll: a `completed` upload means renditions are done. If the\n // user explicitly clicks \"Regenerate\", _maybeStartRenditionsPoll is\n // invoked with { force: true } from that handler so the new renditions\n // appear without a manual refresh.\n }\n\n _getCategoryConfig() {\n return getCategoryConfig(this.model);\n }\n\n async onBeforeDestroy() {\n this._stopRenditionsPoll();\n }\n\n // ── Action handlers ─────────────────────────────\n\n async onActionViewFile() {\n openFileInPreview(this.model, this._getCategoryConfig());\n }\n\n async onActionDownloadFile() {\n downloadFile(this.model);\n }\n\n async onActionCopyUrl() {\n const url = this.model.get('url');\n if (!url) return;\n try {\n if (navigator.clipboard && window.isSecureContext) {\n await navigator.clipboard.writeText(url);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = url;\n document.body.appendChild(textarea);\n textarea.select();\n document.execCommand('copy');\n document.body.removeChild(textarea);\n }\n this.getApp()?.toast?.success?.('URL copied to clipboard');\n } catch (error) {\n console.error('Failed to copy URL:', error);\n this.getApp()?.toast?.error?.('Failed to copy URL');\n }\n }\n\n async onActionEditFile() {\n const resp = await Modal.modelForm({\n title: `Edit File - ${this.model.get('filename')}`,\n model: this.model,\n formConfig: FileForms.edit\n });\n if (resp) {\n this.render();\n }\n }\n\n async onActionMakePublic() {\n await this.model.save({ is_public: true });\n this.render();\n }\n\n async onActionMakePrivate() {\n await this.model.save({ is_public: false });\n this.render();\n }\n\n async onActionShareFile() {\n if (!this.model.get('id')) {\n this.getApp()?.toast?.warning?.('Save the file before sharing it.');\n return;\n }\n\n // Step 1 — collect share options.\n const formResult = await Modal.form({\n title: 'Share Link',\n size: 'sm',\n help: 'Each share creates a distinct, audit-tracked short URL attributed to you.',\n fields: [\n { name: 'expire_days', type: 'number', label: 'Expire after (days)', value: 30, min: 0, cols: 12, help: '0 = never expires. Server max: 3650.' },\n { name: 'track_clicks', type: 'switch', label: 'Track clicks', value: true, cols: 12, help: 'Records per-click history (IP, user-agent, bot/human).' },\n { name: 'note', type: 'textarea', label: 'Note (optional)', rows: 2, cols: 12, maxlength: 512, help: 'Private audit note — not shown to recipients.' },\n ],\n submitText: 'Share',\n });\n if (!formResult) return;\n\n // Build options dict — only include keys with meaningful values.\n const opts = {};\n if (formResult.expire_days !== undefined && formResult.expire_days !== null && formResult.expire_days !== '') {\n opts.expire_days = Number(formResult.expire_days);\n }\n if (formResult.track_clicks !== undefined) {\n opts.track_clicks = !!formResult.track_clicks;\n }\n if (formResult.note) {\n opts.note = String(formResult.note).slice(0, 512);\n }\n\n // Step 2 — mint the share.\n let resp;\n try {\n resp = await this.model.share(Object.keys(opts).length ? opts : true);\n } catch (err) {\n console.error('Share failed:', err);\n Modal.showError(err?.data?.error || err?.message || 'Failed to create share link');\n return;\n }\n\n const data = resp?.data;\n const shortUrl = data?.url;\n if (!resp?.success || !shortUrl) {\n Modal.showError(data?.error || 'Failed to create share link');\n return;\n }\n\n // Best-effort copy to clipboard so the user can paste immediately.\n let copied = false;\n try {\n await navigator.clipboard?.writeText?.(shortUrl);\n copied = true;\n } catch (_e) {\n copied = false;\n }\n\n // Step 3 — show the result with the URL + summary.\n const expiry = data.expires_at\n ? new Date(data.expires_at).toLocaleString()\n : 'Never';\n const tracked = data.track_clicks ? 'Yes' : 'No';\n const copyHint = copied\n ? '<div class=\"form-text text-success mb-2\"><i class=\"bi bi-check-circle me-1\"></i>Copied to clipboard.</div>'\n : '<div class=\"form-text text-muted mb-2\">Select the URL above to copy.</div>';\n const summary = `\n <div class=\"mb-2\">\n <label class=\"form-label small text-muted mb-1\">Share URL</label>\n <input type=\"text\" class=\"form-control font-monospace\" readonly value=\"${escapeAttr(shortUrl)}\">\n ${copyHint}\n </div>\n <dl class=\"row small mb-0\">\n <dt class=\"col-4 text-muted\">Expires</dt><dd class=\"col-8\">${escapeHtml(expiry)}</dd>\n <dt class=\"col-4 text-muted\">Tracked</dt><dd class=\"col-8\">${escapeHtml(tracked)}</dd>\n ${data.shortlink_code ? `<dt class=\"col-4 text-muted\">Code</dt><dd class=\"col-8\"><code>${escapeHtml(data.shortlink_code)}</code></dd>` : ''}\n </dl>\n `;\n await Modal.alert(summary, 'Share link created', { type: 'success' });\n\n // Step 4 — refresh the Shares section if it's mounted.\n try {\n await this.sharesSection?.refreshShares?.();\n } catch (err) {\n console.warn('Failed to refresh shares section:', err);\n }\n }\n\n async onActionRegenerateRenditions() {\n const confirmed = await Modal.confirm(\n 'Rebuild all previews and thumbnails for this file? Existing renditions will be replaced. Generation runs in the background and may take several minutes for video.',\n 'Regenerate Previews',\n { confirmText: 'Regenerate' }\n );\n if (!confirmed) return;\n\n try {\n await this.model.regenerateRenditions();\n this.getApp()?.toast?.success?.('Regenerating previews in the background…');\n } catch (err) {\n console.error('Failed to trigger regenerate_renditions:', err);\n this.getApp()?.toast?.error?.('Failed to start preview regeneration');\n return;\n }\n // Start polling so the new renditions appear automatically as the\n // worker finishes.\n this._maybeStartRenditionsPoll({ force: true });\n }\n\n // ── Renditions polling ──────────────────────────\n // Mirrors the IncidentView analysis-progress polling shape: recursive\n // setTimeout, attempt counter, no overlap with a previous fetch.\n\n _maybeStartRenditionsPoll(options = {}) {\n if (this._renditionsPollTimer) return; // already polling\n // Only the explicit `force: true` path remains — this is invoked from\n // onActionRegenerateRenditions to catch newly built renditions as the\n // worker finishes.\n if (!options.force) return;\n const maxAttempts = 60; // 5 minutes at 5s\n const intervalMs = 5000;\n let attempts = 0;\n\n const tick = () => {\n this._renditionsPollTimer = null;\n if (!this.model) return;\n if (this.model.hasRenditions && this.model.hasRenditions()) return;\n if (++attempts > maxAttempts) return;\n\n this._renditionsPollTimer = setTimeout(async () => {\n try {\n await this.model.fetch();\n } catch (err) {\n console.warn('FileView: renditions poll fetch failed:', err);\n }\n tick();\n }, intervalMs);\n };\n\n tick();\n }\n\n _stopRenditionsPoll() {\n if (this._renditionsPollTimer) {\n clearTimeout(this._renditionsPollTimer);\n this._renditionsPollTimer = null;\n }\n }\n\n async onActionDeleteFile() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete the file \"${this.model.get('filename')}\"? This action cannot be undone.`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n if (!confirmed) return;\n\n const resp = await this.model.destroy();\n if (resp && resp.success) {\n this.emit('file:deleted', { model: this.model });\n }\n }\n\n // Prevent model changes from triggering a full re-render.\n // Section views manage their own reactivity. Same pattern as UserView.\n _onModelChange() {\n // no-op\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 static create(options = {}) {\n return new FileView(options);\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Small HTML helpers — kept private to this file\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction escapeHtml(str) {\n if (str == null) return '';\n return String(str)\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Build a full short URL from a shortlink code. Prefers\n * `app.config.shortlink_base_url`, falls back to `window.location.origin`.\n * Mirrors the helper in src/extensions/admin/shortlinks/ShortLinkView.js\n * but lives here so this core view doesn't depend on the admin extension.\n */\nfunction buildShortUrl(code, app) {\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 escapeAttr(str) {\n return escapeHtml(str);\n}\n\nfunction formatBytes(bytes) {\n if (bytes == null || isNaN(bytes)) return '';\n const n = Number(bytes);\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;\n return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;\n}\n\nFile.VIEW_CLASS = FileView;\nFile.MODEL_REF = 'fileman.File';\n\nexport default FileView;\nexport {\n FileView,\n FileOverviewSection,\n FilePreviewSection,\n FileRenditionsSection,\n FileSharesSection\n};\n","import View from '@core/View.js';\nimport FilePreviewView from '@core/views/data/FilePreviewView.js';\n\n/**\n * ChatMessageView - Individual message display with theme support\n * \n * Supports two themes:\n * - 'compact': List-based admin/activity feed style\n * - 'bubbles': Chat bubble style with left/right positioning\n */\nclass ChatMessageView extends View {\n constructor(options = {}) {\n // Compute full className before super() so _syncAttrs picks it up\n const message = options.message || {};\n const theme = options.theme || 'compact';\n const isCurrentUser = options.isCurrentUser || false;\n const role = message.role || (isCurrentUser ? 'user' : null);\n\n let cls = 'chat-message';\n if (theme === 'bubbles') {\n cls += isCurrentUser ? ' message-right' : ' message-left';\n }\n if (role === 'assistant') cls += ' message-assistant';\n else if (role === 'user') cls += ' message-user';\n\n super({\n className: cls,\n ...options\n });\n\n this.message = message;\n this.theme = theme;\n this.isCurrentUser = isCurrentUser;\n this.role = role;\n }\n\n getTemplate() {\n // System event messages (same for both themes). Content may contain\n // pre-rendered HTML (e.g. status-change badges), so use triple-brace.\n if (this.message.type === 'system_event') {\n return `\n <div class=\"chat-message-system text-center text-muted small py-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n {{{message.content}}}\n </div>\n `;\n }\n\n // Theme-specific templates\n if (this.theme === 'bubbles') {\n return this.getBubblesTemplate();\n } else {\n return this.getCompactTemplate();\n }\n }\n\n /**\n * Get compact theme template (Option 4 - Admin/Activity Feed Style)\n */\n getCompactTemplate() {\n const userClass = this.isCurrentUser ? 'bg-primary' : 'bg-secondary';\n const isAssistant = this.role === 'assistant';\n\n return `\n <div class=\"message-item\">\n <div class=\"message-avatar ${isAssistant ? 'bg-dark' : userClass}\">\n ${isAssistant ? '<img src=\"https://mojo-verify.s3.amazonaws.com/signatures/14e7aab75c2749cb846f7d57298691ac/mojo_ai_7c0322e9.png\" class=\"mojo-avatar-icon\" alt=\"\">' : `\n {{#message.author.avatarUrl}}\n <img src=\"{{message.author.avatarUrl}}\" alt=\"{{message.author.name}}\" class=\"w-100 h-100 rounded-circle\">\n {{/message.author.avatarUrl}}\n {{^message.author.avatarUrl}}\n {{message.author.name|initials}}\n {{/message.author.avatarUrl}}\n `}\n </div>\n <div class=\"message-content\">\n <div class=\"message-header\">\n <div class=\"message-author\">\n ${isAssistant ? 'Mojo' : '{{message.author.name}}'}\n {{#isCurrentUser}}\n <span class=\"badge bg-primary badge-sm ms-1\">You</span>\n {{/isCurrentUser}}\n </div>\n <div class=\"message-time text-muted\">{{message.timestamp|relative}}</div>\n </div>\n <div class=\"message-text\">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container=\"blocks-${this.message.id || this.id}\"></div>\n <div data-container=\"attachments\"></div>\n </div>\n </div>\n `;\n }\n\n /**\n * Get bubbles theme template (Option 1 - Modern Chat Bubbles)\n */\n getBubblesTemplate() {\n const isAssistant = this.role === 'assistant';\n\n return `\n <div class=\"message-bubble-wrapper\">\n <div class=\"message-meta\">\n <strong>${isAssistant ? '<img src=\"https://mojo-verify.s3.amazonaws.com/signatures/14e7aab75c2749cb846f7d57298691ac/mojo_ai_7c0322e9.png\" class=\"mojo-avatar-icon me-1\" alt=\"\">Mojo' : '{{message.author.name}}'}</strong>\n <span class=\"text-muted\">· {{message.timestamp|relative}}</span>\n </div>\n <div class=\"message-bubble\">\n <div class=\"message-text\">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container=\"blocks-${this.message.id || this.id}\"></div>\n <div data-container=\"attachments\"></div>\n </div>\n </div>\n `;\n }\n\n /**\n * Get tool calls display template (collapsible section showing tool usage)\n * @private\n */\n _getToolCallsTemplate() {\n if (!this.message.tool_calls || this.message.tool_calls.length === 0) {\n return '';\n }\n const esc = (str) => {\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n };\n const toolBadges = this.message.tool_calls.map(tc => {\n const name = esc(tc.name || tc.function?.name || 'tool');\n const statusClass = tc.status === 'error' ? 'bg-danger' : 'bg-info';\n return `<span class=\"badge ${statusClass} me-1\">${name}</span>`;\n }).join('');\n\n const collapseId = `tools-${this.message.id || this.id}`;\n return `\n <div class=\"message-tool-calls mt-1\">\n <a class=\"text-muted small\" data-bs-toggle=\"collapse\" href=\"#${collapseId}\" role=\"button\" aria-expanded=\"false\">\n <i class=\"bi bi-tools me-1\"></i>${this.message.tool_calls.length} tool call${this.message.tool_calls.length > 1 ? 's' : ''}\n </a>\n <div class=\"collapse\" id=\"${collapseId}\">\n <div class=\"mt-1\">${toolBadges}</div>\n </div>\n </div>\n `;\n }\n\n async onAfterRender() {\n // Render attachments if any. Clean up any prior FilePreview children first\n // so a re-render doesn't duplicate them.\n if (this.message.attachments && this.message.attachments.length > 0) {\n const attachmentsContainer = this.element.querySelector('[data-container=\"attachments\"]');\n if (attachmentsContainer) {\n attachmentsContainer.innerHTML = '';\n for (const id in this.children) {\n const child = this.children[id];\n if (child instanceof FilePreviewView) this.removeChild(child);\n }\n this.message.attachments.forEach(file => {\n const filePreview = new FilePreviewView({ file });\n this.addChild(filePreview);\n filePreview.render(true, attachmentsContainer);\n });\n }\n }\n }\n}\n\nexport default ChatMessageView;\n","import View from '@core/View.js';\nimport applyFileDropMixin from '@core/mixins/FileDropMixin.js';\nimport { File } from '@core/models/Files.js';\n\n/**\n * ChatInputView - Input area with file drop support and attachment preview\n */\nclass ChatInputView extends View {\n constructor(options = {}) {\n super({\n className: 'chat-input-view',\n ...options\n });\n\n this.placeholder = options.placeholder || 'Type a message...';\n this.buttonText = options.buttonText || 'Send';\n this.showFileInput = options.showFileInput !== false; // default true\n this.attachments = []; // Array of uploaded file data\n this.pendingUploads = new Map(); // Track in-progress uploads\n }\n\n getTemplate() {\n return `\n <div class=\"chat-input-container\">\n <div class=\"chat-input-attachments\" data-container=\"attachments\"></div>\n <div class=\"chat-input-wrapper\">\n <textarea\n class=\"chat-input form-control\"\n placeholder=\"${this.placeholder}\"\n rows=\"2\"></textarea>\n <button class=\"chat-send-btn btn btn-primary\" data-action=\"send-message\" type=\"button\">\n <i class=\"bi bi-send-fill\"></i>\n <span class=\"spinner-border spinner-border-sm d-none\" role=\"status\" aria-hidden=\"true\"></span>\n </button>\n </div>\n ${this.showFileInput ? `\n <div class=\"chat-input-footer\">\n <small class=\"text-muted\">\n <i class=\"bi bi-paperclip\"></i>\n Drag & drop files to attach\n </small>\n </div>\n ` : ''}\n </div>\n `;\n }\n\n async onAfterRender() {\n // Enable file drop on the entire input container (if file input is shown)\n if (this.showFileInput) {\n this.enableFileDrop({\n dropZoneSelector: '.chat-input-container',\n multiple: true,\n acceptedTypes: ['*/*'], // Accept all file types\n visualFeedback: true,\n dragOverClass: 'drag-over',\n dragActiveClass: 'drag-active'\n });\n }\n\n // Auto-resize textarea as user types and handle Enter key\n const textarea = this.element.querySelector('.chat-input');\n if (textarea) {\n textarea.addEventListener('input', () => this.autoResizeTextarea(textarea));\n textarea.addEventListener('keydown', (e) => this.handleKeydown(e));\n }\n }\n\n /**\n * Handle textarea keydown (send on Enter without Shift)\n */\n handleKeydown(event) {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n this.onActionSendMessage(event, event.target);\n }\n }\n\n /**\n * Handle file drop\n * @param {File[]} files - Dropped files\n */\n async onFileDrop(files) {\n for (const file of files) {\n await this.uploadFile(file);\n }\n }\n\n /**\n * Upload a file\n * @param {File} file - File to upload\n */\n async uploadFile(file) {\n const fileModel = new File();\n const uploadId = Date.now() + Math.random();\n\n // Add preview immediately\n this.addFilePreview(uploadId, file, 0);\n this.pendingUploads.set(uploadId, { file, fileModel });\n\n try {\n const result = await fileModel.upload({\n file: file,\n onProgress: (progress) => {\n this.updateFileProgress(uploadId, progress);\n },\n onComplete: (uploadResult) => {\n this.handleUploadComplete(uploadId, fileModel);\n }\n });\n\n } catch (error) {\n console.error('File upload failed:', error);\n this.handleUploadError(uploadId, error);\n }\n }\n\n /**\n * Add file preview to UI\n * @param {string} uploadId - Unique upload ID\n * @param {File} file - File object\n * @param {number} progress - Upload progress (0-100)\n */\n addFilePreview(uploadId, file, progress) {\n const container = this.element.querySelector('[data-container=\"attachments\"]');\n if (!container) return;\n\n const preview = document.createElement('div');\n preview.className = 'attachment-preview';\n preview.dataset.uploadId = uploadId;\n preview.innerHTML = `\n <div class=\"attachment-info\">\n <i class=\"bi bi-file-earmark\"></i>\n <span class=\"attachment-name\">${this.escapeHtml(file.name)}</span>\n <span class=\"attachment-size\">(${this.formatFileSize(file.size)})</span>\n </div>\n <div class=\"attachment-progress\">\n <div class=\"progress\" style=\"height: 4px;\">\n <div class=\"progress-bar\" role=\"progressbar\" style=\"width: ${progress}%\"></div>\n </div>\n </div>\n <button class=\"attachment-remove btn btn-sm btn-link text-danger\" data-action=\"remove-attachment\" data-upload-id=\"${uploadId}\" type=\"button\">\n <i class=\"bi bi-x\"></i>\n </button>\n `;\n\n container.appendChild(preview);\n }\n\n /**\n * Update file upload progress\n * @param {string} uploadId - Upload ID\n * @param {number} progress - Progress (0-100)\n */\n updateFileProgress(uploadId, progress) {\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n const progressBar = preview.querySelector('.progress-bar');\n if (progressBar) {\n progressBar.style.width = `${progress}%`;\n }\n }\n }\n\n /**\n * Handle upload completion\n * @param {string} uploadId - Upload ID\n * @param {Object} result - Upload result data (contains file.id)\n */\n handleUploadComplete(uploadId, fileModel) {\n // Store the file data with its ID\n this.attachments.push({\n id: fileModel.id,\n name: fileModel.get(\"name\"),\n uploadId: uploadId\n });\n this.pendingUploads.delete(uploadId);\n\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n preview.classList.add('upload-complete');\n const progressContainer = preview.querySelector('.attachment-progress');\n if (progressContainer) {\n progressContainer.remove();\n }\n }\n }\n\n /**\n * Handle upload error\n * @param {string} uploadId - Upload ID\n * @param {Error} error - Error object\n */\n handleUploadError(uploadId, error) {\n this.pendingUploads.delete(uploadId);\n\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n preview.classList.add('upload-error');\n preview.querySelector('.attachment-info').innerHTML +=\n `<span class=\"text-danger ms-2\">Upload failed</span>`;\n }\n }\n\n /**\n * Remove attachment\n */\n async onActionRemoveAttachment(event, element) {\n const uploadId = element.dataset.uploadId;\n\n // Remove from pending uploads\n this.pendingUploads.delete(uploadId);\n\n // Remove from completed attachments\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n // TODO: Get the file ID from the preview and remove from attachments array\n preview.remove();\n }\n }\n\n\n\n /**\n * Send message\n */\n async onActionSendMessage(event, element) {\n const textarea = this.element.querySelector('.chat-input');\n const text = textarea.value.trim();\n\n // Don't send if empty and no attachments\n if (!text && this.attachments.length === 0) {\n return;\n }\n\n // Don't send if uploads are pending\n if (this.pendingUploads.size > 0) {\n // TODO: Show message that uploads are in progress\n return;\n }\n\n // Show busy state\n this.setBusy(true);\n\n // Emit event with message data\n this.emit('message:send', {\n text: text,\n files: this.attachments\n });\n\n // Note: Don't clear here - let the parent ChatView call clearInput() after successful send\n }\n\n /**\n * Enable or disable the entire input area\n * @param {boolean} enabled - Whether the input should be enabled\n */\n setEnabled(enabled) {\n const textarea = this.element?.querySelector('.chat-input');\n const button = this.element?.querySelector('.chat-send-btn');\n if (textarea) textarea.disabled = !enabled;\n if (button) button.disabled = !enabled;\n }\n\n /**\n * Set busy state (show/hide spinner)\n * @param {boolean} busy - Whether to show busy state\n */\n setBusy(busy) {\n const button = this.element.querySelector('.chat-send-btn');\n const icon = button.querySelector('.bi-send-fill');\n const spinner = button.querySelector('.spinner-border');\n\n if (busy) {\n button.disabled = true;\n icon.classList.add('d-none');\n spinner.classList.remove('d-none');\n } else {\n button.disabled = false;\n icon.classList.remove('d-none');\n spinner.classList.add('d-none');\n }\n }\n\n /**\n * Clear input and attachments\n */\n clearInput() {\n const textarea = this.element.querySelector('.chat-input');\n if (textarea) {\n textarea.value = '';\n textarea.style.height = 'auto';\n }\n\n const container = this.element.querySelector('[data-container=\"attachments\"]');\n if (container) {\n container.innerHTML = '';\n }\n\n this.attachments = [];\n this.pendingUploads.clear();\n \n // Reset busy state\n this.setBusy(false);\n }\n\n /**\n * Auto-resize textarea based on content\n * @param {HTMLTextAreaElement} textarea\n */\n autoResizeTextarea(textarea) {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';\n }\n\n /**\n * Format file size for display\n * @param {number} bytes\n * @returns {string}\n */\n formatFileSize(bytes) {\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];\n }\n\n /**\n * Escape HTML to prevent XSS\n * @param {string} text\n * @returns {string}\n */\n escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n}\n\n// Apply FileDropMixin\napplyFileDropMixin(ChatInputView);\n\nexport default ChatInputView;\n","import View from '@core/View.js';\nimport ChatMessageView from './ChatMessageView.js';\nimport ChatInputView from './ChatInputView.js';\n\n/**\n * ChatView - Modern chat interface with theme support\n * \n * Themes:\n * - 'compact' (default): Admin/activity feed style, list-based layout\n * - 'bubbles': Modern chat bubbles with left/right positioning\n * \n * Usage:\n * const chat = new ChatView({\n * adapter: myAdapter,\n * theme: 'compact', // or 'bubbles'\n * currentUserId: 123,\n * inputPlaceholder: 'Add a comment...',\n * inputButtonText: 'Send'\n * });\n */\nclass ChatView extends View {\n constructor(options = {}) {\n super({\n className: 'chat-view',\n ...options\n });\n\n this.adapter = options.adapter;\n this.theme = options.theme || 'compact'; // 'compact' or 'bubbles'\n this.currentUserId = options.currentUserId;\n this.inputPlaceholder = options.inputPlaceholder || 'Type a message...';\n this.inputButtonText = options.inputButtonText || 'Send';\n this.showFileInput = options.showFileInput !== false; // default true\n this.showInput = options.showInput !== false; // default true\n this.MessageViewClass = options.messageViewClass || ChatMessageView;\n this.messages = [];\n this.messageViews = new Map(); // Track message views by ID\n this._thinkingEl = null;\n }\n\n getTemplate() {\n return `\n <div class=\"chat-container chat-theme-${this.theme}\">\n <div class=\"chat-messages\" data-container=\"messages\"></div>\n ${this.showInput ? '<div class=\"chat-input-wrapper\" data-container=\"input\"></div>' : ''}\n </div>\n `;\n }\n\n async onInit() {\n // Initial fetch of messages\n this.messages = await this.adapter.fetch();\n\n // Create input view (if not hidden)\n if (this.showInput) {\n this.inputView = new ChatInputView({\n containerId: 'input',\n placeholder: this.inputPlaceholder,\n buttonText: this.inputButtonText,\n showFileInput: this.showFileInput\n });\n this.addChild(this.inputView);\n\n // Listen for new messages\n this.inputView.on('message:send', async (data) => {\n await this.handleSendMessage(data);\n });\n }\n }\n\n async onAfterRender() {\n // Build message views\n this._buildMessageViews();\n \n // Render children (like ListView does)\n await this._renderChildren();\n \n // Scroll to bottom\n this.scrollToBottom();\n }\n\n /**\n * Render child message views (similar to ListView._renderChildren)\n * @private\n */\n async _renderChildren() {\n // Collect message view IDs so we can skip them in the standard path\n const messageViewIds = new Set();\n this.messageViews.forEach(v => messageViewIds.add(v.id));\n\n // Render non-message children (e.g. input view) via the standard path\n for (const id in this.children) {\n const child = this.children[id];\n if (!child || messageViewIds.has(id)) continue;\n child.parent = this;\n await Promise.resolve(child.render()).catch(err =>\n console.warn(`ChatView child render error (${id})`, err)\n );\n }\n\n // Then place message views into the messages container\n const messagesContainer = this.element.querySelector('[data-container=\"messages\"]');\n if (!messagesContainer) return;\n\n this.messageViews.forEach((messageView) => {\n messagesContainer.appendChild(messageView.element);\n messageView.render(false);\n });\n }\n\n /**\n * Build message views for all messages (similar to ListView._buildItems)\n * @private\n */\n _buildMessageViews() {\n if (!this.messages || this.messages.length === 0) return;\n \n this.messages.forEach(message => {\n if (!this.messageViews.has(message.id)) {\n this._createMessageView(message);\n }\n });\n }\n\n /**\n * Create a message view (similar to ListView._createItemView)\n * @private\n */\n _createMessageView(message) {\n if (this.messageViews.has(message.id)) return;\n \n const isCurrentUser = message.author && message.author.id === this.currentUserId;\n \n const messageView = new this.MessageViewClass({\n message: message,\n theme: this.theme,\n isCurrentUser: isCurrentUser\n });\n \n this.addChild(messageView);\n this.messageViews.set(message.id, messageView);\n \n return messageView;\n }\n\n /**\n * Add a new message to the chat (for real-time updates)\n * @param {Object} message - Message data\n * @param {boolean} scroll - Whether to scroll to bottom after adding\n */\n addMessage(message, scroll = true) {\n if (this.messageViews.has(message.id)) return;\n \n const messageView = this._createMessageView(message);\n \n // If already rendered, append to DOM immediately\n if (this.isMounted()) {\n const messagesContainer = this.element.querySelector('[data-container=\"messages\"]');\n if (messagesContainer) {\n messagesContainer.appendChild(messageView.element);\n messageView.render(false);\n }\n }\n \n if (scroll) {\n this.scrollToBottom();\n }\n }\n\n /**\n * Handle sending a new message\n * @param {Object} data - Message data {text, files}\n * @private\n */\n async handleSendMessage(data) {\n try {\n // If there's text, send it as a note\n if (data.text && data.text.trim()) {\n const result = await this.adapter.addNote({\n text: data.text,\n files: data.files && data.files.length > 0 ? [data.files[0]] : []\n });\n \n if (!result.success) {\n throw new Error('Failed to send message');\n }\n }\n \n // If there are multiple files, or files without text, create a note for each\n const startIndex = (data.text && data.text.trim() && data.files?.length > 0) ? 1 : 0;\n \n for (let i = startIndex; i < (data.files?.length || 0); i++) {\n const file = data.files[i];\n const result = await this.adapter.addNote({\n text: '', // Empty text, just the file\n files: [file]\n });\n \n if (!result.success) {\n console.error('Failed to upload file:', file);\n }\n }\n \n // Fetch updated messages\n this.messages = await this.adapter.fetch();\n \n // Find the new message(s) and add them\n this.messages.forEach(message => {\n if (!this.messageViews.has(message.id)) {\n this.addMessage(message, true);\n }\n });\n \n // Clear input (this also resets busy state)\n this.inputView.clearInput();\n \n } catch (error) {\n console.error('Failed to send message:', error);\n // Reset busy state on error\n this.inputView.setBusy(false);\n try {\n this.getApp().toast.error('Failed to send message');\n } catch (e) {\n // Toast not available — fail silently\n }\n }\n }\n\n /**\n * Show an animated thinking indicator at the bottom of the messages area.\n * Only one indicator is shown at a time — subsequent calls update the text.\n * @param {string} [text='Thinking...'] - Status text to display\n */\n showThinking(text = 'Thinking...') {\n const container = this.element?.querySelector('[data-container=\"messages\"]');\n if (!container) return;\n\n if (!this._thinkingEl) {\n this._thinkingEl = document.createElement('div');\n this._thinkingEl.className = 'chat-thinking';\n this._thinkingEl.innerHTML = `\n <div class=\"chat-thinking-content\">\n <span class=\"chat-thinking-dots\">\n <span></span><span></span><span></span>\n </span>\n <span class=\"chat-thinking-text\"></span>\n <span class=\"chat-thinking-timer text-muted\"></span>\n </div>\n `;\n container.appendChild(this._thinkingEl);\n\n this._thinkingStart = Date.now();\n this._thinkingInterval = setInterval(() => {\n const elapsed = Math.floor((Date.now() - this._thinkingStart) / 1000);\n const mins = Math.floor(elapsed / 60);\n const secs = elapsed % 60;\n const timerEl = this._thinkingEl?.querySelector('.chat-thinking-timer');\n if (timerEl) {\n timerEl.textContent = mins > 0\n ? `${mins}m ${String(secs).padStart(2, '0')}s`\n : `${secs}s`;\n }\n }, 1000);\n }\n\n this._thinkingEl.querySelector('.chat-thinking-text').textContent = text;\n this.scrollToBottom();\n }\n\n /**\n * Remove the thinking indicator\n */\n hideThinking() {\n if (this._thinkingInterval) {\n clearInterval(this._thinkingInterval);\n this._thinkingInterval = null;\n }\n if (this._thinkingEl) {\n this._thinkingEl.remove();\n this._thinkingEl = null;\n }\n }\n\n /**\n * Enable or disable the chat input\n * @param {boolean} enabled - Whether the input should be enabled\n */\n setInputEnabled(enabled) {\n if (this.inputView?.setEnabled) {\n this.inputView.setEnabled(enabled);\n }\n }\n\n /**\n * Scroll chat to bottom\n */\n scrollToBottom() {\n const container = this.element.querySelector('.chat-messages');\n if (container) {\n requestAnimationFrame(() => {\n container.scrollTop = container.scrollHeight;\n });\n }\n }\n\n /**\n * Clear all messages\n */\n clearMessages() {\n this.messageViews.forEach((view) => {\n // Remove from parent's children hash so _renderChildren won't re-render them\n delete this.children[view.id];\n view.destroy();\n });\n this.messageViews.clear();\n this.messages = [];\n\n const container = this.element?.querySelector('[data-container=\"messages\"]');\n if (container) {\n container.innerHTML = '';\n }\n }\n\n /**\n * Refresh messages from adapter\n */\n async refresh() {\n this.clearMessages();\n this.messages = await this.adapter.fetch();\n this._buildMessageViews();\n \n if (this.isMounted()) {\n await this._renderChildren();\n this.scrollToBottom();\n }\n }\n}\n\nexport default ChatView;\n"],"names":["MetricsPermission","Model","constructor","data","super","endpoint","id_key","MetricsPermissionList","Collection","options","ModelClass","MetricsForms","edit","title","fields","name","type","label","columns","help","GeoIPForms","size","required","readonly","cols","step","GeoLocatedIP","lookup","ip","model","resp","rest","GET","success","EDIT_FORM","EDIT_LOCATION_FORM","EDIT_SECURITY_FORM","value","EDIT_NETWORK_FORM","GeoLocatedIPList","TablePage","Page","pageName","this","description","collection","defaultQuery","groupField","tableViewConfig","actions","contextMenu","batchActions","batchBarLocation","clickAction","addForm","formFields","formCreate","editForm","formEdit","itemView","itemViewClass","deleteTemplate","formDialogConfig","viewDialogOptions","searchable","sortable","filterable","paginated","selectionMode","selectable","filters","additionalFilters","hideActivePills","hideActivePillNames","searchPlacement","dayRangeFilter","groupBy","groupHeaderTemplate","groupHeaderLabel","groupHeaderStyle","rowStripe","tableOptions","striped","bordered","hover","responsive","emptyMessage","searchPlaceholder","showAdd","showExport","onItemView","onItemEdit","onItemDelete","onAdd","onExport","tableViewOptions","urlSyncEnabled","lastUpdated","isLoading","template","buildTemplate","onInit","applyQueryToCollection","tableView","TableView","containerId","fetchOnMount","async","event","showItemDialog","addChild","setupEventListeners","on","Date","toLocaleTimeString","updateStatusDisplay","syncUrl","params","query","Object","keys","length","start","parseInt","sort","search","reservedParams","entries","forEach","key","includes","startsWith","JSON","parse","e","field","parseFilterKey","hasOwnProperty","setParams","force","getApp","router","currentUrl","URL","window","location","currentParams","searchParams","desiredParams","collectionParams","stringify","hasChanges","some","String","_item","updateBrowserUrl","element","updatedElement","querySelector","textContent","countElement","count","meta","onEnter","requiresGroup","activeGroup","id","setTimeout","updateFilterPills","updateSortIcons","_openDeepLinkedItem","itemId","fetchOne","_clearItemParam","_setItemParam","fetchOnView","Modal","loading","fetch","error","hideLoading","showError","message","ViewClass","getItemViewClass","viewInstance","dialog","header","body","centered","getFormDialogConfig","getModelClass","DIALOG_OPTIONS","getModelName","refresh","getSelectedItems","clearSelection","batchAction","destroy","handler","confirm","items","results","Promise","allSettled","map","save","successes","filter","r","status","failures","app","toast","warning","handleFilterEdit","filterKey","filterConfig","getAllAvailableFilters","find","f","currentValue","config","result","form","filter_value","setFilter","restEnabled","render","clearAllFilters","onGroupChange","group","onBeforeDestroy","off","showStatus","create","resolveAccessor","fieldOrAccessor","get","TypeError","toDate","raw","ms","dataFormatter","normalizeEpoch","Number","isNaN","d","getTime","_err","isoDayKey","date","getFullYear","getMonth","padStart","getDate","groupByDay","access","bucketKey","parts","split","year","month","day","isFinite","now","todayKey","yesterdayKey","monthLabel","formatDayLabel","groupByField","opts","labels","format","fallback","prototype","call","RECENCY_LABELS","groupByRecency","dateKey","recencyBucketKey","STRING_FALSE_VALUES","Set","groupByBoolean","trueLabel","falseLabel","b","lower","trim","toLowerCase","has","Boolean","coerceBoolean","SideNavView","View","sections","activeSection","navWidth","contentPadding","enableResponsive","minWidth","viewOptions","tagName","className","sectionConfigs","sectionViews","sectionKeys","currentMode","resizeObserver","lastContainerWidth","_addSectionConfig","handleResize","bind","permissions","_hasPermission","push","view","parent","perm","activeUser","hasPerm","renderTemplate","nav","_buildDropdownNav","_buildSidebarNav","navWidthStyle","contentPaddingStyle","_normalizeBadge","badge","text","variant","_renderBadge","escapeHtml","isActive","icon","join","activeConfig","c","activeLabel","onAfterRender","_mountSection","_setupResponsive","disconnect","removeEventListener","values","showSection","console","warn","isMounted","contains","previousSection","_unmountSection","activeView","onSectionActivated","_updateNavState","emit","container","_showContentLoading","_hideContentLoading","spinner","document","createElement","innerHTML","style","cssText","prepend","remove","unmount","activeKey","querySelectorAll","link","section","dataset","classList","toggle","selectBtn","onActionNavigate","el","preventDefault","_updateMode","ResizeObserver","parentElement","observe","addEventListener","containerWidth","_getContainerWidth","Math","abs","offsetWidth","newMode","mode","getActiveSection","getSectionKeys","getSection","addSection","makeActive","removeSection","k","setBadge","existing","html","insertAdjacentHTML","_onModelChange","DetailHeaderView","iconTone","iconToneFn","iconHtml","titleField","titleFn","titleAffix","subtitlePath","subtitleFn","subtitlePlaceholder","subtitleEditAction","chips","activeField","auxFn","closable","enableTooltips","contextMenuConfig","_buildTemplate","_resolveTitle","_resolveTitleAffix","_resolveSubtitle","MOJOUtils","getNestedValue","attributes","_resolveIconTone","_resolveIconHtml","_resolveChips","chip","when","textPath","tooltip","action","subtitle","chipsHtml","tooltipAttrs","auxResult","auxHtml","switchHtml","actionsHtml","a","closeHtml","contextMenuHtml","hasGroup2","tone","iconHtmlSlot","iconClass","iconBody","titleAffixHtml","_contextMenuMounted","cm","ContextMenu","context","onActionDefault","dest","events","dispatch","onActionToggleActive","checked","disabled","set","Error","err","isConnected","DetailView","subclassClassName","mergedClassName","headerConfig","sectionsConfig","initialActiveSection","onBeforeBuild","headerView","sideNav","onAfterBuild","catch","VALID_TONES","MetricCard","valueIcon","hint","_resolve","valueText","_renderValue","valueIconHtml","hintHtml","opt","_","v","safeTone","t","add","setAttribute","hasAttribute","setValue","valueEl","span","setHint","slot","appendChild","StatusPanel","state","headline","_toneOpt","_stateOpt","_headlineOpt","_metaOpt","_iconOpt","_actionsOpt","_resolveTone","_resolveActions","arr","Array","isArray","btnClass","aIcon","Timeline","emptyText","limit","_itemsOpt","floor","_resolveItems","filtered","slice","_normalizeTone","it","toneCls","detail","setItems","KnownFieldsCard","knownKeys","rawCollapsed","rawLabel","showRaw","_dataOpt","_knownKeysOpt","_resolveData","_resolveKnownKeys","rowsHtml","spec","_renderRow","_renderRaw","_lookup","isMissing","hideEmpty","valueHtml","_formatValue","json","indexOf","cursor","part","formatter","out","apply","FilePreviewView","file","isImage","content_type","isPdf","getTemplate","thumbnailUrl","url","onActionViewFile","LightboxGallery","MOJO","plugins","show","src","alt","filename","open","PDFViewer","showDialog","SHORTLINK_SOURCE_OPTIONS","TWITTER_CARD_OPTIONS","flattenShortLinkMetadata","metadata","m","og_title","og_description","og_image","twitter_card","twitter_title","twitter_description","twitter_image","buildShortLinkMetadata","formData","flatKey","targetKey","extractShortLinkPayload","flatKeys","payload","ShortLink","ShortLinkList","ShortLinkClick","ShortLinkClickList","_shortLinkSharedFields","placeholder","min","rows","ShortLinkForms","CATEGORY_TONE","_capitalize","s","str","charAt","toUpperCase","CATEGORY_CONFIG","image","previewType","badgeClass","video","audio","pdf","spreadsheet","presentation","archive","other","getCategoryConfig","cat","getCategory","FilePreviewSection","categoryConfig","escapeAttr","getThumbnailUrl","poster","preview","getBestImageRendition","openFileInPreview","onActionDownloadFile","downloadFile","FileRenditionsSection","hasRenditions","_buildGalleryTemplate","isUploadPending","_buildWaitingTemplate","_buildEmptyTemplate","renditions","getRenditions","cards","_buildCard","ct","role","file_size","bytes","n","toFixed","formatBytes","dimensions","width","height","viewData","footer","onActionRefreshRenditions","onActionViewRendition","stopPropagation","Lightbox","images","startIndex","max","findIndex","img","fitToScreen","onActionCopyRenditionUrl","navigator","clipboard","isSecureContext","writeText","textarea","select","execCommand","removeChild","orig","onActionRegenerateFromSection","node","onActionRegenerateRenditions","href","download","click","FileSharesSection","fileId","source","_sharesCollection","sharesTable","visibility","divider","danger","emptyIcon","refreshShares","onActionShareFileFromSection","onActionShareFile","onActionCopyShareCode","code","base","shortlink_base_url","origin","replace","buildShortUrl","_e","onActionRevokeShare","row","closest","rowId","target","confirmText","confirmClass","is_active","FileOverviewSection","hasUrl","kpiSize","kpiType","kpiStatus","created","kpiUploaded","FileView","File","overviewSection","previewSection","detailsSection","DataView","showEmptyValues","emptyValueText","renditionsSection","sharesSection","metadataView","contextItems","fmt","_getCategoryConfig","_stopRenditionsPoll","onActionCopyUrl","onActionEditFile","modelForm","formConfig","FileForms","onActionMakePublic","is_public","onActionMakePrivate","formResult","maxlength","submitText","expire_days","track_clicks","note","share","shortUrl","copied","expiry","expires_at","toLocaleString","tracked","copyHint","summary","shortlink_code","alert","regenerateRenditions","_maybeStartRenditionsPoll","_renditionsPollTimer","attempts","tick","clearTimeout","onActionDeleteFile","sideNavView","VIEW_CLASS","MODEL_REF","ChatMessageView","theme","isCurrentUser","cls","getBubblesTemplate","getCompactTemplate","userClass","isAssistant","_getToolCallsTemplate","tool_calls","toolBadges","tc","div","esc","function","collapseId","attachments","attachmentsContainer","children","child","filePreview","ChatInputView","buttonText","showFileInput","pendingUploads","Map","enableFileDrop","dropZoneSelector","multiple","acceptedTypes","visualFeedback","dragOverClass","dragActiveClass","autoResizeTextarea","handleKeydown","shiftKey","onActionSendMessage","onFileDrop","files","uploadFile","fileModel","uploadId","random","addFilePreview","upload","onProgress","progress","updateFileProgress","onComplete","uploadResult","handleUploadComplete","handleUploadError","formatFileSize","progressBar","delete","progressContainer","onActionRemoveAttachment","setBusy","setEnabled","enabled","button","busy","clearInput","clear","scrollHeight","i","log","parseFloat","pow","applyFileDropMixin","ChatView","adapter","currentUserId","inputPlaceholder","inputButtonText","showInput","MessageViewClass","messageViewClass","messages","messageViews","_thinkingEl","inputView","handleSendMessage","_buildMessageViews","_renderChildren","scrollToBottom","messageViewIds","resolve","messagesContainer","messageView","_createMessageView","author","addMessage","scroll","addNote","showThinking","_thinkingStart","_thinkingInterval","setInterval","elapsed","mins","secs","timerEl","hideThinking","clearInterval","setInputEnabled","requestAnimationFrame","scrollTop","clearMessages"],"mappings":"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"}