{"context":{"domain":"natura-br-cf"},"layers":{"ApplicationLayer":{"reference":"ApplicationLayer","availableInstances":[{"reference":"ComponentsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@DataSet","reference":"SystemsDataSet","params":{"filter":{"domain":"data2"},"pagination":{"size":1},"datasource":{"component":"data2-core-types@ProcessReference","params":{"reference":"SystemsDataSource"}}}},{"reference":"ExternalComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"SystemsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"5e3b796d6228227ca637d0f4","name":"data2","domain":"data2","organization":"62292184611f480b3191c37c","visibility":"public","updatedAt":"2026-07-27T14:19:36.810Z"}]}},{"reference":"OrganizationsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"DataComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@DataSet","reference":"OneFileComponentInstancesDataSet","params":{"pagination":{"size":1},"master":{"dataset":{"component":"data2-core-types@ProcessReference","params":{"reference":"SystemsDataSet"}},"localField":"system","masterField":"_id"},"datasource":{"component":"data2-core-types@ProcessReference","params":{"reference":"FileComponentInstancesDataSource"}}}},{"reference":"ApplicationsDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"ComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"reference":"ServiceComponentInstancesDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"component":"data2-core@Data2TableDataSource","reference":"FileComponentInstancesDataSource","params":{"table":"fileComponentInstances","schema":[{"component":"data2-core-types@Field","params":{"key":"component","title":"Component","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"system","title":"System","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"displayName","title":"Display Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"params","title":"Params","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"reference","title":"Reference","component":"data2-core-primitives@String"}}]}},{"component":"data2-core@MemoryDataSource","reference":"EmptyDataSource","params":{"initialData":[]}},{"reference":"ComponentsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"UserComponentsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"DataComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ServiceComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ExternalComponentInstancesDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}},{"reference":"ApplicationsDataSet","component":"data2-core@DataSet","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"EmptyDataSource"}}}}],"processesParams":{}},"HostApplicationLayer":{"reference":"HostApplicationLayer","availableInstances":[{"_id":"5f15ff190528b92601890f71","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"SystemsTableDataSource"}},"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"domain"}}},"pk":"_id","fields":[{"component":"data2-core-types@FieldReference","params":{"key":"_id"}},{"component":"data2-core-types@FieldReference","params":{"key":"name"}},{"component":"data2-core-types@FieldReference","params":{"key":"domain"}},{"component":"data2-core-types@FieldReference","params":{"key":"visibility"}},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"customDomains"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization._id"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.name"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.billingStatus"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"organization.publicTrial.active"}}},"break":true},{"component":"data2-core-types@FieldReference","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"settings.defaultAccessPolicy"}}},"break":true}],"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}},"staticPagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}},"policies":[{"component":"data2-core-auth@Policy","params":{"allow":{"params":{"actions":[{"params":{"components":[{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"_id"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"},{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"domain"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"},{"params":{"query":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$params.query"}}}},"field":{"component":"data2-core-primitives@String","params":{"content":"customDomains"}},"fieldType":"string"},"component":"data2@7KkQq0vK4a7D2gg6IVJT7"}]},"component":"data2-core-logical-operators@Or"}]},"component":"data2-core-flow@Series"}}}]},"reference":"SystemsDataSet","updatedAt":"2026-08-07T21:51:24.037Z","createdAt":"2020-07-20T20:31:21.838Z"},{"_id":"5f1601ce0528b92601890f7c","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":1000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentsTableDataSource"}},"sort":{"params.package":1,"params.component":1},"pk":"_id","autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"staticFilter":{"system":{"exists":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}},"filter":{"system":{"exists":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}}}},"reference":"ComponentsDataSet","updatedAt":"2025-11-05T19:37:01.935Z","createdAt":"2020-07-20T20:42:54.898Z"},{"_id":"5f9041dbc8da12be8b94aea4","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":2000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentsTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"UserComponentsDataSet","updatedAt":"2025-11-06T21:06:58.095Z","createdAt":"2020-07-20T20:44:09.350Z"},{"_id":"65d8c4fabc9dbb8f94de8d53","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ExternalComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ExternalComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:23.742Z","createdAt":"2024-02-23T16:17:01.816Z"},{"reference":"SystemsTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6671e96b001009d97f222f73","name":"Natura Brasil CF","domain":"natura-br-cf","visibility":"private","settings":{"defaultAccessPolicy":"private"},"updatedAt":"2026-08-19T00:29:05.737Z","createdAt":"2024-06-18T20:09:15.343Z"}]}},{"reference":"ComponentsTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"5f52a278a575399a45d239b4","component":"data2-core-types@Component","params":{"package":"data2-core-primitives","component":"Router","interactions":{"component":"data2-core-types@FieldSet","params":{"fields":{"goBack":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-types@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@This","params":{}},"prop":"history.goBack","args":[]}}}},"icon":{"component":"data2-core-primitives@String","params":{"content":"arrow-left"}},"visibility":"public"}},"goTo":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@This","params":{}},"prop":"history.push","args":[{"component":"data2-core-types@Reference","params":{"reference":"url"}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"preserveQuery"}}}}]}},"parameters":[{"component":"data2-core-primitives@String","params":{"content":"url"}},{"component":"data2-core-primitives@String","params":{"content":"preserveQuery"}}]},"break":true},"icon":{"component":"data2-core-primitives@String","params":{"content":"share"}},"visibility":"public","arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Path to route"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Path to redirect user"}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"},{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Boolean"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Preserve Query"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Preserve query string parameters  preserve query parameters on navigating."}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"}]}},"open":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-types@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"component":"data2-core-types@This","params":{}},"prop":"history.push","args":[{"pathname":{"component":"data2-core-types@Reference","params":{"reference":"value"}}}]}},"parameters":["value"]}},"icon":{"component":"data2-core-primitives@String","params":{"content":"share"}},"visibility":"public","arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Link to open"}},"description":{"component":"data2-core-primitives@String","params":{"content":""}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"},{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Target"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Where to open the link"}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"oneOf":[{"params":{"content":"_blank"},"component":"data2-core-primitives@String"},{"params":{"content":"_self"},"component":"data2-core-primitives@String"},{"params":{"content":"_parent"},"component":"data2-core-primitives@String"},{"params":{"content":"_top"},"component":"data2-core-primitives@String"}]},"component":"data2-core-types@ComponentParameter"}]}},"close":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-types@Function"}},"definition":{"component":"data2-core-functions@Function","params":{"body":{"preventEvaluation":true,"component":"data2-core-types@Getter","params":{"module":{"params":{},"component":"data2-core-web@Window"},"prop":"close"}},"parameters":["value"]}},"icon":{"component":"data2-core-primitives@String","params":{"content":"times"}},"visibility":"public","arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Link to open"}},"description":{"component":"data2-core-primitives@String","params":{"content":""}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2-core-types@ComponentParameter"},{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Target"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Where to open the link"}},"visibility":"public","required":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"oneOf":[{"params":{"content":"_blank"},"component":"data2-core-primitives@String"},{"params":{"content":"_self"},"component":"data2-core-primitives@String"},{"params":{"content":"_parent"},"component":"data2-core-primitives@String"},{"params":{"content":"_top"},"component":"data2-core-primitives@String"}]},"component":"data2-core-types@ComponentParameter"}],"description":{"component":"data2-core-primitives@String","params":{"content":"Closes a window that was opened via router.open."}}}}}}}}},{"_id":"620d28658271aa4129919d89","component":"data2-core-types@Component","params":{"package":"data2-interface-services","component":"DeviceController","interactions":{"params":{"fields":{"postMessage":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":"data2-core-primitives@String","title":{"component":"data2-core-primitives@String","params":{"content":"data"}}}}]},"component":"data2-core-types@ComponentParameter"},"print":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Object"}},"flat":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"parameters":{"zoom":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Number"}},"title":{"component":"data2-core-primitives@String","params":{"content":"zoom"}},"visibility":"public"}}},"visibility":"public"},"component":"data2-core-types@ComponentParameter"}],"visibility":"public"},"component":"data2-core-types@ComponentParameter"},"share":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}}},"component":"data2-core-types@ComponentParameter"},"clipboard.copy":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}}}}]},"component":"data2-core-types@ComponentParameter"},"clipboard.paste":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}}}}]},"component":"data2-core-types@ComponentParameter"},"download":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Filename"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Content"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Object"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Options"}},"parameters":{"params":{"fields":{"type":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Mimetype"}}}}}},"component":"data2-core-types@FieldSet"}}}]},"component":"data2-core-types@ComponentParameter"},"downloadUrl":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Filename"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Url"}}}}]},"component":"data2-core-types@ComponentParameter"},"promptFiles":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":"data2-core-primitives@Object","flat":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"parameters":{"params":{"fields":{"multiple":{"component":"data2-core-types@ComponentParameter","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Multiple"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Whether or not to allow selecting multiple files"}},"component":"data2-core-primitives@Boolean"}},"acceptedFormats":{"component":"data2-core-types@ComponentParameter","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Accepted Formats"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Accepted file formats"}},"component":"data2-core-primitives@String","list":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}},"timeout":{"component":"data2-core-types@ComponentParameter","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Timeout"}},"description":{"component":"data2-core-primitives@String","params":{"content":"Max milliseconds to wait for file upload"}},"component":"data2-core-primitives@Number"}}}},"component":"data2-core-types@FieldSet"}}}],"visibility":"public"},"component":"data2-core-types@ComponentParameter"},"meetingRecord":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","definition":{"params":{"actions":[{"params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"meeting"}},"script":{"component":"data2-core-primitives@String","params":{"content":"(function() {\n  let mediaRecorder;\n  let audioChunks = [];\n  let combinedStream;\n  let audioContext;\n  \n  window.meetingCapture = async function() {\n    try {\n      // Solicitar acesso ao áudio da guia (e vídeo, que será ignorado depois)\n      const tabStream = await navigator.mediaDevices.getDisplayMedia({ \n        video: true, // Necessário para alguns navegadores\n        audio: true\n      });\n      \n      // Remover a trilha de vídeo\n      const tabAudioTrack = tabStream.getAudioTracks()[0];\n      \n      // Solicitar acesso ao microfone\n      const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });\n      const micAudioTrack = micStream.getAudioTracks()[0];\n      \n      // Criar um novo MediaStream com ambas as trilhas de áudio\n      combinedStream = new MediaStream([tabAudioTrack, micAudioTrack]);\n      \n      // Criar um contexto de áudio para misturar os streams\n      audioContext = new AudioContext();\n      const destination = audioContext.createMediaStreamDestination();\n      \n      // Conectar a trilha de áudio da guia\n      const tabSource = audioContext.createMediaStreamSource(new MediaStream([tabAudioTrack]));\n      tabSource.connect(destination);\n      \n      // Conectar a trilha de áudio do microfone\n      const micSource = audioContext.createMediaStreamSource(new MediaStream([micAudioTrack]));\n      micSource.connect(destination);\n      \n      // Iniciar gravação com o stream misturado\n      mediaRecorder = new MediaRecorder(destination.stream);\n      audioChunks = [];\n      mediaRecorder.ondataavailable = (e) => audioChunks.push(e.data);\n      mediaRecorder.start();\n      \n      console.log('Gravação iniciada. Chame meetingCaptureStop() para finalizar.');\n    } catch (error) {\n      console.error('Erro ao iniciar a captura:', error);\n    }\n  };\n  \n  window.meetingCaptureStop = function() {\n    return new Promise((resolve, reject) => {\n      if (!mediaRecorder) {\n        reject('Nenhuma gravação em andamento.');\n        return;\n      }\n      \n      mediaRecorder.onstop = () => {\n        const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });\n        combinedStream.getTracks().forEach(track => track.stop());\n        audioContext.close();\n        \n        resolve(audioBlob);\n        console.log('Gravação finalizada. O arquivo de áudio está pronto para upload.');\n      };\n      \n      mediaRecorder.stop();\n    });\n  };\n})();"}}},"component":"data2-core-web@Script"},{"component":"data2-core-types@Getter","params":{"module":{"params":{},"component":"data2-core-web@Window"},"prop":{"component":"data2-core-primitives@String","params":{"content":"meetingCapture"}}}}]},"component":"data2-core-flow@Series"}},"component":"data2-core-types@ComponentParameter"},"meetingRecordStop":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"public","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Filename"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Content"}}}},{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Object"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Options"}},"parameters":{"params":{"fields":{"type":{"component":"data2-core-types@ComponentParameter","params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@String"}},"title":{"component":"data2-core-primitives@String","params":{"content":"Mimetype"}}}}}},"component":"data2-core-types@FieldSet"}}}],"definition":{"params":{"actions":[{"component":"data2-core-types@Getter","params":{"module":{"params":{},"component":"data2-core-web@Window"},"prop":{"component":"data2-core-primitives@String","params":{"content":"meetingCaptureStop"}}}}]},"component":"data2-core-flow@Series"}},"component":"data2-core-types@ComponentParameter"},"notify":{"params":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core-primitives@Function"}},"visibility":"private","arguments":[{"component":"data2-core-types@ComponentParameter","params":{"component":"data2-core-primitives@Object","parameters":{"title":{"params":{"component":"data2-core-primitives@String","index":{"component":"data2-core-primitives@Number","params":{"content":0}},"title":{"component":"data2-core-primitives@String","params":{"content":"title"}}},"component":"data2-core-types@ComponentParameter"},"sound":{"params":{"component":"data2-core-types@FileComponentUrlReference","index":{"component":"data2-core-primitives@Number","params":{"content":1}},"title":{"component":"data2-core-primitives@String","params":{"content":"sound"}}},"component":"data2-core-types@ComponentParameter"}},"flat":{"component":"data2-core-primitives@Boolean","params":{"content":true}}}}]},"component":"data2-core-types@ComponentParameter"}}},"component":"data2-core-types@FieldSet"}}},{"_id":"6a73f70178c8cb1a2c2c13fb","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"DoXz22oNQ74jb415tQ8PR","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const LISTENERS_RETURN = [\n  { ref: 'EJcm4dixYpPvhK93wgv1_', name: 'Rush - Webchat Authentication', gate: 'UNGATED', ret: 'decodedJWE.accessToken + core_auth + personData', note: 'Chamado pelo getPersonDataFlow-v4. É o caso reportado.' },\n  { ref: 'Uy7fAj-Z5K9KzoFtDlRv1', name: 'Fluxo GetPersonData Listener', gate: 'UNGATED', ret: 'decodedJWE.accessToken + core_auth + personData', note: 'Retorno idêntico. Mesmo vazamento, outro endpoint.' },\n  { ref: 'saVh40dkmh1Dhs1SpUd5f', name: 'backup-Fluxo GetPersonData Listener', gate: 'UNGATED', ret: 'decodedJWE.accessToken + core_auth + personData', note: 'Cópia de backup, igualmente exposta.' },\n  { ref: 'sHZn4e-P3j9-Sc3lakR0V', name: 'GetPersonData Listener Whatsapp', gate: 'UNGATED', ret: 'decodedJWE.accessToken + core_auth + personData', note: 'Mesmo retorno, canal WhatsApp.' },\n  { ref: '1w3zqjjfQ5QyW8RuXuCLM', name: 'JWEtoUserData-Test', gate: 'UNGATED', ret: 'decodedJWE.accessToken', note: 'Parcial — devolve só o accessToken decifrado.' }\n];\n\nconst LISTENERS_BODY = [\n  { ref: '8EZAJ3XAA-cB12IYhuL7E', name: 'Rush - GetOrders By Token', gate: 'UNGATED', takes: 'access_token, authorization, siteid' },\n  { ref: '8zgahkmrXYHpVz986MVOl', name: 'Rush - Get Person Data', gate: 'UNGATED', takes: 'access_token' },\n  { ref: 'oFn2XLZPrGJKG20e_yU00', name: 'Rush - Orders - OMNI', gate: 'UNGATED', takes: 'access_token' },\n  { ref: '9mvmsexdX8cNL8l5gc3Ab', name: 'Rush - BANKS', gate: 'UNGATED', takes: 'authorization, siteId' },\n  { ref: 'Uy7fAj-Z5K9KzoFtDlRv1', name: 'Fluxo GetPersonData Listener', gate: 'UNGATED', takes: 'jwe' },\n  { ref: 'saVh40dkmh1Dhs1SpUd5f', name: 'backup-Fluxo GetPersonData Listener', gate: 'UNGATED', takes: 'jwe' },\n  { ref: 'sHZn4e-P3j9-Sc3lakR0V', name: 'GetPersonData Listener Whatsapp', gate: 'UNGATED', takes: 'jwe' },\n  { ref: '1w3zqjjfQ5QyW8RuXuCLM', name: 'JWEtoUserData-Test', gate: 'UNGATED', takes: 'jwe' },\n  { ref: 'EJcm4dixYpPvhK93wgv1_', name: 'Rush - Webchat Authentication', gate: 'UNGATED', takes: 'jwe, env, siteId, device' },\n  { ref: 'lqEexrZg_qRns_YrpnNth', name: 'Rush - Get Score Boa Vista', gate: 'Policy(deny-expr)', takes: 'access_token, authorization' },\n  { ref: 'RCozTrwyQLspSwcAYhiZ6', name: 'Rush - Test Time Integration', gate: 'Policy(deny-expr)', takes: 'access_token, core_accessToken' },\n  { ref: 'iCUHDLHYCOE_K8EM0vhOl', name: 'Get Core Auth - rush', gate: 'Policy(deny-expr)', takes: 'jwe' }\n];\n\nconst C = [\n{r:\"vStX74yt33-XAveoIJOzO\",t:\"ExecStepQuestion\",a:3,d:4,c:0,j:0,p:0},\n{r:\"ecCpT0Mg8o4FzI4UDpRDZ\",t:\"ExecStepProcessAnswer\",a:3,d:3,c:0,j:0,p:0},\n{r:\"59f1Q2Sw0i0S2fPmbha9N\",t:\"chatroomMessageSend\",a:1,d:0,c:3,j:0,p:24},\n{r:\"PfjlW8C-Tyzv-ky-asFQS\",t:\"replyEndpointFullData2\",a:0,d:0,c:1,j:0,p:0},\n{r:\"aFGoB8NsL4J8La9ozaQc0\",t:\"Ai-Reply-CF-BR\",a:0,d:8,c:8,j:0,p:2},\n{r:\"ECMtUhkUPEaydS6Xeovt3\",t:\"getUserDataFromJWE\",a:0,d:0,c:0,j:0,p:9},\n{r:\"u79oR8AhbZHgD1zPLEpMT\",t:\"Troca-UI-Reason-GetScore\",a:1,d:0,c:0,j:0,p:0},\n{r:\"mMF5pVnn1HshV-aDUWsJF\",t:\"Troca-UI-GetOrders\",a:1,d:0,c:0,j:0,p:0},\n{r:\"hXntUl_1EWfU73TBy5VcM\",t:\"trackEvent\",a:0,d:0,c:0,j:0,p:3},\n{r:\"31cnx4OROIMn8-qmQFwdA\",t:\"REQCancelamento\",a:1,d:0,c:0,j:0,p:0},\n{r:\"1l4DhGQLhdUQhZbXRKGAq\",t:\"Cancelamento-UI-GetOrders\",a:1,d:0,c:0,j:0,p:0},\n{r:\"6FTVzCtzCtGt3gHtBNXDX\",t:\"Cancelamento-UI-PaymentMethod\",a:1,d:0,c:0,j:0,p:0},\n{r:\"4NLLJAOBFE6BAyWN36cGv\",t:\"getPersonDataFlow-v2\",a:0,d:0,c:0,j:0,p:4},\n{r:\"41308b_grhiCnLcuQl4NQ\",t:\"REQ_GET_OMNI\",a:1,d:0,c:0,j:0,p:0},\n{r:\"vKMRCdaIPYJKPs0CkhyRB\",t:\"REQCancelamento\",a:1,d:0,c:0,j:0,p:0},\n{r:\"s98KRCx29ADciDVKziT99\",t:\"REQGetOrders-V2\",a:0,d:1,c:1,j:0,p:0},\n{r:\"uU9BGM-EkBAf4ODy-0x1D\",t:\"getPersonDataFlow-v1\",a:0,d:0,c:0,j:0,p:3},\n{r:\"_OyaMUKD7-7XYmmTcFrYu\",t:\"REQCreateOccurrenceIdCancel\",a:0,d:1,c:1,j:0,p:0},\n{r:\"dUPXYovd2OJ8SyGxUB61H\",t:\"REQCancelamento-V2\",a:0,d:1,c:1,j:0,p:1},\n{r:\"3-e0vK0g8w59Agbx3i89K\",t:\"REQSendEvidence-Natura\",a:0,d:1,c:1,j:0,p:0},\n{r:\"PCe2xvtaPIwP0h2_3I_-Y\",t:\"REQQualityMatrix-Natura\",a:0,d:1,c:1,j:0,p:0},\n{r:\"2c1366etFR9ti8Ytw_LJe\",t:\"REQAnti-Fraude-Natura\",a:0,d:1,c:1,j:0,p:0},\n{r:\"vfn2R9HVP_41trY3IvzC3\",t:\"REQBasketCreate-Natura\",a:0,d:1,c:1,j:0,p:1},\n{r:\"cCLCUyTJbdi1uNyeKxxsv\",t:\"REQBasketClose-Natura\",a:0,d:1,c:1,j:0,p:1},\n{r:\"reO2GlYSJTQbvOzQ57JWV\",t:\"REQProductDetail\",a:0,d:1,c:1,j:0,p:0},\n{r:\"L2ll4USSsuocHTIIgaVaw\",t:\"REQManifestationMatrix\",a:0,d:1,c:1,j:0,p:0},\n{r:\"pl929QdFM_09RCL5RMIXh\",t:\"REQCreateBody - CancelNatura\",a:0,d:1,c:1,j:0,p:2},\n{r:\"_p8gpCNiwuq_CJzqHlkeX\",t:\"REQUpdateOccurrenceId\",a:0,d:1,c:1,j:0,p:0},\n{r:\"fdV1FH94t0VJT_8i6uqf-\",t:\"REQCancelamento\",a:1,d:0,c:0,j:0,p:0},\n{r:\"g7z2M3m49XWaP21t5Pgfh\",t:\"REQReasonMap\",a:0,d:1,c:1,j:0,p:0},\n{r:\"LANcyiakOJDG-8RTiw6Uh\",t:\"REQTroca-V2-Natura\",a:0,d:1,c:1,j:0,p:1},\n{r:\"_aLxTQVRqQaOTGekoZBNi\",t:\"REQCreateOccurrenceIdTroca\",a:0,d:1,c:1,j:0,p:0},\n{r:\"Ui3KwUhDcpAftHRAUARNK\",t:\"REQCreateBody - TrocaNatura\",a:0,d:1,c:1,j:0,p:2},\n{r:\"YbvZ06lvlLe29BWs6hb9c\",t:\"REQUpdateOccurrenceId - Troca\",a:0,d:1,c:1,j:0,p:0},\n{r:\"KYT-W335-vl7OVT0BQCFS\",t:\"REQCancel-Avon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"eQaGLH31-s7qjm7Kk-aFR\",t:\"REQCreateBody - CancelAvon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"NcUxUx92FzLWlPoRwJZBM\",t:\"REQUpdateOccurrenceceId - CancelAvon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"wF41Z5pioJ0n_MVy2Bm7U\",t:\"REQCreateOccurIdCancelAvon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"R_ijY7ASKb3JHhd8gx24F\",t:\"REQReasonMap - Avon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"2q4GJSS_q03pZ7s3aH2bw\",t:\"REQAnti-Fraude-Avon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"TH_tvudD110ZYWO0RP_fd\",t:\"REQQualityMatrix-Avon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"L5yv87YCAWxdALoIjUUKU\",t:\"REQBasketCreate-Avon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"FpF_VqJ6R6ykEdoa-6LFL\",t:\"REQSendEvidence-Avon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"scNin6EnHToKt0xdkyYl7\",t:\"REQBasketClose-Avon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"O_-GK5fTXUuxxrSQ0ZGfw\",t:\"REQTroca-V2-Avon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"_S6ICI6gP6b1mDFmqFqll\",t:\"REQCreateBody - TrocaAvon\",a:0,d:1,c:1,j:0,p:2},\n{r:\"_7Iqat8caVyulZ32v9MAq\",t:\"REQUpdateOccurrenceId - TrocaAvon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"xTbTa7pty9C0-N5aKwk4I\",t:\"REQCreateOccurrenceIdTrocaAvon\",a:0,d:1,c:1,j:0,p:0},\n{r:\"q0K5ZekuEbFkbTc2zs81H\",t:\"REQCreateBody - TrocaSNAC\",a:0,d:1,c:1,j:0,p:1},\n{r:\"rgjc6JA32ezkLuTEhjc9J\",t:\"getPersonDataFlow-v3\",a:0,d:0,c:0,j:0,p:7},\n{r:\"ehiTKUW50DWaB5nHDH749\",t:\"REQTroca-V2-SNAC\",a:0,d:1,c:1,j:0,p:1},\n{r:\"O7XZRLaBlUYM9lYLmEUOX\",t:\"REQTroca-V2-SNAC-Avon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"xI2au4qchQr1WOfFh3GKa\",t:\"REQCreateBody - Avon SNAC Troca\",a:0,d:1,c:1,j:0,p:1},\n{r:\"f-d4dMrbP6nti3OzrsxF8\",t:\"test - getUserDataFromJWE\",a:0,d:0,c:0,j:0,p:2},\n{r:\"i151cpSTbjF2-VNdh6rTB\",t:\"getPersonDataFlow-v4\",a:1,d:1,c:0,j:1,p:1},\n{r:\"5fQzwYjGb0uUuTWcAzGpk\",t:\"REQBasketCreateSNAC-Natura\",a:0,d:1,c:1,j:0,p:1},\n{r:\"HABXLn1pj1LvtMIrzCM-n\",t:\"REQBasketCloseSNAC-Natura\",a:0,d:1,c:1,j:0,p:1},\n{r:\"iTMlHzL47i-fGgTJMuwYz\",t:\"REQBasketCreateSnac-Avon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"zYSbDt-CFHNVLIo807YrM\",t:\"REQBasketCloseSNAC-Avon\",a:0,d:1,c:1,j:0,p:1},\n{r:\"FhloAGogvyF3ECTlYIpdE\",t:\"REQCreateBody - Global\",a:0,d:1,c:1,j:0,p:0},\n{r:\"kjn32D7lj8cjb5qH3kKhk\",t:\"REQCreateOccurrenceId - Global\",a:0,d:1,c:1,j:0,p:0},\n{r:\"3Pe3tZIn6tYdWeOczRut7\",t:\"REQUpdateOccurrenceId - Global\",a:0,d:1,c:1,j:0,p:0},\n{r:\"TJJrDEr5Oq9QGrWQsCM1r\",t:\"V2 GET_PERSON_DATA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"DMJEEPsIXeKu5vFtzBXz4\",t:\"V2 GET_ORDERS (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"BL9HivCT3xPPNp4m4xan4\",t:\"SNAC ONBOARDING (WIP)\",a:0,d:0,c:0,j:2,p:0},\n{r:\"sSsBTDPg2kfeDsatVRl6B\",t:\"SNAC BOA VISTA (WIP)\",a:0,d:0,c:0,j:2,p:0},\n{r:\"Z7iCWDQjbjOu2WaCtG77Y\",t:\"V2 QUALITY MATRIX (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"59rC-UPPBItXDMrd5blFB\",t:\"V2 ANTI FRAUDE (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"6pZaxTK5Vw_SGvFZaUTOz\",t:\"V2 SEND EVIDENCE (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"PRuEdfzOOUS5R3-1f1Ke4\",t:\"V2 CREATE BODY (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"uuOsvgeNXsi-4yjq6YcSn\",t:\"V2 REASON MAP (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"z_ZHt93DHeOchvxqOPjTv\",t:\"V2 BASKET CREATE (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"AqDCWwS3--J5AHEIvqI5D\",t:\"V2 BASKET CLOSE (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"FAxzSrS81fSEG-2lQ18EY\",t:\"V2 CORE TRADE (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"HdszE2oQwAac8Wad9VLMF\",t:\"V2 OCCURRENCE (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"mNsTbqOzawQuuT3WerPIv\",t:\"V2 UPDATE OCCURRENCE (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"ZXajb3LOm5xfA3uTLzA7z\",t:\"insert-step-server-api-response\",a:0,d:0,c:0,j:1,p:0},\n{r:\"oUwMJQizrRMeRd6i58mEr\",t:\"CHANGE VALUES (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"nE4eZEUZovaGcGjHsIpqC\",t:\"V2 MANIFESTATION MATRIX (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"eiobNwlyFAHqkAskRNH82\",t:\"V2 PRODUCT DETAILS (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"_gcZqj2AIp8aiDHznkdpt\",t:\"CATALOG SNAC (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"e3nAL6pzDBShuhfML2FSm\",t:\"V2 BASKET CREATE SNAC (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"-sy1L5mTV96oyA-cQ8WRG\",t:\"V2 CORE TRADE SNAC (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"9VlpmDl0OGkiBcHuQbTAQ\",t:\"V2 CREATE BODY SNAC (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"5YnBmU9d7a5PDP1zvBDkc\",t:\"reply-message-new-llm\",a:0,d:2,c:2,j:0,p:0},\n{r:\"ZelTOYmkGcOmSBPC1RQw9\",t:\"reply-message-old-llm\",a:0,d:2,c:2,j:0,p:0},\n{r:\"8BW6V-l9Ry_ITDh8egP4a\",t:\"rate-limit-register\",a:0,d:0,c:0,j:0,p:2},\n{r:\"0KR1Piva_Nc1kbowS7lgj\",t:\"V2 BASKET CLOSE SNAC (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"Rm2iFyig2avbt__SfMNwW\",t:\"V2 GET_ORDERS_FALTA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"eNgsMP_A12XtupeMDe__F\",t:\"V2 OCCURRENCE FALTA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"F2J1uWEPDkv80DcImZg-2\",t:\"V2 UPDATE OCCURRENCE FALTA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"yZmxrfbW8u-82pLqv59VN\",t:\"V2 REASON MAP FALTA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"F_vKoHHmelESV6cg5xu0i\",t:\"V2 BASKET CREATE FALTA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"Mbe8pNF413Vk0iCC_kR0h\",t:\"V2 BASKET CLOSE FALTA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"vrlX5X1a9WiMHRMgf4OVA\",t:\"V2 CREATE BODY FALTA (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"Vi7k8Z2apVlVvf-5ewASB\",t:\"create-or-update-session-report\",a:0,d:0,c:0,j:0,p:3},\n{r:\"5ech-ZV5XL34h8LH-YNo6\",t:\"placholder-atraso-loadig\",a:0,d:0,c:0,j:0,p:1},\n{r:\"OxF1DUta1NlueM-iU2aEB\",t:\"V2 GET_ORDERS_CANCELAMENTO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"vrbHOkZUUu-_8Hp2OGfND\",t:\"V2-chatroomMessageSend\",a:0,d:0,c:0,j:1,p:0},\n{r:\"lYmRZJBMwCfJZC7TIPrIH\",t:\"ANTI FRAUDE CANCELAMENTO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"albj4W37Kfbr-9BZrq7ka\",t:\"OCCURRENCE CANCELAMENTO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"jSOkamDA7rk_riA7NNKmu\",t:\"REASON MAP CANCELAMENTO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"G4fDjOPdNSnucdNlDGW9B\",t:\"CORE TRADE CANCELAMENTO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"M6pMnxjxvWewLg02wHJs1\",t:\"CREATE BODY CANCELAMENTO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"3lW8CBliC7cZxjrunGgTw\",t:\"UPDATE OCCURRENCE CANCELAMENTO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"5vFbJtYWUwGzMOD7QfoSk\",t:\"V2 GET ORDERS ATRASO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"IJ1--cQTjII3s7ZvZgV2m\",t:\"V2 CREATE BODY ATRASO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"HDYlhZrIXU9eGBpNSefOK\",t:\"V2 OCCURRENCE ATRASO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"DgD5JRT4hL8XUQUV-krbz\",t:\"V2 UPDATE OCCURRENCE ATRASO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"9zPVjcPvlBgd2tNSiW6Vj\",t:\"V2 REASON MAP ATRASO (WIP)\",a:0,d:0,c:0,j:1,p:0},\n{r:\"v3PrepareContext00001\",t:\"V3-PrepareContext\",a:0,d:3,c:0,j:0,p:0}\n];\n\nconst FIELDS = [\n  { k: 'a', label: 'infoUser.access_token', store: 'store-fluxo-troca', desc: 'Cópia do accessToken do usuário Natura.' },\n  { k: 'd', label: 'infoUser.decodedJWE.accessToken', store: 'store-fluxo-troca', desc: 'Token do usuário extraído do JWE. Credencial real.' },\n  { k: 'c', label: 'infoUser.core_auth.access_token', store: 'store-fluxo-troca', desc: 'Token de serviço do Core. Nunca deveria sair do servidor.' },\n  { k: 'j', label: 'data.JWE', store: 'store cliente', desc: 'JWE bruto guardado no cliente.' },\n  { k: 'p', label: 'data.person_data / infoUser.personData', store: 'ambas', desc: 'PII completa do cliente.' }\n];\n\nconst esc = (s) => String(s == null ? '' : s).replace(/[&<>\"]/g, (m) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[m]));\nconst count = (k) => C.filter((x) => x[k] > 0).length;\n\nexport default {\n  css: `\n    :host { display:block; }\n    .wrap { font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n      background:#0f1115; color:#e6e8ee; padding:40px 48px 64px; min-height:100%;\n      -webkit-font-smoothing:antialiased; }\n    .kicker { font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:#8b93a7; margin:0 0 8px; }\n    h1 { font-size:30px; line-height:1.15; margin:0 0 10px; font-weight:650; letter-spacing:-.02em; }\n    .sub { color:#9aa3b8; font-size:14.5px; max-width:78ch; line-height:1.6; margin:0 0 28px; }\n    .kpis { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px; margin:0 0 34px; }\n    .kpi { background:#161a22; border:1px solid #232936; border-radius:12px; padding:16px 18px; }\n    .kpi .n { font-size:28px; font-weight:650; letter-spacing:-.02em; }\n    .kpi .l { font-size:12px; color:#8b93a7; margin-top:4px; line-height:1.4; }\n    .kpi.bad .n { color:#ff6b6b; } .kpi.warn .n { color:#ffb457; } .kpi.ok .n { color:#5ecf8f; }\n    h2 { font-size:17px; margin:36px 0 6px; font-weight:600; letter-spacing:-.01em; }\n    h2 .num { color:#5b657d; font-weight:500; margin-right:8px; }\n    .hint { color:#8b93a7; font-size:13.5px; line-height:1.6; margin:0 0 16px; max-width:86ch; }\n    table { width:100%; border-collapse:collapse; font-size:13px; }\n    th { text-align:left; font-weight:600; color:#8b93a7; font-size:11px; letter-spacing:.06em;\n      text-transform:uppercase; padding:0 12px 8px; border-bottom:1px solid #232936; }\n    td { padding:10px 12px; border-bottom:1px solid #1b202a; vertical-align:top; line-height:1.5; }\n    tr:hover td { background:#141821; }\n    code { font-family: ui-monospace, \"SF Mono\", Menlo, monospace; font-size:12px; color:#9ad0ff;\n      background:#151b25; padding:1.5px 5px; border-radius:4px; }\n    .tag { display:inline-block; font-size:11px; font-weight:600; padding:2.5px 8px; border-radius:999px; white-space:nowrap; }\n    .tag.ungated { background:#3a1a1e; color:#ff7b7b; border:1px solid #542127; }\n    .tag.gated { background:#16301f; color:#5ecf8f; border:1px solid #1f4630; }\n    .payload { background:#141821; border:1px solid #232936; border-left:3px solid #ff6b6b;\n      border-radius:8px; padding:16px 18px; font-family:ui-monospace,Menlo,monospace;\n      font-size:12.5px; line-height:1.75; white-space:pre; overflow-x:auto; color:#cfd6e4; margin:0 0 8px; }\n    .payload .leak { color:#ff8a8a; }\n    .payload .cmt { color:#5b657d; }\n    .chips { display:flex; flex-wrap:wrap; gap:8px; margin:0 0 18px; }\n    .chip { background:#161a22; border:1px solid #232936; color:#c3cad9; font-size:12.5px;\n      padding:7px 13px; border-radius:8px; cursor:pointer; transition:.12s; font-weight:500; }\n    .chip:hover { border-color:#39445a; }\n    .chip.on { background:#1d2635; border-color:#3d6ea8; color:#9ad0ff; }\n    .muted { color:#6d768c; }\n    .foot { margin-top:40px; padding-top:20px; border-top:1px solid #232936; color:#6d768c;\n      font-size:12.5px; line-height:1.75; max-width:92ch; }\n    .foot b { color:#9aa3b8; font-weight:600; }\n    @media (max-width:1100px){ .kpis{grid-template-columns:repeat(2,minmax(0,1fr));} }\n  `,\n\n  render(d2) {\n    const sel = d2.state.field || 'all';\n    const rows = sel === 'all' ? C : C.filter((x) => x[sel] > 0);\n\n    const chips = [{ k: 'all', label: 'Todos', n: C.length }]\n      .concat(FIELDS.map((f) => ({ k: f.k, label: f.label, n: count(f.k) })))\n      .map((c) => `<button class=\"chip ${c.k === sel ? 'on' : ''}\" data-k=\"${c.k}\">${esc(c.label)} <span class=\"muted\">${c.n}</span></button>`)\n      .join('');\n\n    const fieldCols = (x) => FIELDS.filter((f) => x[f.k] > 0).map((f) => `<code>${f.label.split('.').slice(-1)[0]}</code>`).join(' ');\n\n    return `\n      <div class=\"wrap\">\n        <p class=\"kicker\">natura-br-cf · auth-process</p>\n        <h1>Inventário do vazamento de credenciais na autenticação</h1>\n        <p class=\"sub\">Levantamento de quem produz e quem consome os tokens hoje. Base da Fase 0 do plano de correção:\n        sem esta tabela não dá para enxugar o retorno do listener sem quebrar consumidor. Escopo varrido:\n        2.368 HTTPListeners e 562 user components do pacote <code>natura-br-cf</code>.</p>\n\n        <div class=\"kpis\">\n          <div class=\"kpi bad\"><div class=\"n\">5</div><div class=\"l\">listeners devolvem credencial ao browser</div></div>\n          <div class=\"kpi warn\"><div class=\"n\">12</div><div class=\"l\">listeners recebem credencial pelo body do cliente</div></div>\n          <div class=\"kpi bad\"><div class=\"n\">9</div><div class=\"l\">desses 12 estão <b>UNGATED</b></div></div>\n          <div class=\"kpi ok\"><div class=\"n\">${C.length}</div><div class=\"l\">componentes consomem os campos das stores</div></div>\n        </div>\n\n        <h2><span class=\"num\">1</span>O payload que vaza na Network</h2>\n        <p class=\"hint\">Última expressão do handler de <code>EJcm4dixYpPvhK93wgv1_</code>. Como o <code>invoke</code> parte do\n        browser, este objeto é a resposta HTTP — visível na aba Network por construção.</p>\n        <div class=\"payload\">{\n  <span class=\"leak\">decodedJWE: { accessToken }</span>   <span class=\"cmt\">// token do usuário Natura, decifrado do JWE</span>\n  <span class=\"leak\">core_auth</span>                      <span class=\"cmt\">// resposta inteira do Core Auth, inclui access_token de serviço</span>\n  <span class=\"leak\">personData</span>                     <span class=\"cmt\">// PII completa, sem recorte</span>\n  disabled: false                <span class=\"cmt\">// resíduo, não faz parte do contrato</span>\n}</div>\n\n        <h2><span class=\"num\">2</span>Listeners que devolvem credencial ao cliente</h2>\n        <p class=\"hint\">O mesmo retorno está replicado em 5 endpoints — corrigir só o do <code>getPersonDataFlow-v4</code>\n        deixa quatro portas abertas.</p>\n        <table>\n          <thead><tr><th style=\"width:22%\">Listener</th><th style=\"width:18%\">Reference</th><th style=\"width:10%\">Gate</th><th style=\"width:26%\">Devolve</th><th>Observação</th></tr></thead>\n          <tbody>${LISTENERS_RETURN.map((l) => `<tr>\n            <td><b>${esc(l.name)}</b></td>\n            <td><code>${esc(l.ref)}</code></td>\n            <td><span class=\"tag ${l.gate === 'UNGATED' ? 'ungated' : 'gated'}\">${esc(l.gate)}</span></td>\n            <td><code>${esc(l.ret)}</code></td>\n            <td class=\"muted\">${esc(l.note)}</td></tr>`).join('')}\n          </tbody>\n        </table>\n\n        <h2><span class=\"num\">3</span>Listeners que exigem credencial vinda do cliente</h2>\n        <p class=\"hint\">São estes que travam a remoção do token do browser: enquanto lerem <code>$body.access_token</code>\n        ou <code>$body.authorization</code>, o front precisa ter a credencial em mãos. Cada um destes é um item de trabalho\n        da migração para sessão server-side.</p>\n        <table>\n          <thead><tr><th style=\"width:30%\">Listener</th><th style=\"width:22%\">Reference</th><th style=\"width:16%\">Gate</th><th>Lê do body</th></tr></thead>\n          <tbody>${LISTENERS_BODY.map((l) => `<tr>\n            <td><b>${esc(l.name)}</b></td>\n            <td><code>${esc(l.ref)}</code></td>\n            <td><span class=\"tag ${l.gate === 'UNGATED' ? 'ungated' : 'gated'}\">${esc(l.gate)}</span></td>\n            <td><code>${esc(l.takes)}</code></td></tr>`).join('')}\n          </tbody>\n        </table>\n\n        <h2><span class=\"num\">4</span>Campos de credencial nas stores</h2>\n        <table>\n          <thead><tr><th style=\"width:28%\">Campo</th><th style=\"width:16%\">Store</th><th style=\"width:10%\">Consumidores</th><th>O que é</th></tr></thead>\n          <tbody>${FIELDS.map((f) => `<tr>\n            <td><code>${esc(f.label)}</code></td>\n            <td class=\"muted\">${esc(f.store)}</td>\n            <td><b>${count(f.k)}</b></td>\n            <td class=\"muted\">${esc(f.desc)}</td></tr>`).join('')}\n          </tbody>\n        </table>\n\n        <h2><span class=\"num\">5</span>Consumidores client-side <span class=\"muted\" style=\"font-weight:400\">— ${rows.length} de ${C.length}</span></h2>\n        <p class=\"hint\">Filtre por campo para dimensionar o impacto de cada corte. O bloco <code>REQ*</code> concentra a maior\n        parte: são ~55 componentes que leem <code>decodedJWE.accessToken</code> e <code>core_auth.access_token</code>\n        exatamente do mesmo jeito — ou seja, migram por padrão, não um a um no braço.</p>\n        <div class=\"chips\">${chips}</div>\n        <table>\n          <thead><tr><th style=\"width:34%\">Componente</th><th style=\"width:22%\">Reference</th><th>Campos que lê</th></tr></thead>\n          <tbody>${rows.map((x) => `<tr data-key=\"${esc(x.r)}\">\n            <td><b>${esc(x.t)}</b></td>\n            <td><code>${esc(x.r)}</code></td>\n            <td>${fieldCols(x)}</td></tr>`).join('')}\n          </tbody>\n        </table>\n\n        <div class=\"foot\">\n          <b>Como foi levantado.</b> Varredura server-side de <code>ServiceComponentInstancesDataSet</code>\n          (2.368 registros <code>data2-rush@HTTPListener</code>, 5 páginas, cobertura integral) procurando\n          <code>$body.access_token</code>, <code>$body.authorization</code>, <code>core_accessToken</code> e\n          <code>$body.jwe</code>; e de <code>UserComponentsDataSet</code> filtrado por\n          <code>params.package = natura-br-cf</code> (562 registros, cobertura integral) procurando os caminhos\n          das stores. Gates confirmados via <code>list_services{includePolicies}</code> e\n          <code>security_scan</code> — o listener de auth retorna <code>protected: false</code>, <code>currentPolicies: []</code>.<br><br>\n          <b>Limites deste inventário.</b> Cobre user components; blocos de artboard não foram varridos, então\n          um getter escrito direto num bloco de tela não aparece aqui. A varredura dos listeners cruzou datasets\n          centrais que incluem outros sistemas — só entraram na lista os 12 que também aparecem na listagem\n          do próprio sistema. Contagens são ocorrências do caminho no CD, não chamadas em runtime:\n          um componente com <code>(1)</code> pode executar o getter várias vezes.\n        </div>\n      </div>`;\n  },\n\n  mounted(d2, root) {\n    root.addEventListener('click', (e) => {\n      const b = e.target.closest('.chip');\n      if (b) d2.state.field = b.dataset.k;\n    });\n  }\n};"}}}}}}}},{"_id":"6a740a9a90f4048f44299d7f","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"pqiA5M_M7iuczK1RybGPM","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const STORES = [\n  { key: 'chat',  label: 'cf-chatroom',  param: 'chatStore',   ref: 'ufKuWDW7edV_702iYowsi' },\n  { key: 'entry', label: 'entrypoint',   param: 'entryStore',  ref: 'NIc2rphwDz6yq9gNdWJQo' },\n  { key: 'client',label: 'client',       param: 'clientStore', ref: '0m2nXSHsvhtXAnyJbd4cT' },\n  { key: 'embed', label: 'embed-chat',   param: 'embedStore',  ref: 'XYJbrrt9WXGWPqC0jb84k' },\n  { key: 'env',   label: 'env',          param: 'envStore',    ref: 'HvjTXvK5P2jzTnoh81tCB' },\n  { key: 'input', label: 'input',        param: 'inputStore',  ref: '4VY4FzGmgLCdrDJfZd35R' },\n  { key: 'dev',   label: 'dev',          param: 'devStore',    ref: 'yvrRDnZw6fGfy_7ailFxP' }\n]\n\nfunction handle(d2, spec) {\n  return d2.store(spec.param) || d2.store(spec.ref)\n}\n\nfunction esc(s) {\n  return String(s == null ? '' : s)\n    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\nfunction pill(value, kind) {\n  if (value == null || value === '') return '<span class=\"pill pill-none\">—</span>'\n  return '<span class=\"pill pill-' + (kind || 'plain') + '\">' + esc(value) + '</span>'\n}\n\nfunction short(id) {\n  const s = String(id == null ? '' : id)\n  return s.length > 10 ? s.slice(0, 10) + '…' : s\n}\n\nfunction preview(m) {\n  if (m && m.text && m.text.body) return m.text.body\n  if (m && m.audio && m.audio.link) return '[audio]'\n  if (m && m.image && m.image.link) return '[imagem]'\n  if (m && m.video && m.video.link) return '[video]'\n  if (m && m.document && m.document.link) return '[documento] ' + (m.document.filename || '')\n  if (m && m.button_reply) return '[botao] ' + (m.button_reply.title || m.button_reply.id || '')\n  if (m && m.list_reply) return '[lista] ' + (m.list_reply.title || m.list_reply.id || '')\n  return ''\n}\n\nexport default {\n  css: `\n    :host { display: block; }\n    .wrap { display: flex; flex-direction: column; height: 100%; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; background: #12151c; color: #e6e8ee; border-radius: 8px; overflow: hidden; }\n    .tabs { display: flex; gap: 2px; padding: 8px 8px 0 8px; background: #0d1016; flex: 0 0 auto; flex-wrap: wrap; }\n    .tab { padding: 6px 12px; font-size: 12px; border-radius: 6px 6px 0 0; background: #1a1f29; color: #9aa3b2; cursor: pointer; user-select: none; border: 0; }\n    .tab:hover { color: #e6e8ee; }\n    .tab.on { background: #12151c; color: #fff; box-shadow: inset 0 2px 0 #4f8cff; }\n    .body { flex: 1 1 auto; overflow: auto; padding: 12px; }\n    .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 8px; margin-bottom: 14px; }\n    .card { background: #1a1f29; border-radius: 8px; padding: 9px 11px; }\n    .card .k { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: #7a8496; margin-bottom: 5px; }\n    .card .v { font-size: 13px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }\n    .pill { display: inline-block; padding: 1px 7px; border-radius: 999px; font-size: 11px; font-family: ui-monospace, monospace; }\n    .pill-plain { background: #2a3140; color: #cfd6e4; }\n    .pill-none { background: transparent; color: #59616f; }\n    .pill-ok { background: #163d2a; color: #62d99a; }\n    .pill-warn { background: #4a3410; color: #ffc766; }\n    .pill-bad { background: #4a1620; color: #ff8095; }\n    .pill-live { background: #10314a; color: #63b8ff; }\n    h4 { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: #7a8496; margin: 0 0 8px 0; font-weight: 600; }\n    table { width: 100%; border-collapse: collapse; font-size: 12px; }\n    th { text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: #7a8496; padding: 4px 8px; border-bottom: 1px solid #242b38; font-weight: 600; }\n    td { padding: 5px 8px; border-bottom: 1px solid #1b212c; font-family: ui-monospace, monospace; vertical-align: top; }\n    tr.human td { background: rgba(79,140,255,.05); }\n    td.txt { font-family: inherit; max-width: 320px; }\n    pre { margin: 0; font-size: 12px; font-family: ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; color: #b9c2d3; }\n    .empty { color: #59616f; font-size: 12px; padding: 20px 0; text-align: center; }\n  `,\n\n  render(d2) {\n    const tab = d2.state.tab || 'v3'\n    const tabsHtml = ['<button class=\"tab ' + (tab === 'v3' ? 'on' : '') + '\" data-tab=\"v3\">v3 status</button>']\n      .concat(STORES.map(function (s) {\n        return '<button class=\"tab ' + (tab === s.key ? 'on' : '') + '\" data-tab=\"' + s.key + '\">' + s.label + '</button>'\n      }))\n      .join('')\n\n    let body = ''\n\n    if (tab === 'v3') {\n      const chat = handle(d2, STORES[0])\n      const entry = handle(d2, STORES[1])\n      const input = handle(d2, STORES[5])\n      const msgs = (chat && chat.get('data.messages', [])) || []\n      const sess = d2.data('sessionData')\n      const cur = (sess && sess.selected()) || null\n\n      const sending = msgs.filter(function (m) { return m && m.delivery === 'sending' }).length\n      const failed = msgs.filter(function (m) { return m && m.delivery === 'failed' }).length\n      const noId = msgs.filter(function (m) { return m && !m._id }).length\n      const responding = chat && chat.get('data.responding')\n\n      body += '<div class=\"grid\">'\n      body += '<div class=\"card\"><div class=\"k\">chatId</div><div class=\"v\">' + pill(chat && chat.get('data.chatId'), 'live') + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">session.status (servidor)</div><div class=\"v\">' + pill(cur && cur.status, 'live') + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">chat_status (ack)</div><div class=\"v\">' + pill(chat && chat.get('data.chat_status')) + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">responding</div><div class=\"v\">' + (responding ? pill('digitando', 'warn') : pill('parado', 'ok')) + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">na tela / sending / failed</div><div class=\"v\">' + pill(msgs.length) + ' ' + (sending ? pill(sending + ' sending', 'warn') : '') + ' ' + (failed ? pill(failed + ' failed', 'bad') : '') + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">sem _id (morre no socket v1)</div><div class=\"v\">' + (noId ? pill(noId, 'bad') : pill('0', 'ok')) + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">audio_state</div><div class=\"v\">' + pill(chat && chat.get('data.audio_state')) + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">attachment_state</div><div class=\"v\">' + pill(chat && chat.get('data.attachment_state')) + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">input</div><div class=\"v\">' + pill(input && input.get('data.input')) + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">question (servidor)</div><div class=\"v\">' + pill(entry && entry.get('data.question')) + '</div></div>'\n      body += '<div class=\"card\"><div class=\"k\">use_case da ultima resposta</div><div class=\"v\">' + pill(entry && entry.get('data.answerObj.selected_usecase')) + '</div></div>'\n      body += '</div>'\n\n      body += '<h4>mensagens em cf-chatroom-store.data.messages</h4>'\n      if (!msgs.length) {\n        body += '<div class=\"empty\">nenhuma mensagem</div>'\n      } else {\n        body += '<table><thead><tr><th>idx</th><th>sender</th><th>type</th><th>delivery</th><th>id</th><th>_id</th><th>texto</th></tr></thead><tbody>'\n        msgs.forEach(function (m, i) {\n          const dv = m && m.delivery\n          const dpill = dv === 'failed' ? pill('failed', 'bad') : (dv === 'sending' ? pill('sending', 'warn') : (dv ? pill(dv, 'ok') : pill(null)))\n          body += '<tr class=\"' + (m && m.sender === 'HUMAN' ? 'human' : '') + '\" data-key=\"' + esc((m && (m.id || m._id)) || i) + '\">'\n          body += '<td>' + esc(m && m.index != null ? m.index : '—') + '</td>'\n          body += '<td>' + esc((m && m.sender) || '') + '</td>'\n          body += '<td>' + esc((m && m.type) || '') + '</td>'\n          body += '<td>' + dpill + '</td>'\n          body += '<td>' + esc(short(m && m.id)) + '</td>'\n          body += '<td>' + (m && m._id ? pill('sim', 'ok') : pill('nao', 'bad')) + '</td>'\n          body += '<td class=\"txt\">' + esc(preview(m)) + '</td>'\n          body += '</tr>'\n        })\n        body += '</tbody></table>'\n      }\n    } else {\n      const spec = STORES.filter(function (s) { return s.key === tab })[0]\n      const h = spec && handle(d2, spec)\n      if (!h) {\n        body += '<div class=\"empty\">store nao resolvida: ' + esc(spec ? spec.ref : tab) + '</div>'\n      } else {\n        let json\n        try { json = JSON.stringify(h.get('data', {}), null, 2) } catch (e) { json = String(e) }\n        body += '<h4>' + esc(spec.label) + ' &middot; ' + esc(spec.ref) + '</h4>'\n        body += '<pre>' + esc(json) + '</pre>'\n      }\n    }\n\n    return '<div class=\"wrap\"><div class=\"tabs\">' + tabsHtml + '</div><div class=\"body\">' + body + '</div></div>'\n  },\n\n  mounted(d2) {\n    d2.on('.tab', 'click', function (e) {\n      const t = e.target.closest('.tab')\n      if (t) d2.state.tab = t.getAttribute('data-tab')\n    })\n  }\n}"}},"chatStore":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"chatStore"}}}},"entryStore":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"entryStore"}}}},"clientStore":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"clientStore"}}}},"embedStore":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"embedStore"}}}},"envStore":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"envStore"}}}},"inputStore":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"inputStore"}}}},"devStore":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"devStore"}}}},"sessionData":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessionData"}}}}}}}}}},{"_id":"6a7652bbe612e66e181d6f72","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"mcp-historico-mensagens","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"export default {\n  css: `\n    :host{all:initial}\n    *{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}\n    .wrap{display:flex;flex-direction:column;height:100%;background:#faf9f7;color:#1a1714;font-size:14px}\n    .bar{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;padding:14px 16px;background:#fff;border-bottom:1px solid #e6e1db}\n    .fld{display:flex;flex-direction:column;gap:4px}\n    .fld label{font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:#6b625a;font-weight:600}\n    .fld input{padding:8px 10px;border:1px solid #e6e1db;border-radius:8px;font-size:13px;background:#fff;color:#1a1714;min-width:150px}\n    .fld input:focus,.fld select:focus{outline:2px solid #EB6619;outline-offset:-1px;border-color:transparent}\n    .fld.wide input{min-width:230px}\n    .fld.sm input{min-width:92px;width:92px}\n    .fld select{padding:8px 10px;border:1px solid #e6e1db;border-radius:8px;font-size:13px;background:#fff;color:#1a1714;min-width:118px;font-family:inherit}\n    .btn{padding:9px 16px;border:none;border-radius:8px;background:#EB6619;color:#fff;font-size:13px;font-weight:600;cursor:pointer}\n    .btn:hover{background:#d25a13}\n    .btn.sec{background:#fff;color:#6b625a;border:1px solid #e6e1db;font-weight:500}\n    .hint{font-size:12px;color:#6b625a;margin-left:auto;white-space:nowrap}\n    .body{display:flex;flex:1;min-height:0}\n    .left{width:330px;min-width:280px;border-right:1px solid #e6e1db;overflow:auto;background:#fff}\n    .right{flex:1;overflow:auto;padding:18px 22px;min-width:0}\n    .sitem{padding:11px 14px;border-bottom:1px solid #f0ece7;cursor:pointer}\n    .sitem:hover{background:#fdf0e7}\n    .sitem[data-sel]{background:#fdf0e7;box-shadow:inset 3px 0 0 #EB6619}\n    .sitem .n{font-weight:600;font-size:13px;margin-bottom:2px}\n    .sitem .m{font-size:11.5px;color:#6b625a;font-family:ui-monospace,Menlo,Consolas,monospace;word-break:break-all}\n    .sitem .t{font-size:11px;color:#8a8078;margin-top:3px}\n    .empty{padding:28px 18px;color:#6b625a;font-size:13px;line-height:1.6}\n    .hd{margin-bottom:16px;padding-bottom:14px;border-bottom:2px solid #EB6619}\n    .hd h2{margin:0 0 8px;font-size:19px}\n    .hd .meta{display:flex;flex-wrap:wrap;gap:6px}\n    .tag{font-size:11px;font-family:ui-monospace,Menlo,Consolas,monospace;padding:4px 9px;border:1px solid #e6e1db;border-radius:999px;color:#6b625a;background:#fff}\n    .msg{margin-bottom:12px;max-width:78%}\n    .msg .who{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;font-weight:700;margin-bottom:4px}\n    .msg .bub{display:inline-block;text-align:left;padding:10px 13px;border-radius:12px;white-space:pre-wrap;word-wrap:break-word;line-height:1.5;font-size:13.5px;\n      font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Segoe UI Emoji','Apple Color Emoji','Noto Color Emoji',Roboto,sans-serif}\n    .msg .tm{font-size:10.5px;color:#8a8078;margin-top:3px;font-family:ui-monospace,Menlo,Consolas,monospace}\n    /* cores do repeater do chatroom (artboard brZydGcsrgomAvTtAUsPy) */\n    .m-HUMAN{margin-left:auto;text-align:right}\n    .m-HUMAN .who{color:#484848;text-align:right}\n    .m-HUMAN .bub{background:#484848;color:#fff;border-radius:10px 2px 10px 10px}\n    .m-HUMAN .tm{text-align:right}\n    .m-AGENT .who{color:#6b625a}\n    .m-AGENT .bub{background:#fff;border:1px solid #e6e1db;border-radius:2px 10px 10px 10px}\n    .m-ASSISTANT .who{color:#6b625a}\n    .m-ASSISTANT .bub{background:#fff;border:1px solid #e6e1db;border-radius:2px 10px 10px 10px}\n    .m-SYSTEM{max-width:100%;text-align:center}\n    .m-SYSTEM .who{display:none}\n    .m-SYSTEM .bub{background:#d4d4d4;color:#1a1714;font-size:12.5px;display:inline-block;text-align:left;border-radius:10px}\n    .btns{margin-top:7px;display:flex;flex-wrap:wrap;gap:6px}\n    .bopt{font-size:11.5px;padding:4px 10px;border:1px solid #c9c1b8;border-radius:999px;background:#fff;color:#6b625a}\n    .clk{font-weight:600}\n    .m-HUMAN .bopt{background:transparent;border-color:rgba(255,255,255,.45);color:#f0ebe4}\n    .ld{padding:30px;text-align:center;color:#6b625a}\n    .info{width:320px;min-width:290px;border-left:1px solid #e6e1db;overflow:auto;background:#fff}\n    .info .tit{padding:12px 14px 0;font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:#8a8078;font-weight:700}\n    .sdet{padding:12px 14px 26px}\n    .sdet section{margin-bottom:15px}\n    .sdet h4{margin:0 0 6px;font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:#EB6619;font-weight:700}\n    .kv{display:flex;gap:8px;font-size:12px;line-height:1.45;padding:3px 0;border-bottom:1px solid #f4f1ed}\n    .kv:last-child{border-bottom:none}\n    .kv b{flex:0 0 92px;font-weight:500;color:#8a8078}\n    .kv span{flex:1;word-break:break-word;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11.5px}\n    .fase{display:flex;align-items:baseline;gap:8px;font-size:12px;padding:5px 0 5px 10px;border-left:2px solid #e6e1db;margin-left:3px}\n    .fase.on{border-left-color:#EB6619;background:#fdf0e7}\n    .fase .nm{flex:0 0 74px;font-weight:600}\n    .fase .hr{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;color:#8a8078}\n    .fase .dr{margin-left:auto;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;color:#EB6619;font-weight:700}\n\n    /* telas estreitas: empilha lista sobre conversa */\n    @media (max-width: 900px){\n      .body{flex-direction:column}\n      .left{width:100%;min-width:0;max-height:32%;border-right:none;border-bottom:1px solid #e6e1db}\n      .info{width:100%;min-width:0;max-height:34%;border-left:none;border-top:1px solid #e6e1db}\n      .right{padding:14px 16px}\n      .msg{max-width:92%}\n      .fld input{min-width:0;width:100%}\n      .fld,.fld.wide{flex:1 1 150px}\n      .hint{margin-left:0;width:100%;text-align:right}\n    }\n    @media (max-width: 520px){\n      .bar{padding:10px 12px;gap:8px}\n      .left{max-height:34%}\n      .msg{max-width:100%}\n      .hd h2{font-size:16px}\n    }\n  `,\n\n  render(d2) {\n    const st = d2.state\n    const ses = d2.data('sessions')\n    // o DataSet nao tem sort: ordena aqui (mais recente primeiro), sem refetch\n    const rows = (ses.records() || []).slice().sort((a, b) => String(b.createdAt || '').localeCompare(String(a.createdAt || '')))\n    const pi = (ses.pageInfo && ses.pageInfo()) || {}\n    const total = (pi.total != null && pi.total > rows.length) ? pi.total : null\n\n    const bar = `\n      <div class=\"bar\">\n        <div class=\"fld wide\"><label>Chat id</label><input class=\"f-id\" value=\"${st.fId || ''}\"></div>\n        <div class=\"fld wide\"><label>Protocolo</label><input class=\"f-prot\" value=\"${st.fProt || ''}\"></div>\n        <div class=\"fld wide\"><label>E-mail</label><input class=\"f-mail\" value=\"${st.fMail || ''}\"></div>\n        <div class=\"fld\"><label>CPF</label><input class=\"f-cpf\" value=\"${st.fCpf || ''}\"></div>\n        <div class=\"fld\"><label>Atendente</label><input class=\"f-ag\" value=\"${st.fAg || ''}\"></div>\n        <div class=\"fld\"><label>Status</label><select class=\"f-st\">${opt(['', 'ai', 'agent', 'queue', 'session_ask', 'survey', 'completed'], st.fSt)}</select></div>\n        <div class=\"fld\"><label>Marca</label><select class=\"f-mc\">${opt(['', 'NATURA', 'AVON', 'MINHALOJA'], st.fMc)}</select></div>\n        <div class=\"fld\"><label>Env</label><select class=\"f-env\">${opt(['', 'PRD', 'HML'], st.fEnv)}</select></div>\n        <div class=\"fld sm\"><label>Atend. ate (s)</label><input class=\"f-at\" type=\"number\" min=\"0\" placeholder=\"60\" value=\"${st.fAt || ''}\"></div>\n        <div class=\"fld\"><label>Data (de)</label><input class=\"f-ini\" type=\"date\" value=\"${st.fIni || ''}\"></div>\n        <div class=\"fld\"><label>Data (ate)</label><input class=\"f-fim\" type=\"date\" value=\"${st.fFim || ''}\"></div>\n        <button class=\"btn b-go\">Buscar</button>\n        <button class=\"btn sec b-clr\">Limpar</button>\n        <span class=\"hint\">${st.busca\n          ? rows.length + ' sessao(oes)'\n            + (total ? ' de ' + total + ' &middot; refine o filtro' : '')\n            + (st.protInfo ? ' &middot; ' + esc(st.protInfo) : '')\n          : 'somente leitura'}</span>\n      </div>`\n\n    const lista = !st.busca\n      ? `<div class=\"empty\">Informe ao menos um filtro e clique <b>Buscar</b>.<br><br>Chat id busca exata.<br><b>Protocolo</b> (32 hex) resolve o chat pelo transbordo &mdash; so existe se a conversa passou por atendente.<br>E-mail e Atendente buscam parcial.<br>Data usa <code>createdAt</code> da sessao.<br>Lista ordenada da mais recente para a mais antiga.</div>`\n      : (ses.loading && !rows.length)\n        ? `<div class=\"ld\">Carregando...</div>`\n        : !rows.length\n          ? `<div class=\"empty\">Nenhuma sessao encontrada.</div>`\n          : rows.map(r => {\n              const ud = r.user_data || {}\n              const nome = [ud.name, ud.lastName].filter(Boolean).join(' ') || r.email || '(sem nome)'\n              return `<div class=\"sitem\" data-key=\"${r._id}\" data-id=\"${r._id}\" data-chat=\"${r.chat_id || ''}\" ${st.selId === r._id ? 'data-sel' : ''}>\n                <div class=\"n\">${esc(nome)}</div>\n                <div class=\"m\">${esc(r._id)}</div>\n                <div class=\"t\">${esc(dd(r.createdAt))} &middot; ${esc(r.marca || '')} &middot; ${esc(r.status || '')}</div>\n              </div>`\n            }).join('')\n\n    const sel = st.selId ? (rows.find(r => r._id === st.selId) || st.selRec || {}) : null\n    let painel = `<div class=\"empty\">Selecione uma sessao a esquerda para ver o historico.</div>`\n    if (sel) {\n      const s = sel\n      const ud = s.user_data || {}\n      const msgs = st.msgs || []\n      const hd = `<div class=\"hd\">\n          <h2>${esc([ud.name, ud.lastName].filter(Boolean).join(' ') || s.email || '(sem nome)')}</h2>\n          <div class=\"meta\">\n            <span class=\"tag\">session ${esc(s._id || '')}</span>\n            <span class=\"tag\">chat ${esc(s.chat_id || '')}</span>\n            ${s.email ? `<span class=\"tag\">${esc(s.email)}</span>` : ''}\n            ${s.cpf ? `<span class=\"tag\">CPF ${esc(s.cpf)}</span>` : ''}\n            ${s.marca ? `<span class=\"tag\">${esc(s.marca)}</span>` : ''}\n            ${s.queue_label ? `<span class=\"tag\">${esc(s.queue_label)}</span>` : ''}\n            <span class=\"tag\">${msgs.length} mensagens</span>\n          </div>\n        </div>`\n      painel = hd + (st.loadingMsgs\n        ? `<div class=\"ld\">Carregando mensagens...</div>`\n        : !msgs.length\n          ? `<div class=\"empty\">Sem mensagens para este chat_id.</div>`\n          : msgs.map(m => bolha(m)).join(''))\n    }\n\n    const info = sel ? `<div class=\"info\"><div class=\"tit\">Dados da sessao</div>${detalhes(sel)}</div>` : ''\n    return `<div class=\"wrap\">${bar}<div class=\"body\"><div class=\"left\">${lista}</div><div class=\"right\">${painel}</div>${info}</div></div>`\n  },\n\n  mounted(d2) {\n    d2.state.busca = false\n    d2.state.msgs = []\n\n    const lerCampos = () => {\n      const g = c => { const el = d2.$('.' + c); return el ? String(el.value).trim() : '' }\n      const s = d2.state\n      s.fId = g('f-id'); s.fProt = g('f-prot'); s.fMail = g('f-mail'); s.fCpf = g('f-cpf')\n      s.fAg = g('f-ag'); s.fSt = g('f-st'); s.fMc = g('f-mc'); s.fEnv = g('f-env')\n      s.fAt = g('f-at'); s.fIni = g('f-ini'); s.fFim = g('f-fim')\n    }\n\n    const buscar = async () => {\n      lerCampos()\n      const st = d2.state\n      const q = {}\n      st.protInfo = ''\n      // Busca por chat_id (nanoid de 21 chars). A operacao nao pesquisa pelo _id do Mongo.\n      if (st.fId) q.chat_id = st.fId\n\n      // Protocolo vive em chat_transfers, nao na sessao: resolve para chat_id antes de filtrar.\n      // 1 protocolo -> 1 conversa; uma conversa pode ter varios protocolos (um por transbordo).\n      // Se vier junto com Chat id, o protocolo manda.\n      if (st.fProt) {\n        const chats = await chatDoProtocolo(d2, st.fProt)\n        if (chats === null) return\n        if (!chats.length) {\n          d2.setState({ busca: true, selId: null, msgs: [], protInfo: 'protocolo nao encontrado' })\n          await d2.data('sessions').filter({ chat_id: '--protocolo-sem-correspondencia--' })\n          return\n        }\n        q.chat_id = chats.length === 1 ? chats[0] : { $in: chats }\n        st.protInfo = chats.length === 1 ? 'protocolo -> chat ' + chats[0] : 'protocolo -> ' + chats.length + ' chats'\n      }\n      if (st.fMail) q.email = { $regex: st.fMail, $options: 'i' }\n      if (st.fCpf) q.cpf = String(st.fCpf).replace(/\\D/g, '')\n      if (st.fAg) q.agent_name = { $regex: st.fAg, $options: 'i' }\n      if (st.fSt) q.status = st.fSt\n      if (st.fMc) q.marca = st.fMc\n      if (st.fEnv) q.env = st.fEnv\n      if (st.fAt !== '' && st.fAt != null && !isNaN(Number(st.fAt))) q.agent_time = { $lte: Number(st.fAt) }\n      if (st.fIni || st.fFim) {\n        q.createdAt = {}\n        if (st.fIni) q.createdAt.$gte = st.fIni + 'T00:00:00.000Z'\n        if (st.fFim) q.createdAt.$lte = st.fFim + 'T23:59:59.999Z'\n      }\n      if (!Object.keys(q).length) { d2.dialog.alert('Informe ao menos um filtro.'); return }\n      d2.state.busca = true\n      d2.state.selId = null\n      d2.state.msgs = []\n      const ses = d2.data('sessions')\n      await ses.filter(q)\n    }\n\n    d2.on('.b-go', 'click', buscar)\n    d2.on('.f-id', 'keydown', e => { if (e.key === 'Enter') buscar() })\n    d2.on('.f-prot', 'keydown', e => { if (e.key === 'Enter') buscar() })\n    d2.on('.f-mail', 'keydown', e => { if (e.key === 'Enter') buscar() })\n\n    d2.on('.b-clr', 'click', () => {\n      d2.setState({\n        fId: '', fProt: '', fMail: '', fCpf: '', fAg: '', fSt: '', fMc: '', fEnv: '', fAt: '',\n        fIni: '', fFim: '', busca: false, selId: null, msgs: [], protInfo: ''\n      })\n    })\n\n    d2.on('.sitem', 'click', async (e, el) => {\n      const id = el.dataset.id, chat = el.dataset.chat\n      const ses = d2.data('sessions')\n      d2.setState({ selId: id, selRec: (ses.records() || []).find(r => r._id === id), loadingMsgs: true, msgs: [] })\n      if (!chat) { d2.setState({ loadingMsgs: false }); return }\n      const msgs = d2.data('messages')\n      await msgs.filter({ chat_id: chat })\n      await msgs.refresh()\n      let all = []\n      try { all = await msgs.loadAll() } catch (err) { all = msgs.records() || [] }\n      all = (all || []).slice().sort((a, b) => (a.index || 0) - (b.index || 0))\n      d2.setState({ msgs: all, loadingMsgs: false })\n    })\n  },\n}\n\n// Protocolo -> chat_id, via chat_transfers (dataset no param `transfers`).\n// Devolve array de chat_ids, [] se nao achou, ou null se o dataset nao esta ligado no bloco.\nasync function chatDoProtocolo(d2, valor) {\n  const prot = String(valor).trim().replace(/\\s+/g, '').toUpperCase()\n  const tr = d2.data('transfers')\n  if (!tr) {\n    d2.dialog.alert('Busca por protocolo indisponivel: o dataset de transbordo nao esta ligado neste bloco.')\n    return null\n  }\n  await tr.filter({ protocol: prot })\n  await tr.refresh()\n  const achados = tr.records() || []\n  return [...new Set(achados.map(t => t.chat_id).filter(Boolean))]\n}\n\nfunction opt(vals, cur) {\n  return vals.map(v => `<option value=\"${esc(v)}\"${String(cur || '') === v ? ' selected' : ''}>${v || 'todos'}</option>`).join('')\n}\n\nfunction esc(s) {\n  return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n\nfunction hh(v) { if (!v) return ''; const t = hbr(v); return t ? t + ' BRT' : '' }\nfunction dd(v) { if (!v) return ''; const t = hbr(v); return t ? dbr(v) + ' ' + t + ' BRT' : String(v).slice(0, 19).replace('T', ' ') }\nfunction dbr(v) {\n  if (!v) return ''\n  const d = new Date(String(v))\n  if (isNaN(d.getTime())) return String(v).slice(0, 10)\n  try {\n    return new Intl.DateTimeFormat('pt-BR', {\n      timeZone: 'America/Sao_Paulo',\n      day: '2-digit', month: '2-digit', year: 'numeric'\n    }).format(d)\n  } catch (e) {\n    return new Date(d.getTime() - 3 * 3600 * 1000).toISOString().slice(0, 10).split('-').reverse().join('/')\n  }\n}\nfunction hbr(v) {\n  if (!v) return ''\n  const s = String(v)\n  const d = new Date(s)\n  if (isNaN(d.getTime())) return s.length >= 19 ? s.slice(11, 19) : s\n  try {\n    return new Intl.DateTimeFormat('pt-BR', {\n      timeZone: 'America/Sao_Paulo',\n      hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false\n    }).format(d)\n  } catch (e) {\n    return new Date(d.getTime() - 3 * 3600 * 1000).toISOString().slice(11, 19)\n  }\n}\nfunction dur(sec) {\n  if (sec == null || sec === '') return ''\n  const n = Number(sec); if (isNaN(n)) return ''\n  if (n < 60) return n + 's'\n  const m = Math.floor(n / 60), r = n % 60\n  if (m < 60) return m + 'm ' + r + 's'\n  return Math.floor(m / 60) + 'h ' + (m % 60) + 'm'\n}\nfunction id(v) { return v == null ? '' : (v._id || v) }\nfunction kv(k, v) { return (v == null || v === '') ? '' : `<div class=\"kv\"><b>${esc(k)}</b><span>${esc(v)}</span></div>` }\n\nfunction detalhes(s) {\n  const ud = s.user_data || {}\n  const nome = [ud.name, ud.lastName].filter(Boolean).join(' ')\n  const fases = [\n    ['Perfil', s.profile_started_at, s.profile_time],\n    ['Identific.', s.identification_started_at, s.identification_time],\n    ['Proativo', s.proactivo_started_at, s.proactivo_time],\n    ['Bot / IA', s.ai_started_at, s.ai_time],\n    ['Fila', s.queue_started_at, s.queue_time],\n    ['Atendente', s.agent_started_at, s.agent_time],\n    ['Pesquisa', s.survey_started_at, s.survey_time],\n  ].filter(f => f[1])\n  const linha = fases.map(f =>\n    `<div class=\"fase${f[0] === 'Atendente' ? ' on' : ''}\"><span class=\"nm\">${f[0]}</span><span class=\"hr\">${hh(f[1])}</span><span class=\"dr\">${dur(f[2])}</span></div>`\n  ).join('') + (s.ended_at ? `<div class=\"fase\"><span class=\"nm\">Encerrada</span><span class=\"hr\">${hh(s.ended_at)}</span><span class=\"dr\"></span></div>` : '')\n\n  const blocos = [\n    ['Linha do tempo', linha ? `<div>${linha}</div>` : ''],\n    ['Atendente', kv('nome', s.agent_name) + kv('matricula', s.agent_internal_number) + kv('agent id', id(s.agent)) + kv('iniciado por', id(s.agent_started_by)) + kv('encerrado por', id(s.agent_ended_by)) + kv('rejeitado em', hh(s.agent_rejected_at))],\n    ['Sessao', kv('status', s.status) + kv('marca', s.marca) + kv('perfil', s.perfil) + kv('env', s.env) + kv('canal', s.device_channel || s.channel) + kv('fila', s.queue_label) + kv('IP', s.ip) + kv('chat_id', s.chat_id) + kv('report', s.session_report) + kv('criada', dd(s.createdAt)) + kv('atualizada', dd(s.updatedAt))],\n    ['Cliente', kv('nome', nome) + kv('e-mail', s.email || ud.email) + kv('CPF', s.cpf || ud.document1) + kv('telefone', ud.phone1 || ud.whatsappNumber) + kv('customerId', ud.customerId) + kv('customerNo', ud.customerNo) + kv('conta desde', String(ud.accountCreatedAt || '').slice(0, 10)) + kv('identificacao', s.identification_method) + kv('consultor', s.consultant_id) + kv('setor', s.sector_code || s.sector)],\n    ['Ultimas mensagens', kv('do cliente', hh(s.last_user_message_at)) + kv('do atendente', hh(s.last_agent_message_at)) + kv('do bot', hh(s.last_assistant_message_at))],\n  ].filter(b => b[1])\n\n  return `<div class=\"sdet\">${blocos.map(b => `<section><h4>${b[0]}</h4>${b[1]}</section>`).join('')}</div>`\n}\n\nfunction bolha(m) {\n  const snd = m.sender || 'SYSTEM'\n  let quem = snd\n  if (snd === 'AGENT') quem = m.agent_name ? 'Atendente: ' + m.agent_name : 'Bot (saudacao automatica)'\n  else if (snd === 'HUMAN') quem = 'Cliente'\n  else if (snd === 'ASSISTANT') quem = 'Bot / Assistente'\n  else if (snd === 'SYSTEM') quem = 'Sistema'\n\n  let corpo = '', extra = ''\n  if (m.type === 'text' && m.text) corpo = m.text.body || ''\n  else if (m.type === 'button_reply' && m.button_reply) {\n    corpo = 'CLICOU: \"' + (m.button_reply.title || m.button_reply.id || '') + '\"'\n    extra = `<div class=\"btns\"><span class=\"bopt clk\">id: ${esc(m.button_reply.id || '')}</span></div>`\n  } else if (m.type === 'interactive' && m.interactive) {\n    corpo = (m.interactive.body && m.interactive.body.text) || '(interativo)'\n    const bs = (m.interactive.action && m.interactive.action.buttons) || []\n    if (bs.length) extra = '<div class=\"btns\">' + bs.map(b => `<span class=\"bopt\">${esc((b.reply && b.reply.title) || '')}</span>`).join('') + '</div>'\n  } else if (m.type === 'image' && m.image) corpo = '[IMAGEM] ' + (m.image.link || '')\n  else corpo = '[' + (m.type || '?') + ']'\n\n  const hora = hbr(m.time)\n  return `<div class=\"msg m-${esc(snd)}\" data-key=\"${esc(m._id || m.index)}\">\n      <div class=\"who\">${esc(quem)}</div>\n      <div class=\"bub\">${esc(corpo)}${extra}</div>\n      <div class=\"tm\" title=\"UTC ${esc(String(m.time || '').slice(11, 19))}\">#${esc(m.index)} &middot; ${esc(hora)} BRT</div>\n    </div>`\n}\n"}},"sessions":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"sessions"}}}},"messages":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"messages"}}}},"transfers":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"transfers"}}}}}}}}}},{"_id":"6a76861ee612e66e181dba68","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"mcp-consulta-ips","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"export default {\n  css: `\n    :host{all:initial}\n    *{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}\n    .wrap{display:flex;flex-direction:column;height:100%;background:#faf9f7;color:#1a1714;font-size:14px}\n    .bar{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;padding:14px 16px;background:#fff;border-bottom:1px solid #e6e1db}\n    .fld{display:flex;flex-direction:column;gap:4px}\n    .fld label{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#6b625a;font-weight:600}\n    .fld input,.fld select{padding:8px 10px;border:1px solid #e6e1db;border-radius:8px;font-size:13px;background:#fff;color:#1a1714;min-width:132px;font-family:inherit}\n    .fld input:focus,.fld select:focus{outline:2px solid #EB6619;outline-offset:-1px;border-color:transparent}\n    .fld.wide input{min-width:200px}\n    .btn{padding:9px 16px;border:none;border-radius:8px;background:#EB6619;color:#fff;font-size:13px;font-weight:600;cursor:pointer}\n    .btn:hover{background:#d25a13}\n    .btn.sec{background:#fff;color:#6b625a;border:1px solid #e6e1db;font-weight:500}\n    .sum{display:flex;gap:18px;flex-wrap:wrap;align-items:center;padding:10px 16px;background:#fdf0e7;border-bottom:1px solid #f5d5bd;font-size:12.5px}\n    .sum b{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:15px;color:#EB6619}\n    .sum .it{display:flex;gap:6px;align-items:baseline}\n    .body{flex:1;overflow:auto;min-height:0}\n    table{border-collapse:collapse;width:100%;font-size:12.5px}\n    thead th{position:sticky;top:0;z-index:1;background:#fff;text-align:left;padding:9px 12px;border-bottom:1px solid #e6e1db;\n      font-size:10px;letter-spacing:.07em;text-transform:uppercase;color:#6b625a;font-weight:700;white-space:nowrap}\n    td{padding:8px 12px;border-bottom:1px solid #f0ece7;vertical-align:top;white-space:nowrap}\n    tbody tr:hover{background:#fdf0e7}\n    .mono{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11.5px}\n    .lnk{color:#EB6619;cursor:pointer;text-decoration:underline;text-underline-offset:2px}\n    .vaz{color:#c9c1b8}\n    .msg{padding:34px 20px;color:#6b625a;line-height:1.7;font-size:13px}\n    .foot{padding:12px 16px;border-top:1px solid #e6e1db;background:#fff;display:flex;gap:12px;align-items:center;font-size:12.5px;color:#6b625a}\n    @media (max-width:900px){\n      .fld,.fld.wide{flex:1 1 140px}\n      .fld input,.fld select{min-width:0;width:100%}\n      td,thead th{white-space:normal}\n    }\n  `,\n\n  render(d2) {\n    const st = d2.state\n    const ds = d2.data('ips')\n    const rows = ds.records() || []\n    const pi = ds.pageInfo ? (ds.pageInfo() || {}) : {}\n\n    const bar = `\n      <div class=\"bar\">\n        <div class=\"fld\"><label>IP</label><input class=\"f-ip\" value=\"${st.fIp || ''}\"></div>\n        <div class=\"fld wide\"><label>Chat id</label><input class=\"f-chat\" value=\"${st.fChat || ''}\"></div>\n        <div class=\"fld wide\"><label>E-mail</label><input class=\"f-mail\" value=\"${st.fMail || ''}\"></div>\n        <div class=\"fld\"><label>CPF</label><input class=\"f-cpf\" value=\"${st.fCpf || ''}\"></div>\n        <div class=\"fld\"><label>Nome</label><input class=\"f-user\" value=\"${st.fUser || ''}\"></div>\n        <div class=\"fld\"><label>Telefone</label><input class=\"f-fone\" value=\"${st.fFone || ''}\"></div>\n        <div class=\"fld\"><label>customer id</label><input class=\"f-cid\" value=\"${st.fCid || ''}\"></div>\n        <div class=\"fld\"><label>customer no</label><input class=\"f-cno\" value=\"${st.fCno || ''}\"></div>\n        <div class=\"fld\"><label>Site</label><select class=\"f-site\">${op(['', 'NATURA', 'AVON', 'MINHA LOJA'], st.fSite)}</select></div>\n        <div class=\"fld\"><label>Env</label><select class=\"f-env\">${op(['', 'PRD', 'HML'], st.fEnv)}</select></div>\n        <div class=\"fld\"><label>Data (de)</label><input class=\"f-ini\" type=\"date\" value=\"${st.fIni || ''}\"></div>\n        <div class=\"fld\"><label>Data (ate)</label><input class=\"f-fim\" type=\"date\" value=\"${st.fFim || ''}\"></div>\n        <button class=\"btn b-go\">Buscar</button>\n        <button class=\"btn sec b-clr\">Limpar</button>\n      </div>`\n\n    if (!st.busca) {\n      return `<div class=\"wrap\">${bar}<div class=\"body\"><div class=\"msg\">\n        Preencha ao menos um filtro e clique <b>Buscar</b>.<br><br>\n        <b>Exatos:</b> IP, chat id, CPF, customer id, customer no, site, env.<br>\n        <b>Parciais:</b> e-mail, nome, telefone.<br>\n        A data usa <code>createdAt</code> do registro de IP.<br><br>\n        Clique num <b>IP</b> ou <b>chat id</b> da tabela para filtrar por ele.\n      </div></div></div>`\n    }\n\n    if (ds.loading && !rows.length) return `<div class=\"wrap\">${bar}<div class=\"body\"><div class=\"msg\">Carregando...</div></div></div>`\n    if (!rows.length) return `<div class=\"wrap\">${bar}<div class=\"body\"><div class=\"msg\">Nenhum registro encontrado.</div></div></div>`\n\n    const uniq = (k) => new Set(rows.map(r => (r[k] || '').trim()).filter(Boolean)).size\n    const sum = `<div class=\"sum\">\n        <span class=\"it\"><b>${rows.length}</b> registros</span>\n        <span class=\"it\"><b>${uniq('ip')}</b> IPs</span>\n        <span class=\"it\"><b>${uniq('chat_id')}</b> chats</span>\n        <span class=\"it\"><b>${uniq('cpf')}</b> CPFs</span>\n        <span class=\"it\"><b>${uniq('email')}</b> e-mails</span>\n      </div>`\n\n    const cel = (v, cls) => v ? `<td class=\"${cls || ''}\">${esc(v)}</td>` : `<td class=\"vaz\">&mdash;</td>`\n    const corpo = rows.map(r => `<tr data-key=\"${esc(r._id)}\">\n        <td class=\"mono\">${esc(String(r.createdAt || '').slice(0, 19).replace('T', ' '))}</td>\n        <td class=\"mono lnk f-por-ip\" data-v=\"${esc(r.ip)}\">${esc(r.ip || '')}</td>\n        <td class=\"mono lnk f-por-chat\" data-v=\"${esc(r.chat_id)}\">${esc(r.chat_id || '')}</td>\n        ${cel(clean(r.user))}\n        ${cel(r.email, 'mono')}\n        ${cel(r.cpf, 'mono')}\n        ${cel(r.phone, 'mono')}\n        ${cel(r.customer_id, 'mono')}\n        ${cel(r.customer_no, 'mono')}\n        ${cel(r.site_id)}\n        ${cel(r.env, 'mono')}\n      </tr>`).join('')\n\n    const tabela = `<table>\n      <thead><tr>\n        <th>Criado (UTC)</th><th>IP</th><th>Chat id</th><th>Nome</th><th>E-mail</th>\n        <th>CPF</th><th>Telefone</th><th>customer id</th><th>customer no</th><th>Site</th><th>Env</th>\n      </tr></thead><tbody>${corpo}</tbody></table>`\n\n    const total = pi.total\n    const temMais = total != null && rows.length < total\n    const foot = `<div class=\"foot\">\n        <span>Exibindo ${rows.length}${total != null ? ' de ' + total : ''}</span>\n        ${temMais ? `<button class=\"btn sec b-mais\">Carregar mais</button>` : ''}\n        ${st.carregando ? '<span>carregando...</span>' : ''}\n      </div>`\n\n    return `<div class=\"wrap\">${bar}${sum}<div class=\"body\">${tabela}</div>${foot}</div>`\n  },\n\n  mounted(d2) {\n    d2.state.busca = false\n\n    const ler = () => {\n      const g = c => { const el = d2.$('.' + c); return el ? String(el.value).trim() : '' }\n      const s = d2.state\n      s.fIp = g('f-ip'); s.fChat = g('f-chat'); s.fMail = g('f-mail'); s.fCpf = g('f-cpf')\n      s.fUser = g('f-user'); s.fFone = g('f-fone'); s.fCid = g('f-cid'); s.fCno = g('f-cno')\n      s.fSite = g('f-site'); s.fEnv = g('f-env'); s.fIni = g('f-ini'); s.fFim = g('f-fim')\n    }\n\n    const montarQuery = () => {\n      const s = d2.state, q = {}\n      const rx = v => ({ $regex: v, $options: 'i' })\n      if (s.fIp) q.ip = s.fIp\n      if (s.fChat) q.chat_id = s.fChat\n      if (s.fMail) q.email = rx(s.fMail)\n      if (s.fCpf) q.cpf = String(s.fCpf).replace(/\\D/g, '')\n      if (s.fUser) q.user = rx(s.fUser)\n      if (s.fFone) q.phone = rx(String(s.fFone).replace(/\\D/g, ''))\n      if (s.fCid) q.customer_id = s.fCid\n      if (s.fCno) q.customer_no = s.fCno\n      if (s.fSite) q.site_id = { $regex: '^' + s.fSite + '$', $options: 'i' }\n      if (s.fEnv) q.env = s.fEnv\n      if (s.fIni || s.fFim) {\n        q.createdAt = {}\n        if (s.fIni) q.createdAt.$gte = s.fIni + 'T00:00:00.000Z'\n        if (s.fFim) q.createdAt.$lte = s.fFim + 'T23:59:59.999Z'\n      }\n      return q\n    }\n\n    const buscar = async () => {\n      ler()\n      const q = montarQuery()\n      if (!Object.keys(q).length) { d2.dialog.alert('Preencha ao menos um filtro.'); return }\n      d2.state.busca = true\n      const ds = d2.data('ips')\n      await ds.filter(q)\n    }\n\n    d2.on('.b-go', 'click', buscar)\n    for (const c of ['f-ip', 'f-chat', 'f-mail', 'f-cpf', 'f-user', 'f-fone', 'f-cid', 'f-cno']) {\n      d2.on('.' + c, 'keydown', e => { if (e.key === 'Enter') buscar() })\n    }\n\n    d2.on('.b-clr', 'click', () => {\n      d2.setState({\n        fIp: '', fChat: '', fMail: '', fCpf: '', fUser: '', fFone: '', fCid: '', fCno: '',\n        fSite: '', fEnv: '', fIni: '', fFim: '', busca: false\n      })\n    })\n\n    d2.on('.f-por-ip', 'click', async (e, el) => {\n      if (!el.dataset.v) return\n      d2.setState({ fIp: el.dataset.v, fChat: '' })\n      const ds = d2.data('ips')\n      await ds.refresh(); await ds.filter(montarQuery())\n    })\n\n    d2.on('.f-por-chat', 'click', async (e, el) => {\n      if (!el.dataset.v) return\n      d2.setState({ fChat: el.dataset.v, fIp: '' })\n      const ds = d2.data('ips')\n      await ds.refresh(); await ds.filter(montarQuery())\n    })\n\n    d2.on('.b-mais', 'click', async () => {\n      d2.state.carregando = true\n      try { await d2.data('ips').appendNext() } catch (err) { }\n      d2.state.carregando = false\n    })\n  },\n}\n\nfunction esc(s) {\n  return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\nfunction clean(v) { const s = String(v == null ? '' : v).trim(); return s }\nfunction op(vals, cur) {\n  return vals.map(v => `<option value=\"${esc(v)}\"${String(cur || '') === v ? ' selected' : ''}>${v || 'todos'}</option>`).join('')\n}\n"}},"ips":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"ips"}}}}}}}}}},{"_id":"6a873d1037012648c6c11501","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"MBXFuqZdNSJ3xQqwshzHm","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\\"/g, '&quot;') }\n\nfunction fmtDur(sec) {\n  sec = Math.max(0, Math.floor(sec))\n  const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60\n  if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m`\n  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\n\nconst STATUS_META = {\n  atendendo:       { label: 'Atendendo',        color: '#0a7d0a', dot: '#0a7d0a' },\n  disponivel:      { label: 'Disponível',       color: '#0ca30c', dot: '#0ca30c' },\n  aguardandoPausa: { label: 'Aguardando Pausa', color: '#a67c00', dot: '#fab219' },\n  pausa:           { label: 'Pausa',            color: '#a67c00', dot: '#fab219' },\n  indisponivel:    { label: 'Indisponível',      color: '#898781', dot: '#898781' },\n}\n\nfunction classify(a) {\n  if (a.status_type === 'online' && a.in_service) return 'atendendo'\n  if (a.status_type === 'online') return 'disponivel'\n  if (a.status_type === 'pause' && a.in_service) return 'aguardandoPausa'\n  if (a.status_type === 'pause') return 'pausa'\n  return 'indisponivel'\n}\n\nexport default {\n  css: `\n    .wrap { font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif; display:flex; flex-direction:column; height:100%; box-sizing:border-box; background:#ffffff; border:1px solid rgba(0,0,0,0.08); border-radius:12px; overflow:hidden; }\n    .hd { display:flex; align-items:center; gap:16px; padding:18px 24px; border-bottom:1px solid rgba(0,0,0,0.08); flex:0 0 auto; }\n    .hd h2 { margin:0; font-size:17px; font-weight:700; color:#1a1a1a; flex:0 0 auto; }\n    .chk { display:flex; align-items:center; gap:6px; font-size:12.5px; color:#52514e; flex:0 0 auto; white-space:nowrap; }\n    .search { flex:0 0 220px; margin-left:auto; font-size:12.5px; padding:6px 10px; border-radius:8px; border:1px solid rgba(0,0,0,0.12); }\n    .kpirow { display:grid; grid-template-columns:repeat(7,1fr); gap:1px; background:rgba(0,0,0,0.06); flex:0 0 auto; }\n    .kpi { background:#fff; padding:12px 10px; text-align:center; }\n    .kpi-val { font-size:20px; font-weight:700; color:#1a1a1a; }\n    .kpi-label { font-size:10.5px; color:#898781; text-transform:uppercase; letter-spacing:.02em; margin-top:2px; }\n    .listhd { display:grid; grid-template-columns: 1fr 70px 60px 150px 64px; gap:10px; padding:8px 20px; font-size:10.5px; color:#898781; text-transform:uppercase; letter-spacing:.02em; border-bottom:1px solid rgba(0,0,0,0.06); flex:0 0 auto; }\n    .list { flex:1 1 auto; overflow-y:auto; padding:4px 8px 16px; }\n    .row-wrap { border-radius:8px; }\n    .row { display:grid; grid-template-columns: 20px 1fr 70px 60px 150px 64px; align-items:center; gap:10px; padding:9px 8px; border-radius:8px; cursor:pointer; }\n    .row:hover { background:#f7f7f5; }\n    .caret { font-size:11px; color:#898781; text-align:center; }\n    .caret.hidden { visibility:hidden; }\n    .info { min-width:0; }\n    .name { font-size:13px; font-weight:600; color:#1a1a1a; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n    .sub { font-size:11.5px; color:#898781; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n    .slots, .total { font-size:12.5px; color:#52514e; text-align:center; font-variant-numeric:tabular-nums; }\n    .pill { display:flex; align-items:center; gap:6px; padding:4px 10px; border-radius:999px; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.02em; justify-content:center; }\n    .dot { width:7px; height:7px; border-radius:50%; display:inline-block; flex:0 0 auto; }\n    .elapsed { font-size:12px; color:#52514e; text-align:right; font-variant-numeric:tabular-nums; }\n    .chats { padding:2px 20px 10px 46px; }\n    .chat-row { display:flex; justify-content:space-between; font-size:11.5px; color:#52514e; padding:4px 8px; background:#f7f7f5; border-radius:6px; margin-top:4px; font-variant-numeric:tabular-nums; }\n    .chat-row.empty-sub { color:#898781; justify-content:flex-start; }\n    .empty, .loading { padding:24px; text-align:center; color:#898781; font-size:13px; }\n  `,\n  render(d2) {\n    const snap = d2.data('agentsSnapshot')\n    const chats = d2.data('activeChats')\n    if (!snap || !chats) return `<div class=\"wrap\"><div class=\"empty\">Fontes de dado não conectadas.</div></div>`\n    if (snap.loading || chats.loading) return `<div class=\"wrap\"><div class=\"loading\">Carregando agentes…</div></div>`\n\n    const now = d2.state.now || Date.now()\n    const showOffline = !!d2.state.showOffline\n    const search = (d2.state.search || '').trim().toLowerCase()\n    const expanded = d2.state.expanded || {}\n\n    const agentRows = snap.records()\n    const chatRows = chats.records()\n\n    const chatsByName = new Map()\n    for (const c of chatRows) {\n      const key = String(c.agent_name || '').trim().toLowerCase()\n      if (!key) continue\n      if (!chatsByName.has(key)) chatsByName.set(key, [])\n      chatsByName.get(key).push(c)\n    }\n\n    const agents = agentRows.map(r => {\n      const email = (r.agent && r.agent.email) || ''\n      const name = (r.agent && r.agent.name) || email || 'Agente'\n      const cls = classify(r)\n      const meta = STATUS_META[cls]\n      const startMs = (cls === 'pausa' || cls === 'aguardandoPausa')\n        ? (r.last_pause_at ? new Date(r.last_pause_at).getTime() : now)\n        : (r.last_transfer_at ? new Date(r.last_transfer_at).getTime() : now)\n      const elapsedSec = Math.max(0, (now - startMs) / 1000)\n      const myChats = chatsByName.get(name.trim().toLowerCase()) || []\n      return { key: email || name, name, email, cls, meta, elapsedSec, transfersCount: r.transfers_count || 0, chats: myChats }\n    })\n\n    const counts = { total: agents.length, conectados: 0, atendendo: 0, aguardando: 0, pausa: 0, inativos: 0 }\n    for (const a of agents) {\n      if (a.cls === 'indisponivel') { counts.inativos++; continue }\n      counts.conectados++\n      if (a.cls === 'atendendo') counts.atendendo++\n      else if (a.cls === 'disponivel') counts.aguardando++\n      else if (a.cls === 'pausa') counts.pausa++\n    }\n    const slotsCapacity = counts.conectados * 2\n    const slotsOccupied = chatRows.length\n    const slotsFree = Math.max(0, slotsCapacity - slotsOccupied)\n\n    let visible = agents.filter(a => showOffline || a.cls !== 'indisponivel')\n    if (search) visible = visible.filter(a => a.name.toLowerCase().includes(search) || a.email.toLowerCase().includes(search))\n    const order = { atendendo: 0, disponivel: 1, aguardandoPausa: 2, pausa: 3, indisponivel: 4 }\n    visible.sort((a, b) => (order[a.cls] - order[b.cls]) || (b.elapsedSec - a.elapsedSec))\n\n    const kpis = [\n      ['Total de agentes', counts.total],\n      ['Conectados', counts.conectados],\n      ['Em atendimento', counts.atendendo],\n      ['Aguardando', counts.aguardando],\n      ['Em pausa', counts.pausa],\n      ['Inativos', counts.inativos],\n      ['Janelas disponíveis', slotsFree],\n    ]\n    const kpiHtml = kpis.map(([label, val]) => `<div class=\"kpi\"><div class=\"kpi-val\">${val}</div><div class=\"kpi-label\">${esc(label)}</div></div>`).join('')\n\n    const rowsHtml = visible.length === 0\n      ? `<div class=\"empty\">Nenhum agente encontrado.</div>`\n      : visible.map(a => {\n        const isOpen = !!expanded[a.key]\n        const hasChats = a.chats.length > 0\n        const chatsHtml = hasChats\n          ? a.chats.map(c => `\n            <div class=\"chat-row\">\n              <span>${esc(c.chat_id || c.protocol || '—')}</span>\n              <span>${fmtDur((now - new Date(c.agent_joined_at).getTime()) / 1000)}</span>\n            </div>`).join('')\n          : `<div class=\"chat-row empty-sub\">Sem chats ativos agora</div>`\n        return `\n        <div class=\"row-wrap\">\n          <div class=\"row\" data-key=\"${esc(a.key)}\">\n            <span class=\"caret ${hasChats ? '' : 'hidden'}\">${isOpen ? '▾' : '▸'}</span>\n            <div class=\"info\">\n              <div class=\"name\">${esc(a.name)}</div>\n              <div class=\"sub\">${esc(a.email)}</div>\n            </div>\n            <div class=\"slots\">${a.chats.length}/2</div>\n            <div class=\"total\">${a.transfersCount}</div>\n            <div class=\"pill\" style=\"background:${a.meta.color}1a; color:${a.meta.color};\"><span class=\"dot\" style=\"background:${a.meta.dot}\"></span>${esc(a.meta.label)}</div>\n            <div class=\"elapsed\">${fmtDur(a.elapsedSec)}</div>\n          </div>\n          ${isOpen ? `<div class=\"chats\">${chatsHtml}</div>` : ''}\n        </div>`\n      }).join('')\n\n    return `<div class=\"wrap\">\n      <div class=\"hd\">\n        <h2>Agentes — agora</h2>\n        <label class=\"chk\"><input type=\"checkbox\" class=\"toggle-offline\" ${showOffline ? 'checked' : ''}> Ver inativos</label>\n        <input class=\"search\" type=\"text\" placeholder=\"Buscar agente…\" value=\"${esc(d2.state.search || '')}\">\n      </div>\n      <div class=\"kpirow\">${kpiHtml}</div>\n      <div class=\"listhd\"><span>Agente</span><span>Janelas</span><span>Total</span><span>Status</span><span>Tempo</span></div>\n      <div class=\"list\">${rowsHtml}</div>\n    </div>`\n  },\n  mounted(d2, root) {\n    d2.data('agentsSnapshot').sort('last_transfer_at', 'desc')\n    d2.data('activeChats')\n\n    d2.on('.toggle-offline', 'change', (e, el) => { d2.state.showOffline = el.checked })\n    d2.on('.search', 'input', (e, el) => { d2.state.search = el.value })\n    d2.on('.row', 'click', (e, el) => {\n      const key = el.dataset.key\n      const cur = d2.state.expanded || {}\n      d2.state.expanded = { ...cur, [key]: !cur[key] }\n    })\n\n    const tickId = setInterval(() => { d2.state.now = Date.now() }, 1000)\n    const refreshId = setInterval(() => {\n      d2.data('agentsSnapshot').refresh()\n      d2.data('activeChats').refresh()\n    }, 20000)\n    d2.onUnmount(() => { clearInterval(tickId); clearInterval(refreshId) })\n  },\n}"}},"agentsSnapshot":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentsSnapshot"}}}},"activeChats":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"activeChats"}}}}}}}}}},{"_id":"6a873d750835788f6ed8adde","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"Fc0OgkhXW3HEd9hygYQ8k","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;') }    const STATUS = {\n      online:  { label: 'Online',  color: '#0ca30c' },\n      busy:    { label: 'Busy (in service)', color: '#0a7d0a' },\n      pause:   { label: 'Pause',   color: '#fab219' },\n      offline: { label: 'Offline', color: '#898781' },\n      none:    { label: 'No data', color: '#e1e0d9' },\n    }\n\nfunction fmtHM(ms) {\n  const d = new Date(ms)\n  return `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}`\n}\nfunction fmtDur(sec) {\n  sec = Math.max(0, Math.round(sec))\n  const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60)\n  if (h > 0) return `${h}h ${String(m).padStart(2, '0')}m`\n  return `${m}m`\n}\nfunction todayLocalDateStr() {\n  const localMs = Date.now() - 3 * 3600000 // BRT = fixed UTC-3\n  const d = new Date(localMs)\n  return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`\n}\nfunction localDateToUtcMs(dateStr) {\n  const [y, m, d] = dateStr.split('-').map(Number)\n  return Date.UTC(y, m - 1, d, 3, 0, 0, 0) // local 00:00 BRT\n}\nfunction localRangeUtcMs(fromDateStr, toDateStr) {\n  const fromMs = localDateToUtcMs(fromDateStr)\n  const toMs = localDateToUtcMs(toDateStr) + 86400000\n  return { fromMs, toMs }\n}\nfunction splitBusy(segs, windows) {\n  if (!windows || !windows.length) return segs\n  const sorted = windows.slice().sort((a, b) => a.start - b.start)\n  const out = []\n  for (const s of segs) {\n    if (s.type !== 'online') { out.push(s); continue }\n    const clipped = sorted\n      .map(w => ({ start: Math.max(w.start, s.start), end: Math.min(w.end, s.end) }))\n      .filter(w => w.end > w.start)\n    if (!clipped.length) { out.push(s); continue }\n    let cursor = s.start\n    for (const w of clipped) {\n      if (w.start > cursor) out.push({ ...s, type: 'online', start: cursor, end: w.start })\n      out.push({ ...s, type: 'busy', title: 'Atendimento', start: Math.max(w.start, cursor), end: w.end })\n      cursor = Math.max(cursor, w.end)\n    }\n    if (cursor < s.end) out.push({ ...s, type: 'online', start: cursor, end: s.end })\n  }\n  return out\n}\nfunction buildAgentTimelines(rows, fromMs, toMs, nowMs, transfersByName) {\n  const byAgent = new Map()\n  for (const r of rows) {\n    const key = (r.agent && (r.agent.email || r.agent.name || r.agent._id)) || 'unknown'\n    const name = (r.agent && (r.agent.name || r.agent.email)) || 'Unknown agent'\n    if (!byAgent.has(key)) byAgent.set(key, { key, name, recs: [] })\n    byAgent.get(key).recs.push(r)\n  }\n  const timelines = []\n  for (const agent of byAgent.values()) {\n    agent.recs.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())\n    const rawSegs = []\n    for (let i = 0; i < agent.recs.length; i++) {\n      const r = agent.recs[i]\n      const start = new Date(r.createdAt).getTime()\n      const next = agent.recs[i + 1]\n      const end = next ? new Date(next.createdAt).getTime() : (r.finished ? start + (r.duration || 0) * 1000 : nowMs)\n      const clipStart = Math.max(start, fromMs)\n      const clipEnd = Math.min(end, toMs)\n      if (clipEnd > clipStart) rawSegs.push({ type: (r.status && r.status.type) || 'offline', title: (r.status && r.status.title) || '', start: clipStart, end: clipEnd })\n    }\n    const windows = transfersByName ? (transfersByName.get(agent.name.trim().toLowerCase()) || []) : []\n    const segs = splitBusy(rawSegs, windows)\n    if (segs.length && segs[0].start > fromMs) segs.unshift({ type: 'none', title: 'No data', start: fromMs, end: segs[0].start })\n    if (!segs.length) segs.push({ type: 'none', title: 'No data', start: fromMs, end: toMs })\n    timelines.push({ key: agent.key, name: agent.name, segs })\n  }\n  timelines.sort((a, b) => a.name.localeCompare(b.name))\n  return timelines\n}\nfunction hourlyCounts(timelines, fromMs) {\n  const buckets = Array.from({ length: 24 }, (_, h) => ({ hour: h, online: 0, pause: 0, offline: 0 }))\n  for (const t of timelines) {\n    for (let h = 0; h < 24; h++) {\n      const mid = fromMs + h * 3600000 + 1800000\n      const seg = t.segs.find(s => mid >= s.start && mid < s.end)\n      if (seg && buckets[h][seg.type] !== undefined) buckets[h][seg.type]++\n    }\n  }\n  return buckets\n}\nfunction computeKpis(timelines) {\n  let totalAgents = 0, changes = 0, onlineSecs = 0, onlineAgents = 0\n  for (const t of timelines) {\n    const real = t.segs.filter(s => s.type !== 'none')\n    if (real.length) totalAgents++\n    changes += real.length\n    const on = real.filter(s => s.type === 'online' || s.type === 'busy').reduce((a, s) => a + (s.end - s.start), 0) / 1000\n    if (on > 0) { onlineSecs += on; onlineAgents++ }\n  }\n  return { totalAgents, changes, avgOnlineSec: onlineAgents ? onlineSecs / onlineAgents : 0 }\n}\n\nexport default {\n  css: `\n    .wrap { font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif; display:flex; flex-direction:column; height:100%; box-sizing:border-box; background:#ffffff; border:1px solid rgba(0,0,0,0.08); border-radius:12px; overflow:hidden; }\n    .hd { display:flex; align-items:center; gap:18px; flex-wrap:wrap; padding:20px 24px; border-bottom:1px solid rgba(0,0,0,0.08); flex:0 0 auto; row-gap:14px; }\n    .hd h2 { margin:0; font-size:18px; font-weight:700; color:#1a1a1a; margin-right:auto; flex:1 1 100%; }\n    .ctl { display:flex; align-items:center; gap:8px; }\n    .ctl label { font-size:11.5px; color:#898781; text-transform:uppercase; letter-spacing:.03em; }\n    input[type=date], select { font-size:13px; padding:6px 10px; border-radius:8px; border:1px solid rgba(0,0,0,0.15); background:#fff; color:#232323; }\n    .toggle { display:flex; background:#f0efec; border-radius:8px; padding:3px; }\n    .toggle button { border:none; background:transparent; padding:6px 14px; font-size:12.5px; font-weight:600; color:#52514e; border-radius:6px; cursor:pointer; }\n    .toggle button.active { background:#ffffff; color:#1a1a1a; box-shadow:0 1px 2px rgba(0,0,0,0.12); }\n    .clearBtn { border:1px solid rgba(229,0,75,0.3); background:rgba(229,0,75,0.06); color:#e5004b; font-size:12px; font-weight:600; padding:6px 12px; border-radius:8px; cursor:pointer; }\n    .kpis { display:flex; gap:24px; padding:14px 20px; border-bottom:1px solid rgba(0,0,0,0.06); flex:0 0 auto; }\n    .kpi .v { font-size:20px; font-weight:700; color:#1a1a1a; font-variant-numeric:tabular-nums; }\n    .kpi .l { font-size:11px; color:#898781; text-transform:uppercase; letter-spacing:.03em; }\n    .body { flex:1 1 auto; overflow-y:auto; padding:16px 20px; }\n    .legend { display:flex; gap:16px; margin-bottom:12px; font-size:12px; color:#52514e; }\n    .legend span.dot { width:8px; height:8px; border-radius:50%; display:inline-block; margin-right:5px; }\n    .trow { display:flex; align-items:center; gap:12px; margin-bottom:8px; }\n    .trow .tname { width:160px; flex:0 0 auto; font-size:12.5px; font-weight:600; color:#232323; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:pointer; }\n    .tbar { position:relative; flex:1 1 auto; height:22px; background:#f0efec; border-radius:6px; overflow:hidden; }\n    .tbar.big { height:40px; }\n    .seg { position:absolute; top:0; bottom:0; }\n    .detail { margin-top:16px; border-top:1px solid rgba(0,0,0,0.06); padding-top:12px; }\n    .detail table { width:100%; border-collapse:collapse; font-size:12.5px; }\n    .detail th { text-align:left; color:#898781; font-weight:600; font-size:11px; text-transform:uppercase; padding:4px 8px; }\n    .detail td { padding:6px 8px; border-top:1px solid rgba(0,0,0,0.05); color:#232323; }\n    .hourly { display:flex; align-items:flex-end; gap:4px; height:220px; padding-top:8px; }\n    .hcol { flex:1 1 auto; display:flex; flex-direction:column; align-items:stretch; border-radius:3px 3px 0 0; overflow:hidden; }\n    .hcol .seg2 { width:100%; }\n    .hlabels { display:flex; gap:4px; margin-top:6px; }\n    .hlabels span { flex:1 1 auto; text-align:center; font-size:10px; color:#898781; }\n    .empty { padding:32px; text-align:center; color:#898781; font-size:13px; }\n  `,\n  render(d2) {\n    const fromDate = d2.state.fromDate || todayLocalDateStr()\n    const toDate = d2.state.toDate || fromDate\n    const view = d2.state.view || 'timeline'\n    const sharedFocus = d2.shared.get('opsFocusAgent', null, { global: true })\n    const focusKey = d2.state.focusOverride !== undefined ? d2.state.focusOverride : (sharedFocus ? sharedFocus.key : '')\n    const timelines = d2.state.timelines || []\n    const range = d2.state.range\n    const kpis = d2.state.kpis || { totalAgents: 0, changes: 0, avgOnlineSec: 0 }\n    const loading = d2.state.periodLoading\n\n    const focusedTimeline = focusKey ? timelines.find(t => t.key === focusKey) : null\n    const agentOptions = timelines.slice().sort((a, b) => a.name.localeCompare(b.name))\n      .map(t => `<option value=\"${esc(t.key)}\" ${t.key === focusKey ? 'selected' : ''}>${esc(t.name)}</option>`).join('')\n    const legend = `<div class=\"legend\">${['online', 'busy', 'pause', 'offline'].map(k => `<span><span class=\"dot\" style=\"background:${STATUS[k].color}\"></span>${STATUS[k].label}</span>`).join('')}</div>`\n    const segStyle = (s, fromMs, spanMs) => {\n      const left = ((s.start - fromMs) / spanMs) * 100\n      const width = Math.max(0.4, ((s.end - s.start) / spanMs) * 100)\n      return `left:${left}%; width:${width}%; background:${STATUS[s.type].color};`\n    }\n\n    const periodError = d2.state.periodError\n    let bodyHtml = ''\n    if (loading) {\n      bodyHtml = `<div class=\"empty\">Loading period…</div>`\n    } else if (periodError) {\n      bodyHtml = `<div class=\"empty\">Could not load this period — ${esc(periodError)}</div>`\n    } else if (!range) {\n      bodyHtml = `<div class=\"empty\">Pick a date to load the period.</div>`\n    } else if (view === 'timeline') {\n      const rowsToShow = focusedTimeline ? [focusedTimeline] : timelines\n      if (!rowsToShow.length) {\n        bodyHtml = `<div class=\"empty\">No status data for this period.</div>`\n      } else {\n        const spanMs = range.toMs - range.fromMs\n        bodyHtml = legend + rowsToShow.map(t => `\n          <div class=\"trow\">\n            <div class=\"tname\" data-agentname=\"${esc(t.key)}\" title=\"Focus this agent\">${esc(t.name)}</div>\n            <div class=\"tbar ${focusedTimeline ? 'big' : ''}\">\n              ${t.segs.map(s => `<div class=\"seg\" style=\"${segStyle(s, range.fromMs, spanMs)}\" title=\"${esc(t.name)} — ${STATUS[s.type].label}${s.title && s.title !== STATUS[s.type].label ? ' (' + esc(s.title) + ')' : ''} · ${fmtHM(s.start)}–${fmtHM(s.end)}\"></div>`).join('')}\n            </div>\n          </div>\n        `).join('')\n        if (focusedTimeline) {\n          bodyHtml += `<div class=\"detail\"><table><thead><tr><th>Status</th><th>From</th><th>To</th><th>Duration</th></tr></thead><tbody>\n            ${focusedTimeline.segs.filter(s => s.type !== 'none').map(s => `<tr>\n              <td>${STATUS[s.type].label}${s.title && s.title !== STATUS[s.type].label ? ' — ' + esc(s.title) : ''}</td>\n              <td>${fmtHM(s.start)}</td><td>${fmtHM(s.end)}</td><td>${fmtDur((s.end - s.start) / 1000)}</td>\n            </tr>`).join('')}\n          </tbody></table></div>`\n        }\n      }\n    } else {\n      const buckets = d2.state.hourly || []\n      const maxCount = Math.max(1, ...buckets.map(b => b.online + b.pause + b.offline))\n      bodyHtml = legend + `<div class=\"hourly\">\n        ${buckets.map(b => {\n          const total = b.online + b.pause + b.offline\n          const h = total ? (total / maxCount) * 100 : 0\n          const parts = ['online', 'pause', 'offline'].filter(k => b[k] > 0).map(k =>\n            `<div class=\"seg2\" style=\"height:${(b[k] / (total || 1)) * 100}%; background:${STATUS[k].color};\" title=\"${String(b.hour).padStart(2, '0')}:00 — ${STATUS[k].label}: ${b[k]}\"></div>`\n          ).join('')\n          return `<div class=\"hcol\" style=\"height:${h}%;\">${parts}</div>`\n        }).join('')}\n      </div>\n      <div class=\"hlabels\">${buckets.map(b => `<span>${b.hour % 3 === 0 ? String(b.hour).padStart(2, '0') + 'h' : ''}</span>`).join('')}</div>`\n    }\n\n    return `<div class=\"wrap\">\n      <div class=\"hd\">\n        <h2>Period explorer</h2>\n        <div class=\"ctl\"><label>From</label><input type=\"date\" class=\"date-from\" value=\"${fromDate}\"></div>\n        <div class=\"ctl\"><label>To</label><input type=\"date\" class=\"date-to\" value=\"${toDate}\"></div>\n        <div class=\"ctl\"><label>Agent</label><select class=\"agent-select\"><option value=\"\">All agents</option>${agentOptions}</select></div>\n        <div class=\"toggle\">\n          <button class=\"v-timeline ${view === 'timeline' ? 'active' : ''}\">Timeline</button>\n          <button class=\"v-hourly ${view === 'hourly' ? 'active' : ''}\">Hourly</button>\n        </div>\n        ${focusKey ? `<button class=\"clearBtn\">Clear focus</button>` : ``}\n      </div>\n      <div class=\"kpis\">\n        <div class=\"kpi\"><div class=\"v\">${kpis.totalAgents}</div><div class=\"l\">Agents active</div></div>\n        <div class=\"kpi\"><div class=\"v\">${kpis.changes}</div><div class=\"l\">Status changes</div></div>\n        <div class=\"kpi\"><div class=\"v\">${fmtDur(kpis.avgOnlineSec)}</div><div class=\"l\">Avg. online / agent</div></div>\n      </div>\n      <div class=\"body\">${bodyHtml}</div>\n    </div>`\n  },\n  mounted(d2, root) {\n    const load = async (fromDateStr, toDateStr) => {\n      d2.state.periodLoading = true\n      d2.state.periodError = null\n      const { fromMs, toMs } = localRangeUtcMs(fromDateStr, toDateStr)\n      try {\n        const ds = d2.data('statusLog')\n        ds.filter({ createdAt: { $gte: new Date(fromMs - 86400000).toISOString(), $lte: new Date(toMs).toISOString() } })\n        await ds.refresh()\n        const rawRows = await ds.loadAll()\n        const rows = Array.isArray(rawRows) ? rawRows : (rawRows && Array.isArray(rawRows.records) ? rawRows.records : [])\n        const nowMs = Date.now()\n\n        // Human-attendance cross-reference (optional — a failure here must not\n        // break the status timeline). \"Human attendance\" = a transfer row where\n        // a human agent actually joined (agent_joined_at present).\n        const transfersByName = new Map()\n        try {\n          const tds = d2.data('transfers')\n          tds.filter({ agent_joined_at: { $gte: new Date(fromMs - 86400000).toISOString(), $lte: new Date(toMs).toISOString() } })\n          await tds.refresh()\n          const rawT = await tds.loadAll()\n          const transferRows = Array.isArray(rawT) ? rawT : (rawT && Array.isArray(rawT.records) ? rawT.records : [])\n          for (const t of transferRows) {\n            if (!t.agent_joined_at || !t.agent_name) continue\n            const key = String(t.agent_name).trim().toLowerCase()\n            const win = { start: new Date(t.agent_joined_at).getTime(), end: t.ended_at ? new Date(t.ended_at).getTime() : nowMs }\n            if (!transfersByName.has(key)) transfersByName.set(key, [])\n            transfersByName.get(key).push(win)\n          }\n        } catch (te) { /* transfers optional — timeline still works without the busy/free split */ }\n\n        const timelines = buildAgentTimelines(rows, fromMs, toMs, nowMs, transfersByName)\n        d2.state.timelines = timelines\n        d2.state.range = { fromMs, toMs }\n        d2.state.hourly = hourlyCounts(timelines, fromMs)\n        d2.state.kpis = computeKpis(timelines)\n      } catch (err) {\n        // Never leave the UI stuck on \"Loading period…\" — surface the failure\n        // (e.g. an auth-gated table with no session) as a visible, dismissable state.\n        d2.state.timelines = []\n        d2.state.range = { fromMs, toMs }\n        d2.state.hourly = []\n        d2.state.kpis = { totalAgents: 0, changes: 0, avgOnlineSec: 0 }\n        d2.state.periodError = (err && err.message) ? err.message : 'Could not load this period.'\n      }\n      d2.state.periodLoading = false\n    }\n    d2.state.fromDate = d2.state.fromDate || todayLocalDateStr()\n    d2.state.toDate = d2.state.toDate || d2.state.fromDate\n    load(d2.state.fromDate, d2.state.toDate)\n\n    d2.on('.date-from', 'change', (e, el) => { d2.state.fromDate = el.value; if (d2.state.toDate < el.value) d2.state.toDate = el.value; load(d2.state.fromDate, d2.state.toDate) })\n    d2.on('.date-to', 'change', (e, el) => { d2.state.toDate = el.value; load(d2.state.fromDate, d2.state.toDate) })\n    d2.on('.agent-select', 'change', (e, el) => {\n      d2.state.focusOverride = el.value\n      const t = (d2.state.timelines || []).find(x => x.key === el.value)\n      d2.shared.set('opsFocusAgent', t ? { key: t.key, label: t.name } : null, { global: true })\n    })\n    d2.on('.v-timeline', 'click', () => { d2.state.view = 'timeline' })\n    d2.on('.v-hourly', 'click', () => { d2.state.view = 'hourly' })\n    d2.on('.clearBtn', 'click', () => { d2.state.focusOverride = ''; d2.shared.set('opsFocusAgent', null, { global: true }) })\n    d2.on('.tname', 'click', (e, el) => {\n      const key = el.dataset.agentname\n      d2.state.focusOverride = key\n      const t = (d2.state.timelines || []).find(x => x.key === key)\n      d2.shared.set('opsFocusAgent', t ? { key: t.key, label: t.name } : null, { global: true })\n    })\n  },\n}\n"}},"statusLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"statusLog"}}}}}}}}}},{"_id":"6a87418e371849885f6502cd","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"AjVH2Zae2wVou7EYYHSN1","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;') }\n\nexport default {\n  css: `\n    .wrap { font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif; display:flex; flex-direction:column; height:100%; box-sizing:border-box; background:#ffffff; border:1px solid rgba(0,0,0,0.08); border-radius:12px; overflow:hidden; }\n    .hd { display:flex; align-items:center; justify-content:space-between; padding:22px 24px; border-bottom:1px solid rgba(0,0,0,0.08); flex:0 0 auto; }\n    .hd h2 { margin:0; font-size:18px; font-weight:700; color:#1a1a1a; }\n    .hd .ts { font-size:11.5px; color:#898781; }\n    .body { flex:1 1 auto; padding:24px; display:flex; flex-direction:column; gap:20px; }\n    .kpis { display:flex; gap:32px; }\n    .kpi .v { font-size:28px; font-weight:700; color:#1a1a1a; font-variant-numeric:tabular-nums; }\n    .kpi .l { font-size:11px; color:#898781; text-transform:uppercase; letter-spacing:.03em; margin-top:2px; }\n    .bar { position:relative; height:28px; border-radius:8px; background:#f0efec; overflow:hidden; display:flex; }\n    .seg { height:100%; display:flex; align-items:center; justify-content:center; color:#fff; font-size:11.5px; font-weight:700; white-space:nowrap; overflow:hidden; }\n    .legend { display:flex; gap:16px; font-size:12px; color:#52514e; }\n    .legend span.dot { width:8px; height:8px; border-radius:50%; display:inline-block; margin-right:5px; }\n    .warn { font-size:12px; color:#d03b3b; }\n    .empty { padding:24px; text-align:center; color:#898781; font-size:13px; }\n  `,\n  render(d2) {\n    const ds = d2.data('statusLog')\n    if (ds.loading) return `<div class=\"wrap\"><div class=\"empty\">Loading…</div></div>`\n\n    const FREE = '#0ca30c', BUSY = '#0a7d0a'\n    const rows = ds.records()\n    const byAgent = new Map()\n    for (const r of rows) {\n      const key = (r.agent && (r.agent.email || r.agent.name || r.agent._id)) || 'unknown'\n      const prev = byAgent.get(key)\n      if (!prev || new Date(r.createdAt).getTime() > new Date(prev.createdAt).getTime()) byAgent.set(key, r)\n    }\n    let onlineAgents = 0\n    for (const r of byAgent.values()) {\n      if (((r.status && r.status.type) || 'offline') === 'online') onlineAgents++\n    }\n    const available = onlineAgents * 2\n    const occupied = d2.state.occupiedSlots || 0\n    const occupiedClamped = Math.min(occupied, available)\n    const free = Math.max(0, available - occupiedClamped)\n    const overbooked = occupied > available\n\n    const pct = (n) => available > 0 ? (n / available) * 100 : 0\n    const bar = available === 0\n      ? `<div class=\"bar\"></div>`\n      : `<div class=\"bar\">\n          ${occupiedClamped > 0 ? `<div class=\"seg\" style=\"width:${pct(occupiedClamped)}%; background:${BUSY};\" title=\"Occupied: ${occupiedClamped}\">${occupiedClamped}</div>` : ``}\n          ${free > 0 ? `<div class=\"seg\" style=\"width:${pct(free)}%; background:${FREE};\" title=\"Free: ${free}\">${free}</div>` : ``}\n        </div>`\n\n    const ts = d2.state.lastRefresh ? new Date(d2.state.lastRefresh).toLocaleTimeString() : ''\n\n    return `<div class=\"wrap\">\n      <div class=\"hd\">\n        <h2>Live capacity slots</h2>\n        <div class=\"ts\">Auto-refresh · 60s${ts ? ' · last: ' + esc(ts) : ''}</div>\n      </div>\n      <div class=\"body\">\n        <div class=\"kpis\">\n          <div class=\"kpi\"><div class=\"v\">${available}</div><div class=\"l\">Available (${onlineAgents} online × 2)</div></div>\n          <div class=\"kpi\"><div class=\"v\">${occupiedClamped}</div><div class=\"l\">Occupied</div></div>\n          <div class=\"kpi\"><div class=\"v\">${free}</div><div class=\"l\">Free</div></div>\n        </div>\n        ${bar}\n        <div class=\"legend\">\n          <span><span class=\"dot\" style=\"background:${BUSY}\"></span>Occupied</span>\n          <span><span class=\"dot\" style=\"background:${FREE}\"></span>Free</span>\n        </div>\n        ${overbooked ? `<div class=\"warn\">Occupied (${occupied}) exceeds available capacity (${available}) — status log and active-chat counts may be out of sync.</div>` : ``}\n      </div>\n    </div>`\n  },\n  mounted(d2, root) {\n    const loadOccupied = async () => {\n      try {\n        const tds = d2.data('transfers')\n        tds.filter({ agent_joined_at: { $exists: true }, ended_at: null })\n        await tds.refresh()\n        const raw = await tds.loadAll()\n        const rows = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.records) ? raw.records : [])\n        d2.state.occupiedSlots = rows.length\n      } catch (e) {\n        d2.state.occupiedSlots = 0\n      }\n      d2.state.lastRefresh = Date.now()\n    }\n    loadOccupied()\n    d2.data('statusLog').refresh()\n    const id = setInterval(() => {\n      d2.data('statusLog').refresh()\n      loadOccupied()\n    }, 60000)\n    d2.onUnmount(() => clearInterval(id))\n  },\n}\n"}},"statusLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"statusLog"}}}},"transfers":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"transfers"}}}}}}}}}},{"_id":"6a875135aab9ec8de4871d9d","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"VV4M5h515AvyRKvfQntKl","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\\"/g, '&quot;') }\n\nfunction fmtDur(sec) {\n  sec = Math.max(0, Math.floor(sec))\n  const m = Math.floor(sec / 60), s = sec % 60\n  if (m > 0) return `${m}m${String(s).padStart(2, '0')}s`\n  return `${s}s`\n}\n\nfunction dotsHtml(list, maxShow) {\n  const shown = list.slice(0, maxShow)\n  const overflow = list.length - shown.length\n  let html = shown.map(d => `<span class=\"dot\" style=\"background:${d.bg}; border-color:${d.border || d.bg};\" title=\"${esc(d.title || '')}\"></span>`).join('')\n  if (overflow > 0) html += `<span class=\"dot dot-more\">+${overflow}</span>`\n  return html\n}\n\nexport default {\n  css: `\n    .wrap { font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif; display:flex; flex-direction:column; height:100%; box-sizing:border-box; background:#ffffff; border:1px solid rgba(0,0,0,0.08); border-radius:12px; overflow:hidden; }\n    .hd { display:flex; align-items:baseline; gap:10px; padding:18px 24px 4px; flex:0 0 auto; }\n    .hd h2 { margin:0; font-size:17px; font-weight:700; color:#1a1a1a; }\n    .hd .ts { font-size:11px; color:#898781; margin-left:auto; }\n    .note { padding:0 24px 12px; font-size:11.5px; color:#898781; flex:0 0 auto; }\n    .cols { flex:1 1 auto; display:grid; grid-template-columns:1fr 1fr 1fr; gap:1px; background:rgba(0,0,0,0.06); overflow:hidden; }\n    .col { background:#fff; display:flex; flex-direction:column; overflow:hidden; }\n    .col-hd { padding:12px 18px 8px; flex:0 0 auto; }\n    .col-title { font-size:11px; color:#898781; text-transform:uppercase; letter-spacing:.03em; }\n    .col-val { font-size:26px; font-weight:800; color:#1a1a1a; font-variant-numeric:tabular-nums; }\n    .col-sub { font-size:11px; color:#898781; margin-top:1px; }\n    .swarm { flex:1 1 auto; overflow-y:auto; padding:6px 16px 16px; display:flex; flex-wrap:wrap; align-content:flex-start; gap:4px; }\n    .dot { width:12px; height:12px; border-radius:50%; border:1.5px solid transparent; box-sizing:border-box; flex:0 0 auto; }\n    .dot-more { width:auto; height:12px; border-radius:6px; padding:0 5px; font-size:9px; line-height:12px; color:#898781; background:#f0efec; border:none; font-weight:700; }\n    .empty-dot { padding:6px 0; font-size:11.5px; color:#c4c2bc; }\n    .legend { display:flex; flex-wrap:wrap; gap:12px; padding:10px 24px; border-top:1px solid rgba(0,0,0,0.06); font-size:11px; color:#52514e; flex:0 0 auto; }\n    .legend .dot { display:inline-block; margin-right:4px; vertical-align:middle; }\n    .loading { padding:24px; text-align:center; color:#898781; font-size:13px; }\n  `,\n  render(d2) {\n    const bot = d2.data('botSessions')\n    const queue = d2.data('queueTransfers')\n    const snap = d2.data('agentsSnapshot')\n    const chats = d2.data('activeChats')\n    if (!bot || !queue || !snap || !chats) return `<div class=\"wrap\"><div class=\"loading\">Fontes de dado não conectadas.</div></div>`\n    if (bot.loading || queue.loading || snap.loading || chats.loading) return `<div class=\"wrap\"><div class=\"loading\">Carregando pulso da operação…</div></div>`\n\n    const now = d2.state.now || Date.now()\n\n    // ---- Bot column: live sessions currently with AI, colored by how long they've been at it ----\n    const botRows = bot.records()\n    const botDots = botRows.map(r => {\n      const startMs = r.ai_started_at ? new Date(r.ai_started_at).getTime() : now\n      const ageSec = Math.max(0, (now - startMs) / 1000)\n      let bg = '#3ea6ff' // fresh\n      if (ageSec > 360) bg = '#d03b3b' // >6min — possibly stuck\n      else if (ageSec > 120) bg = '#fab219' // 2-6min\n      return { bg, title: `Com o bot há ${fmtDur(ageSec)}` }\n    })\n    const botLong = botDots.filter(d => d.bg === '#d03b3b').length\n\n    // ---- Queue column: live in-queue transfers ----\n    const queueRows = queue.records()\n    const queueDots = queueRows.map(r => {\n      const startMs = r.queued_at ? new Date(r.queued_at).getTime() : now\n      const waitSec = Math.max(0, (now - startMs) / 1000)\n      let bg = '#fab219'\n      if (waitSec > 60) bg = '#d03b3b'\n      return { bg, title: `Na fila há ${fmtDur(waitSec)}${r.queue_label ? ' — ' + r.queue_label : ''}` }\n    })\n\n    // ---- Slots column: agent capacity, occupied vs free ----\n    const agentRows = snap.records()\n    let onlineAgents = 0\n    for (const r of agentRows) { if (r.status_type === 'online' || (r.status_type === 'pause' && r.in_service)) onlineAgents++ }\n    const capacity = onlineAgents * 2\n    const chatRows = chats.records()\n    const occupied = Math.min(chatRows.length, capacity)\n    const free = Math.max(0, capacity - occupied)\n    const slotDots = []\n    for (let i = 0; i < occupied; i++) slotDots.push({ bg: '#0a7d0a', title: 'Vaga ocupada' })\n    for (let i = 0; i < free; i++) slotDots.push({ bg: '#ffffff', border: '#0ca30c', title: 'Vaga livre' })\n\n    const ts = new Date(now).toLocaleTimeString('pt-BR')\n\n    const col = (title, val, sub, dots, emptyMsg) => `\n      <div class=\"col\">\n        <div class=\"col-hd\">\n          <div class=\"col-title\">${esc(title)}</div>\n          <div class=\"col-val\">${val}</div>\n          <div class=\"col-sub\">${esc(sub)}</div>\n        </div>\n        <div class=\"swarm\">${dots.length ? dotsHtml(dots, 400) : `<div class=\"empty-dot\">${esc(emptyMsg)}</div>`}</div>\n      </div>`\n\n    return `<div class=\"wrap\">\n      <div class=\"hd\"><h2>Pulso da operação — agora</h2><div class=\"ts\">Atualizado ${esc(ts)} · a cada 20s</div></div>\n      <div class=\"note\">Bot = sessões com IA nos últimos 15min (chat_sessions). Fila = transferências em espera agora (chat_transfers). Vagas = agentes online × 2 menos chats em atendimento agora.</div>\n      <div class=\"cols\">\n        ${col('Com o bot (15min)', botRows.length, botLong > 0 ? `${botLong} há mais de 6min` : 'tempo normal', botDots, 'Nenhuma conversa com o bot agora')}\n        ${col('Na fila', queueRows.length, queueRows.length > 0 ? 'esperando agente' : 'sem espera', queueDots, 'Fila vazia agora')}\n        ${col('Vagas de atendimento', `${occupied}/${capacity}`, `${free} livres · ${onlineAgents} agentes online`, slotDots, 'Nenhum agente online')}\n      </div>\n      <div class=\"legend\">\n        <span><span class=\"dot\" style=\"background:#3ea6ff\"></span>Bot: recente (&lt;2min)</span>\n        <span><span class=\"dot\" style=\"background:#fab219\"></span>2–6min / fila &lt;1min</span>\n        <span><span class=\"dot\" style=\"background:#d03b3b\"></span>Bot &gt;6min ou fila &gt;1min</span>\n        <span><span class=\"dot\" style=\"background:#0a7d0a\"></span>Vaga ocupada</span>\n        <span><span class=\"dot\" style=\"background:#fff;border-color:#0ca30c\"></span>Vaga livre</span>\n      </div>\n    </div>`\n  },\n  mounted(d2, root) {\n    const applyBotFilter = () => {\n      const since = new Date(Date.now() - 15 * 60 * 1000).toISOString()\n      d2.data('botSessions').filter({ status: 'ai', ai_started_at: { $gte: since } })\n    }\n    applyBotFilter()\n    d2.data('queueTransfers')\n    d2.data('agentsSnapshot')\n    d2.data('activeChats')\n\n    const tickId = setInterval(() => { d2.state.now = Date.now() }, 1000)\n    const refreshId = setInterval(() => {\n      applyBotFilter()\n      d2.data('queueTransfers').refresh()\n      d2.data('agentsSnapshot').refresh()\n      d2.data('activeChats').refresh()\n    }, 20000)\n    d2.onUnmount(() => { clearInterval(tickId); clearInterval(refreshId) })\n  },\n}"}},"botSessions":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"botSessions"}}}},"queueTransfers":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"queueTransfers"}}}},"agentsSnapshot":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentsSnapshot"}}}},"activeChats":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"activeChats"}}}}}}}}}},{"_id":"6a8753b1e36482d333ae0a2d","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"XXwmjIzDUgNohPKPUqBGl","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\\"/g, '&quot;') }\nfunction fmtHMS(sec) {\n  sec = Math.max(0, Math.round(sec || 0))\n  const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = sec%60\n  return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`\n}\nfunction fmtClock(d) { return d.toLocaleTimeString('pt-BR') }\nfunction extractMsgText(m) {\n  if (m.text != null) {\n    if (typeof m.text === 'string') return m.text\n    if (typeof m.text === 'object') return m.text.body || m.text.content || m.text.message || m.text.caption || JSON.stringify(m.text)\n  }\n  if (m.interactive) return '[resposta interativa]'\n  if (m.button_reply) return `[botão] ${(m.button_reply && (m.button_reply.title || m.button_reply.id)) || ''}`\n  if (m.list_reply) return `[lista] ${(m.list_reply && (m.list_reply.title || m.list_reply.id)) || ''}`\n  if (m.image) return '[imagem]'\n  if (m.audio) return '[áudio]'\n  if (m.video) return '[vídeo]'\n  if (m.document) return '[documento]'\n  if (m.sticker) return '[sticker]'\n  return m.event ? `[evento: ${m.event}]` : '—'\n}\n\n// Real enum values confirmed against production data — verified via direct API calls, never guessed.\nconst RED = 'rgba(229,0,75,1)', ORANGE = 'rgba(244,134,70,1)', DARK = 'rgba(54,54,54,1)', GRAY = 'rgba(150,150,150,1)'\n// Colors sourced from the real production BlockChart dataColors on artboard 9Xfo34kBDsXgiG4yWOzVR (verified, not guessed).\nconst CLS_META = {\n  ended_by_agent:   { label: 'Encerrado pelo agente',   color: 'rgba(104,218,90,1)' },\n  ended_by_user:    { label: 'Encerrado pelo usuário',  color: 'rgba(52,166,238,1)' },\n  inactivity_agent: { label: 'Inatividade do agente',   color: 'rgba(167,4,4,1)' },\n  inactivity_user:  { label: 'Inatividade do cliente',  color: 'rgba(9,255,220,1)' },\n}\nconst STATUS_META = {\n  completed:        { label: 'Atendido',              color: 'rgba(0,255,93,1)' },\n  outside_hours:    { label: 'Fora de horário',        color: 'rgba(255,171,0,1)' },\n  abandoned:        { label: 'Abandonado',             color: 'rgba(255,0,0,1)' },\n  unavailable_agent:{ label: 'Operador indisponível',  color: 'rgba(134,8,8,1)' },\n}\nconst PERIODS = [\n  { key: 'hoje', label: 'Hoje', days: 1 },\n  { key: '3d',   label: '-3D',  days: 3 },\n  { key: '7d',   label: '-7D',  days: 7 },\n  { key: '15d',  label: '-15D', days: 15 },\n  { key: '30d',  label: '-30D', days: 30 },\n]\nconst QUEUE_PALETTE = ['rgba(229,0,75,1)','rgba(52,166,238,1)','rgba(244,134,70,1)','rgba(104,218,90,1)','rgba(167,4,4,1)','rgba(9,255,220,1)','rgba(150,150,150,1)','rgba(255,171,0,1)']\nconst CHART_LIB = 'chart.js@4.4.1/auto'\n\nconst DRILL_LABELS = { classification: 'Classificação', status: 'Status', dayHour: 'Data/hora', abandonBucket: 'Espera (abandono)' }\nfunction buildDrillMongoFilter(drill, includeTimeDims) {\n  const extra = {}\n  if (drill.classification) extra.classification = drill.classification.value\n  if (drill.status) extra.transfer_status = drill.status.value\n  if (drill.abandonBucket) { extra.transfer_status = 'abandoned'; extra.queue_time = drill.abandonBucket.value }\n  if (includeTimeDims) {\n    if (drill.dayHour) extra.createdAt = { $gte: drill.dayHour.value.start, $lt: drill.dayHour.value.end }\n  }\n  return extra\n}\n\nfunction resolveChart(lib) {\n  if (!lib) return null\n  if (typeof lib === 'function') return lib\n  return lib.Chart || lib.default || null\n}\n\nfunction upsertChart(root, Chart, key, canvasSel, config) {\n  const canvas = root.querySelector(canvasSel)\n  if (!canvas) return\n  const existing = (typeof Chart.getChart === 'function') ? Chart.getChart(canvas) : null\n  if (existing && existing.config.type === config.type) {\n    existing.data = config.data; existing.options = config.options\n    existing.resize()\n    existing.update('none')\n  } else {\n    if (existing) { try { existing.destroy() } catch (e) {} }\n    config.options = Object.assign({ animation: false }, config.options)\n    const chart = new Chart(canvas, config)\n    chart.resize()\n  }\n}\n\nexport default {\n  css: `\n    .wrap { font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif; display:flex; flex-direction:column; gap:14px; padding:4px; box-sizing:border-box; color:#1a1a1a; }\n    .engine { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; background:#ececec; border:none; padding:14px 18px; font-size:13px; color:#3a3a3a; flex-wrap:wrap; }\n    .engine .rows { display:flex; flex-direction:column; gap:6px; }\n    .engine .row b { font-weight:700; margin-left:2px; }\n    .engine .status-wrap { display:flex; align-items:center; gap:8px; font-size:13px; }\n    .engine .badge { padding:6px 16px; border-radius:6px; font-weight:700; font-size:13px; }\n    .engine .badge.on { background:#2f7bf5; color:#fff; }\n    .engine .badge.off { background:rgba(150,150,150,0.25); color:${GRAY}; }\n    .engine .refresh { color:#8a8a8a; font-size:18px; line-height:1; }\n    .engine .runs { display:flex; gap:4px; margin-top:2px; }\n    .engine .run { min-width:24px; height:20px; padding:0 4px; border-radius:4px; font-size:10.5px; color:#1a3a1a; display:flex; align-items:center; justify-content:center; font-weight:700; }\n    .engine .run.ok { background:#3ddc5b; }\n    .engine .run.running { background:#2f7bf5; color:#fff; animation:pulse 1s infinite ease-in-out; }\n    @keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:.45;} }\n    .panel { background:#fff; border:1px solid rgba(0,0,0,0.08); border-radius:12px; padding:20px 24px; }\n    .panel h3 { margin:0 0 14px; font-size:15px; font-weight:700; color:#1a1a1a; }\n    .hd-row { display:flex; align-items:center; gap:16px; flex-wrap:wrap; }\n    .hd-row h2 { margin:0; font-size:18px; font-weight:800; }\n    .periods { display:flex; gap:4px; }\n    .customrange { display:flex; align-items:center; gap:6px; margin-left:8px; }\n    .customrange input[type=date] { font-size:12px; padding:5px 8px; border-radius:8px; border:1px solid rgba(0,0,0,0.15); color:#52514e; }\n    .rangesep { font-size:11px; color:#898781; }\n    .pbtn { padding:6px 12px; border-radius:8px; border:1px solid rgba(0,0,0,0.1); background:#fff; font-size:12px; font-weight:600; color:#52514e; cursor:pointer; }\n    .pbtn.active { background:${DARK}; color:#fff; border-color:${DARK}; }\n    .qchips-row { display:flex; align-items:center; gap:8px; margin-top:14px; padding-top:14px; border-top:1px solid rgba(0,0,0,0.06); flex-wrap:wrap; }\n    .qchips-label { font-size:11.5px; font-weight:700; color:#8a8a8a; text-transform:uppercase; letter-spacing:.02em; }\n    .qchips { display:flex; gap:6px; flex-wrap:wrap; }\n    .qchip { padding:5px 11px; border-radius:999px; border:1px solid rgba(0,0,0,0.12); background:#fff; font-size:11.5px; color:#52514e; cursor:pointer; }\n    .qchip.on { background:rgba(229,0,75,0.08); border-color:${RED}; color:${RED}; font-weight:700; }\n    .dchip { padding:5px 11px; border-radius:999px; border:1px solid ${DARK}; background:${DARK}; color:#fff; font-size:11.5px; font-weight:700; cursor:pointer; }\n    .dchip b { margin-left:4px; font-weight:800; }\n    .kpirow { display:grid; grid-template-columns:repeat(5,1fr); gap:1px; background:rgba(0,0,0,0.06); border-radius:10px; overflow:hidden; }\n    .kpi { background:#fff; padding:16px; }\n    .kpi-label { font-size:11px; color:#898781; text-transform:uppercase; letter-spacing:.02em; }\n    .kpi-val { font-size:26px; font-weight:800; color:#1a1a1a; margin-top:4px; font-variant-numeric:tabular-nums; }\n    .kpi-sub { font-size:11px; color:#898781; margin-top:2px; }\n    .kpi-delta { font-size:10.5px; font-weight:700; margin-top:6px; }\n    .kpi-delta.good { color:#1a9c4a; }\n    .kpi-delta.bad { color:${RED}; }\n    .kpi-delta.neutral { color:#b0aea8; font-weight:600; }\n    .compare-note { font-size:11px; color:#898781; margin:-6px 0 14px; }\n    .alertbox { display:flex; flex-direction:column; gap:8px; padding:14px 20px; }\n    .alert-row { display:flex; align-items:center; gap:10px; font-size:12.5px; font-weight:600; }\n    .alert-row.bad { color:${RED}; }\n    .alert-row.warn { color:#a86400; }\n    .alert-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; background:currentColor; }\n    .queuegrid { display:grid; grid-template-columns:1.4fr 1fr; gap:20px; }\n    .qbox { background:#f7f7f5; border-radius:10px; padding:14px 18px; }\n    .qbox .big { font-size:32px; font-weight:800; color:#1a1a1a; font-variant-numeric:tabular-nums; }\n    .qbox .lbl { font-size:11px; color:#898781; text-transform:uppercase; margin-top:2px; }\n    .qmetrics { display:flex; gap:24px; margin-top:10px; }\n    .qmetric .v { font-size:18px; font-weight:700; color:#1a1a1a; font-variant-numeric:tabular-nums; }\n    .qmetric .l { font-size:10.5px; color:#898781; }\n    .qrow { display:flex; justify-content:space-between; font-size:12.5px; padding:6px 0; border-bottom:1px solid rgba(0,0,0,0.05); }\n    .qrow .cap { font-size:11px; color:#b0aea8; margin-left:6px; }\n    .charts { display:grid; grid-template-columns:1fr 1fr; gap:24px; align-items:start; }\n    .chart-box { height:220px; position:relative; overflow:hidden; }\n    .chart-box canvas { width:100% !important; height:100% !important; }\n    .legend { display:flex; flex-wrap:wrap; gap:10px; font-size:11px; color:#52514e; margin-top:8px; }\n    .legend .sw { width:8px; height:8px; border-radius:2px; display:inline-block; margin-right:4px; }\n    .legend-item { cursor:pointer; user-select:none; }\n    .legend-item.muted { opacity:0.35; text-decoration:line-through; }\n    .typebtns { display:flex; gap:6px; margin-bottom:8px; }\n    .typebtn { font-size:10.5px; padding:3px 9px; border-radius:6px; border:1px solid rgba(0,0,0,0.12); background:#fff; color:#52514e; cursor:pointer; }\n    .typebtn.active { background:${DARK}; color:#fff; border-color:${DARK}; }\n    .loading, .empty { padding:24px; text-align:center; color:#898781; font-size:13px; }\n    .note { font-size:11px; color:#c4c2bc; margin-top:10px; }\n    .atable { display:flex; flex-direction:column; }\n    .arow { display:grid; grid-template-columns:1fr 1.3fr 1.6fr 1.4fr 0.9fr 0.9fr; gap:8px; align-items:center; padding:9px 6px; font-size:12.5px; border-bottom:1px solid rgba(0,0,0,0.05); }\n    .arow[data-peek] { cursor:pointer; }\n    .arow[data-peek]:hover { background:#f7f7f5; }\n    .arow.ahead { font-size:10.5px; font-weight:700; color:#898781; text-transform:uppercase; letter-spacing:.02em; border-bottom:2px solid rgba(0,0,0,0.08); }\n    .arow .sw { width:8px; height:8px; border-radius:2px; display:inline-block; margin-right:6px; }\n    .sesstable { display:flex; flex-direction:column; }\n    .sessrow { display:grid; grid-template-columns:0.8fr 1fr 1.4fr 2fr 1fr; gap:8px; align-items:center; padding:9px 6px; font-size:12.5px; border-bottom:1px solid rgba(0,0,0,0.05); cursor:pointer; }\n    .sessrow:hover { background:#f7f7f5; }\n    .sessrow.shead { font-size:10.5px; font-weight:700; color:#898781; text-transform:uppercase; letter-spacing:.02em; border-bottom:2px solid rgba(0,0,0,0.08); cursor:default; }\n    .sessrow.shead:hover { background:transparent; }\n    .sess-badge { background:rgba(229,0,75,0.1); color:${RED}; border-radius:6px; padding:2px 7px; font-size:11px; font-weight:600; }\n    .sess-detail { background:#faf9f7; border-radius:8px; padding:8px 12px; margin:2px 0 8px; }\n    .sess-detail-row { display:grid; grid-template-columns:1fr 1.6fr 1.4fr 1fr 1fr; gap:8px; font-size:11.5px; color:#52514e; padding:5px 0; border-bottom:1px solid rgba(0,0,0,0.04); }\n    .sess-detail-row:last-child { border-bottom:none; }\n    .peek-backdrop { position:fixed; inset:0; background:rgba(0,0,0,0.4); display:flex; align-items:center; justify-content:center; z-index:1000; }\n    .peek-modal { background:#fff; border-radius:12px; padding:20px 24px; width:560px; max-width:90vw; max-height:85vh; overflow-y:auto; }\n    .peek-thread-wrap { margin-top:16px; border-top:1px solid rgba(0,0,0,0.08); padding-top:14px; }\n    .peek-thread-wrap h4 { margin:0 0 10px; font-size:13px; }\n    .thread { display:flex; flex-direction:column; gap:8px; max-height:320px; overflow-y:auto; padding-right:4px; }\n    .thread-msg { display:flex; }\n    .thread-msg.user { justify-content:flex-end; }\n    .thread-msg.agent { justify-content:flex-start; }\n    .thread-msg.bot { justify-content:flex-start; }\n    .thread-bubble { max-width:78%; border-radius:12px; padding:8px 12px; font-size:12.5px; line-height:1.4; }\n    .thread-msg.user .thread-bubble { background:rgba(229,0,75,0.08); color:#1a1a1a; }\n    .thread-msg.agent .thread-bubble { background:#f0efeb; color:#1a1a1a; }\n    .thread-msg.bot .thread-bubble { background:#f7f7f5; color:#52514e; font-style:italic; }\n    .thread-meta { font-size:10px; color:#898781; margin-top:4px; text-transform:uppercase; letter-spacing:.02em; }\n    .peek-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }\n    .peek-head h3 { margin:0; font-size:16px; }\n    .peek-close { cursor:pointer; font-size:20px; color:#898781; line-height:1; padding:0 4px; }\n    .peek-row { display:flex; justify-content:space-between; gap:12px; font-size:13px; padding:7px 0; border-bottom:1px solid rgba(0,0,0,0.05); }\n    .peek-row b { color:#898781; font-weight:600; }\n    .topops { display:flex; flex-direction:column; gap:10px; }\n    .toprow { display:grid; grid-template-columns:140px 1fr 40px 90px; gap:10px; align-items:center; font-size:12.5px; }\n    .topname { font-weight:600; color:#1a1a1a; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n    .topbar-track { background:#f0efeb; border-radius:6px; height:16px; overflow:hidden; }\n    .topbar { background:${RED}; height:100%; border-radius:6px; }\n    .topval { font-weight:700; text-align:right; font-variant-numeric:tabular-nums; }\n    .topsub { color:#898781; font-size:11px; }\n    .skel { background:linear-gradient(90deg, #eee 25%, #f2f2f0 37%, #eee 63%); background-size:400% 100%; animation:shimmer 1.4s ease infinite; border-radius:6px; }\n    @keyframes shimmer { 0%{background-position:100% 50%;} 100%{background-position:0% 50%;} }\n    .skel-kpi-label { width:70%; height:9px; margin-bottom:8px; }\n    .skel-kpi-val { width:50%; height:24px; margin-bottom:6px; }\n    .skel-kpi-sub { width:80%; height:8px; }\n    .skel-qbig { width:60px; height:32px; margin-bottom:8px; }\n    .skel-qlbl { width:70%; height:9px; }\n    .skel-chart { width:100%; height:220px; border-radius:8px; }\n    .skel-legend { width:40%; height:8px; margin-top:8px; }\n    .skel-pill { width:90px; height:14px; border-radius:999px; }\n  `,\n  render(d2) {\n    const skelKpiRow = () => `<div class=\"kpirow\">${Array.from({length:5}).map(() => `<div class=\"kpi\"><div class=\"skel skel-kpi-label\"></div><div class=\"skel skel-kpi-val\"></div><div class=\"skel skel-kpi-sub\"></div></div>`).join('')}</div>`\n    const skelQueue = () => `<div class=\"queuegrid\">\n      <div class=\"qbox\"><div class=\"skel skel-qbig\"></div><div class=\"skel skel-qlbl\"></div></div>\n      <div class=\"qbox\">${Array.from({length:3}).map(() => `<div class=\"skel\" style=\"width:100%;height:12px;margin-bottom:8px;\"></div>`).join('')}</div>\n    </div>`\n    const skelCharts = () => `<div class=\"charts\">\n      <div><div class=\"skel skel-chart\"></div><div class=\"skel skel-legend\"></div></div>\n      <div><div class=\"skel skel-chart\"></div><div class=\"skel skel-legend\"></div></div>\n    </div>`\n\n    const eng = d2.state.engine\n    const engineHtml = !eng ? `<div class=\"panel engine\">${Array.from({length:4}).map(() => `<div class=\"skel skel-pill\"></div>`).join('')}</div>` : `\n      <div class=\"panel engine\">\n        <div class=\"rows\">\n          <div class=\"row\">Tempo médio de distribuição: <b>${eng.avgDuration != null ? eng.avgDuration.toFixed(1) + 's' : '—'}</b></div>\n          <div class=\"row\">Última distribuição finalizada: <b>${eng.lastFinishedAt ? fmtClock(eng.lastFinishedAt) : '—'}</b></div>\n          <div class=\"runs\">${eng.recent.map(r => `<span class=\"run ${r.running ? 'running' : 'ok'}\" title=\"${r.running ? 'em andamento' : r.duration.toFixed(1) + 's'}\">${r.running ? '…' : Math.round(r.duration) + 's'}</span>`).join('')}</div>\n        </div>\n        <div class=\"status-wrap\"><span>Status:</span><span class=\"badge ${eng.running ? 'on' : 'off'}\">${eng.running ? 'Distribuindo Chamadas' : 'Ocioso'}</span></div>\n        <span class=\"refresh\" title=\"Atualiza a cada 10s\">⟳</span>\n      </div>`\n\n    const k = d2.state.data\n    const periodBtns = PERIODS.map(p => `<button class=\"pbtn ${d2.state.period === p.key ? 'active' : ''}\" data-period=\"${p.key}\">${p.label}</button>`).join('')\n\n    if (!k) {\n      return `<div class=\"wrap\">\n        <div class=\"panel\"><div class=\"hd-row\"><h2>Dashboard da Operação</h2><div class=\"periods\">${periodBtns}</div></div></div>\n        <div class=\"panel\">${skelKpiRow()}</div>\n        <div class=\"panel\"><h3>Monitoramento de fila</h3>${skelQueue()}</div>\n        ${engineHtml}\n        <div class=\"panel\"><h3>Atendimentos concluídos, por classificação</h3>${skelCharts()}</div>\n        <div class=\"panel\"><h3>Atendimentos por status</h3>${skelCharts()}</div>\n        <div class=\"panel\"><h3>Tempo máximo de espera por fila, por hora</h3><div class=\"chart-box\" style=\"height:260px\"><div class=\"skel skel-chart\"></div></div></div>\n        <div class=\"panel\"><h3>Curva de abandono, por tempo de espera</h3><div class=\"chart-box\" style=\"height:260px\"><div class=\"skel skel-chart\"></div></div></div>\n        <div class=\"panel\"><h3>Top operadores</h3>${Array.from({length:5}).map(() => `<div class=\"skel\" style=\"width:100%;height:16px;margin-bottom:10px;\"></div>`).join('')}</div>\n        <div class=\"panel\"><h3>Atendimentos recentes</h3>${Array.from({length:5}).map(() => `<div class=\"skel\" style=\"width:100%;height:14px;margin-bottom:10px;\"></div>`).join('')}</div>\n      </div>`\n    }\n    if (k.error) {\n      return `<div class=\"wrap\"><div class=\"panel\"><div class=\"hd-row\"><h2>Dashboard da Operação</h2><div class=\"periods\">${periodBtns}</div></div></div><div class=\"panel\"><div class=\"empty\">${esc(k.error)}</div></div>${engineHtml}</div>`\n    }\n\n    const peekRecord = d2.state.peekId ? (k.recentRecords || []).find(r => r._id === d2.state.peekId) : null\n    const drillActive = d2.state.drillFilters || {}\n    const drillChips = Object.entries(drillActive).map(([dim, f]) => `<span class=\"dchip\" data-dim=\"${esc(dim)}\" title=\"Clique para remover este filtro\">${esc(DRILL_LABELS[dim] || dim)}: ${esc(f.label)} <b>×</b></span>`).join('')\n    const drillChipsRow = drillChips ? `<div class=\"qchips-row\"><span class=\"qchips-label\">Filtros (clique no gráfico p/ adicionar):</span><div class=\"qchips\">${drillChips}</div></div>` : ''\n\n    const deselected = d2.state.deselectedQueues || {}\n    const qchips = k.allQueues.map(q => `<span class=\"qchip ${deselected[q] ? '' : 'on'}\" data-queue=\"${esc(q)}\">${esc(q)}</span>`).join('')\n    const hourlyMode = d2.state.hourlyMode || 'hour'\n    const queueMetric = d2.state.queueMetric || 'wait'\n\n    const prev = k.prevStats\n    const deltaPP = (curr, prevVal, higherIsBetter) => {\n      if (prevVal == null || curr == null) return ''\n      const diff = Math.round((curr - prevVal) * 10) / 10\n      if (diff === 0) return `<div class=\"kpi-delta neutral\">≈ igual ao período anterior</div>`\n      const good = higherIsBetter ? diff > 0 : diff < 0\n      return `<div class=\"kpi-delta ${good ? 'good' : 'bad'}\">${diff > 0 ? '▲' : '▼'} ${Math.abs(diff)}pp vs. período anterior</div>`\n    }\n    const deltaPct = (curr, prevVal, higherIsBetter) => {\n      if (prevVal == null || curr == null || prevVal === 0) return ''\n      const pctChange = Math.round(((curr - prevVal) / Math.abs(prevVal)) * 1000) / 10\n      if (pctChange === 0) return `<div class=\"kpi-delta neutral\">≈ igual ao período anterior</div>`\n      const good = higherIsBetter ? pctChange > 0 : pctChange < 0\n      return `<div class=\"kpi-delta ${good ? 'good' : 'bad'}\">${pctChange > 0 ? '▲' : '▼'} ${Math.abs(pctChange)}% vs. período anterior</div>`\n    }\n    const kpis = [\n      ['Total de atendimentos', k.total, 'no período', deltaPct(k.total, prev && prev.total, true)],\n      ['Consumidores', k.consumidores, 'CPFs distintos', ''],\n      ['Nível de serviço', k.completedCount > 0 ? k.nivelServico + '%' : '—', `${k.slaOkCount}/${k.completedCount} em até 60s`, deltaPP(k.nivelServico, prev && prev.nivelServico, true)],\n      ['Abandono', k.abandonoBase > 0 ? k.abandonoPct + '%' : '0%', `${k.abandonedRealCount} clientes`, deltaPP(k.abandonoPct, prev && prev.abandonoPct, false)],\n      ['TME', fmtHMS(k.tme), 'tempo médio de espera', deltaPct(k.tme, prev && prev.tme, false)],\n    ]\n    const kpiHtml = kpis.map(([l,v,s,d]) => `<div class=\"kpi\"><div class=\"kpi-label\">${esc(l)}</div><div class=\"kpi-val\">${v}</div><div class=\"kpi-sub\">${esc(s)}</div>${d}</div>`).join('')\n\n    const alerts = []\n    if (prev) {\n      if (k.completedCount > 0 && prev.nivelServico != null) {\n        const diff = k.nivelServico - prev.nivelServico\n        if (diff <= -10) alerts.push({ severity: 'bad', text: `Nível de serviço caiu de ${prev.nivelServico}% para ${k.nivelServico}% (${Math.round(diff)}pp) comparado ao período anterior equivalente.` })\n      }\n      if (k.abandonoBase > 0 && prev.abandonoPct != null) {\n        if (prev.abandonoPct > 0) {\n          const relChange = Math.round(((k.abandonoPct - prev.abandonoPct) / prev.abandonoPct) * 100)\n          if (relChange >= 30 && (k.abandonoPct - prev.abandonoPct) >= 1) alerts.push({ severity: 'bad', text: `Abandono subiu de ${prev.abandonoPct}% para ${k.abandonoPct}% (+${relChange}%) comparado ao período anterior equivalente.` })\n        } else if (k.abandonoPct >= 5) {\n          alerts.push({ severity: 'bad', text: `Abandono foi de 0% no período anterior equivalente para ${k.abandonoPct}% agora.` })\n        }\n      }\n    }\n    const queueOverallMax = {}\n    for (const q of (k.allQueues || [])) {\n      const hoursMap = (k.waitByQueueHour && k.waitByQueueHour[q]) || {}\n      queueOverallMax[q] = Math.max(0, ...Object.values(hoursMap))\n    }\n    const queueMaxes = Object.values(queueOverallMax).filter(v => v > 0)\n    if (queueMaxes.length >= 3) {\n      const sortedMaxes = [...queueMaxes].sort((a, b) => a - b)\n      const median = sortedMaxes[Math.floor(sortedMaxes.length / 2)]\n      if (median > 0) {\n        for (const [q, m] of Object.entries(queueOverallMax)) {\n          if (m > median * 2 && m > 60) alerts.push({ severity: 'warn', text: `Fila \"${esc(q)}\" teve espera máxima de ${fmtHMS(m)} no período, bem acima da mediana entre as filas (${fmtHMS(median)}).` })\n        }\n      }\n    }\n    alerts.sort((a, b) => (a.severity === 'bad' ? 0 : 1) - (b.severity === 'bad' ? 0 : 1))\n    const alertsHtml = alerts.length ? `<div class=\"panel alertbox\">${alerts.slice(0, 3).map(a => `<div class=\"alert-row ${a.severity}\"><span class=\"alert-dot\"></span>${a.text}</div>`).join('')}</div>` : ''\n\n    const hiddenCls = d2.state.hiddenCls || {}\n    const hiddenStatus = d2.state.hiddenStatus || {}\n\n    const clsItems = k.byClassification.map(c => ({ val: c.total, color: (CLS_META[c.key]||{}).color || GRAY, label: (CLS_META[c.key]||{}).label || c.key, key: c.key }))\n    const clsLegend = clsItems.map(c => `<span class=\"legend-item ${hiddenCls[c.key] ? 'muted' : ''}\" data-domain=\"cls\" data-key=\"${esc(c.key)}\"><span class=\"sw\" style=\"background:${c.color}\"></span>${esc(c.label)}</span>`).join('')\n\n    const statusItems = k.byStatus.map(c => ({ val: c.total, color: (STATUS_META[c.key]||{}).color || GRAY, label: (STATUS_META[c.key]||{}).label || c.key, key: c.key }))\n    const statusLegend = statusItems.map(c => `<span class=\"legend-item ${hiddenStatus[c.key] ? 'muted' : ''}\" data-domain=\"status\" data-key=\"${esc(c.key)}\"><span class=\"sw\" style=\"background:${c.color}\"></span>${esc(c.label)}</span>`).join('')\n\n    const hiddenQueue = d2.state.hiddenQueue || {}\n    const queueListSorted = [...(k.allQueues || [])].sort()\n    const queueWaitLegend = queueListSorted.map((q, i) => `<span class=\"legend-item ${hiddenQueue[q] ? 'muted' : ''}\" data-domain=\"queue\" data-key=\"${esc(q)}\"><span class=\"sw\" style=\"background:${QUEUE_PALETTE[i % QUEUE_PALETTE.length]}\"></span>${esc(q)}</span>`).join('')\n\n    const queueLimits = k.queueLimits || {}\n    const queueRows = k.liveQueueByLabel.length\n      ? k.liveQueueByLabel.map(q => `<div class=\"qrow\"><span>${esc(q.label)}</span><span>${q.count}${queueLimits[q.label] != null ? `<span class=\"cap\">/ limite ${queueLimits[q.label]}</span>` : ''}</span></div>`).join('')\n      : `<div class=\"empty\">Nada na fila agora</div>`\n\n    const Chart = resolveChart(d2.lib(CHART_LIB))\n    const chartsFailed = d2.lib.state(CHART_LIB) === 'error'\n    const chartTypes = d2.state.chartTypes || {}\n    const typeBtns = (chartKey, options) => options.map(t => `<button class=\"typebtn ${((chartTypes[chartKey] || options[0].v) === t.v) ? 'active' : ''}\" data-chart=\"${chartKey}\" data-type=\"${t.v}\">${t.l}</button>`).join('')\n    const chartArea = (idA, idB, legend, keyA, keyB) => (!Chart\n      ? (chartsFailed ? `<div class=\"empty\">Não foi possível carregar a biblioteca de gráficos.</div>` : skelCharts())\n      : `<div class=\"charts\">\n          <div><div class=\"typebtns\">${typeBtns(keyA, [{v:'bar',l:'Barras'},{v:'pie',l:'Pizza'},{v:'doughnut',l:'Rosca'}])}</div><div class=\"chart-box\"><canvas id=\"${idA}\" width=\"480\" height=\"220\"></canvas></div><div class=\"legend\">${legend}</div></div>\n          <div><div class=\"typebtns\">${typeBtns(keyB, [{v:'line',l:'Linha'},{v:'bar',l:'Barras'}])}</div><div class=\"chart-box\"><canvas id=\"${idB}\" width=\"480\" height=\"220\"></canvas></div><div class=\"legend\">${legend}</div></div>\n        </div>`)\n\n    return `<div class=\"wrap\">\n      <div class=\"panel\">\n        <div class=\"hd-row\">\n          <h2>Dashboard da Operação</h2>\n          <div class=\"periods\">${periodBtns}</div>\n        </div>\n        <div class=\"qchips-row\">\n          <span class=\"qchips-label\">Personalizado:</span>\n          <div class=\"customrange\">\n            <input type=\"date\" class=\"rangeStart\" value=\"${esc(d2.state.customStart || '')}\" />\n            <span class=\"rangesep\">até</span>\n            <input type=\"date\" class=\"rangeEnd\" value=\"${esc(d2.state.customEnd || '')}\" />\n            <button class=\"applyRangeBtn typebtn ${d2.state.period === 'custom' ? 'active' : ''}\">Aplicar período</button>\n          </div>\n        </div>\n${drillChipsRow}\n        <div class=\"qchips-row\"><span class=\"qchips-label\">Filas:</span><div class=\"qchips\">${qchips}</div></div>\n        <div class=\"qchips-row\">\n          <span class=\"qchips-label\">Gráficos por hora:</span>\n          <button class=\"hmode-btn typebtn ${hourlyMode === 'hour' ? 'active' : ''}\" data-mode=\"hour\">Por hora do dia</button>\n          <button class=\"hmode-btn typebtn ${hourlyMode === 'dayhour' ? 'active' : ''}\" data-mode=\"dayhour\">Por dia e hora</button>\n        </div>\n      </div>\n\n      ${alertsHtml}\n\n      <div class=\"panel\">\n        <h3>Indicadores de serviço</h3>\n        <div class=\"compare-note\">Comparação \"vs. período anterior\": ${d2.state.period === 'custom' ? 'intervalo de duração igual, imediatamente anterior ao início do período selecionado' : d2.state.period === 'hoje' ? 'ontem, até este mesmo horário' : `os ${(PERIODS.find(x => x.key === d2.state.period) || PERIODS[0]).days} dias imediatamente anteriores a este período (mesma duração)`}.</div>\n        <div class=\"kpirow\">${kpiHtml}</div>\n      </div>\n\n      <div class=\"panel\">\n        <h3>Monitoramento de fila</h3>\n        <div class=\"queuegrid\">\n          <div class=\"qbox\">\n            <div class=\"big\">${k.queueLiveCount}</div>\n            <div class=\"lbl\">aguardando na fila agora</div>\n            <div class=\"qmetrics\">\n              <div class=\"qmetric\"><div class=\"v\">${fmtHMS(k.queueAvgWait)}</div><div class=\"l\">TEMPO MÉDIO (agora)</div></div>\n              <div class=\"qmetric\"><div class=\"v\">${fmtHMS(k.queueMaxWait)}</div><div class=\"l\">TEMPO MÁXIMO (agora)</div></div>\n            </div>\n          </div>\n          <div class=\"qbox\"><div class=\"lbl\" style=\"margin-bottom:8px;\">QUEBRA POR FILA (esperando agora)</div>${queueRows}</div>\n        </div>\n      </div>\n\n      ${engineHtml}\n\n      <div class=\"panel\">\n        <h3>Atendimentos concluídos, por classificação</h3>\n        ${chartArea('clsBar', 'clsLine', clsLegend, 'clsTotal', 'clsHourly')}\n        <div class=\"note\">Classificação só existe em atendimentos concluídos.</div>\n      </div>\n\n      <div class=\"panel\"><h3>Atendimentos por status</h3>${chartArea('statusBar', 'statusLine', statusLegend, 'statusTotal', 'statusHourly')}</div>\n\n      <div class=\"panel\">\n        <div class=\"hd-row\">\n          <h3>${queueMetric === 'volume' ? 'Volume de atendimentos com fila > 1min, por fila' : 'Tempo máximo de espera por fila'}</h3>\n          <div class=\"typebtns\">\n            <button class=\"qmetric-btn typebtn ${queueMetric === 'wait' ? 'active' : ''}\" data-metric=\"wait\">Tempo máx. de espera</button>\n            <button class=\"qmetric-btn typebtn ${queueMetric === 'volume' ? 'active' : ''}\" data-metric=\"volume\">Volume com fila > 1min</button>\n          </div>\n        </div>\n        ${!Chart\n          ? (chartsFailed ? `<div class=\"empty\">Não foi possível carregar a biblioteca de gráficos.</div>` : `<div class=\"chart-box\" style=\"height:260px\"><div class=\"skel skel-chart\"></div></div>`)\n          : `<div class=\"chart-box\" style=\"height:260px\"><canvas id=\"queueWaitLine\" width=\"960\" height=\"260\"></canvas></div><div class=\"legend\">${queueWaitLegend}</div>`}\n      </div>\n\n      <div class=\"panel\">\n        <h3>Curva de abandono, por tempo de espera</h3>\n        ${!Chart\n          ? (chartsFailed ? `<div class=\"empty\">Não foi possível carregar a biblioteca de gráficos.</div>` : `<div class=\"chart-box\" style=\"height:260px\"><div class=\"skel skel-chart\"></div></div>`)\n          : `<div class=\"chart-box\" style=\"height:260px\"><canvas id=\"abandonCurve\" width=\"960\" height=\"260\"></canvas></div><div class=\"note\">Barras = qtd. de abandonos naquela faixa de espera. Linha = % acumulado dos abandonos até aquela faixa.</div>`}\n      </div>\n\n      <div class=\"panel\">\n        <h3>Top operadores</h3>\n        ${k.topOperators.length ? `<div class=\"topops\">${(() => { const maxTotal = Math.max(1, ...k.topOperators.map(o => o.total)); return k.topOperators.map(o => `\n          <div class=\"toprow\">\n            <span class=\"topname\">${esc(o.name)}</span>\n            <div class=\"topbar-track\"><div class=\"topbar\" style=\"width:${Math.round((o.total / maxTotal) * 100)}%\"></div></div>\n            <span class=\"topval\">${o.total}</span>\n            <span class=\"topsub\">${fmtHMS(o.avgHandleTime)} méd.</span>\n          </div>`).join('') })()}</div>` : `<div class=\"empty\">Nenhum atendimento no período/filtro atual.</div>`}\n        <div class=\"note\">Ranking por volume de atendimentos no período/filtro atual, com tempo médio de atendimento.</div>\n      </div>\n\n      <div class=\"panel\">\n        <h3>Atendimentos recentes</h3>\n        <div class=\"atable\">\n          <div class=\"arow ahead\"><span>Horário</span><span>Fila</span><span>Operador</span><span>Status</span><span>Espera</span><span>Duração</span></div>\n          ${k.recentRecords.length ? k.recentRecords.map(r => `<div class=\"arow\" data-peek=\"${esc(r._id)}\">\n            <span>${esc(fmtClock(new Date(r.createdAt)))}</span>\n            <span>${esc(r.queue_label || '—')}</span>\n            <span>${esc(r.agentName || '—')}</span>\n            <span><span class=\"sw\" style=\"background:${(STATUS_META[r.transfer_status] || {}).color || GRAY}\"></span>${esc((STATUS_META[r.transfer_status] || {}).label || r.transfer_status || '—')}</span>\n            <span>${fmtHMS(r.queue_time)}</span>\n            <span>${fmtHMS(r.agent_time)}</span>\n          </div>`).join('') : `<div class=\"empty\">Nenhum atendimento no período/filtro atual.</div>`}\n        </div>\n        <div class=\"note\">Mostrando até 25 atendimentos mais recentes, respeitando os filtros ativos acima. Clique numa linha para ver os detalhes.</div>\n      </div>\n\n      <div class=\"panel\">\n        <h3>Sessões com múltiplas transferências</h3>\n        ${k.bouncingSessions && k.bouncingSessions.length ? `<div class=\"sesstable\">\n          <div class=\"sessrow shead\"><span>Sessão</span><span>Transferências</span><span>Zero-engajamento</span><span>Filas</span><span>Última em</span></div>\n          ${k.bouncingSessions.map(s => `<div class=\"sessrow\" data-session=\"${esc(s.sessionId)}\">\n            <span>${esc(String(s.sessionId).slice(-6))}</span>\n            <span><b>${s.total}</b></span>\n            <span>${s.wasted > 0 ? `<span class=\"sess-badge\">${s.wasted} sem atendimento</span>` : '—'}</span>\n            <span>${esc(s.queues.slice(0, 2).join(', '))}${s.queues.length > 2 ? '…' : ''}</span>\n            <span>${esc(fmtClock(new Date(s.lastCreatedAt)))}</span>\n          </div>${d2.state.sessionPeekId === s.sessionId ? `<div class=\"sess-detail\">\n            ${!d2.state.sessionPeekRows ? `<div class=\"empty\">Carregando…</div>` : (d2.state.sessionPeekRows.length ? d2.state.sessionPeekRows.map(row => `<div class=\"sess-detail-row\">\n              <span>${esc(fmtClock(new Date(row.createdAt)))}</span>\n              <span>${esc(row.agent_name || '—')}</span>\n              <span>${esc((CLS_META[row.classification] || {}).label || row.classification || '—')}</span>\n              <span>fila ${fmtHMS(row.queue_time)}</span>\n              <span>atend. ${fmtHMS(row.agent_time)}</span>\n            </div>`).join('') : `<div class=\"empty\">Sem transferências.</div>`)}\n          </div>` : ''}`).join('')}\n        </div>` : `<div class=\"empty\">Nenhuma sessão com mais de uma transferência no período/filtro atual.</div>`}\n        <div class=\"note\">Sessões em que o usuário foi transferido para atendimento mais de uma vez no período. \"Zero-engajamento\" = transferências que terminaram sem o operador chegar a atender (agent_time = 0). Clique numa sessão para ver a linha do tempo.</div>\n      </div>\n      ${peekRecord ? `<div class=\"peek-backdrop\">\n        <div class=\"peek-modal\">\n          <div class=\"peek-head\"><h3>Atendimento</h3><span class=\"peek-close\">×</span></div>\n          <div class=\"peek-body\">\n            <div class=\"peek-row\"><b>Horário</b><span>${esc(new Date(peekRecord.createdAt).toLocaleString('pt-BR'))}</span></div>\n            <div class=\"peek-row\"><b>Fila</b><span>${esc(peekRecord.queue_label || '—')}</span></div>\n            <div class=\"peek-row\"><b>Operador</b><span>${esc(peekRecord.agentName || '—')}</span></div>\n            <div class=\"peek-row\"><b>Status</b><span>${esc((STATUS_META[peekRecord.transfer_status] || {}).label || peekRecord.transfer_status || '—')}</span></div>\n            <div class=\"peek-row\"><b>Classificação</b><span>${esc((CLS_META[peekRecord.classification] || {}).label || peekRecord.classification || '—')}</span></div>\n            <div class=\"peek-row\"><b>Tempo de espera</b><span>${fmtHMS(peekRecord.queue_time)}</span></div>\n            <div class=\"peek-row\"><b>Duração do atendimento</b><span>${fmtHMS(peekRecord.agent_time)}</span></div>\n            <div class=\"peek-row\"><b>Ambiente</b><span>${esc(peekRecord.env || '—')}</span></div>\n          </div>\n          <div class=\"peek-thread-wrap\">\n            <h4>Thread da conversa</h4>\n            ${!d2.state.peekThread ? `<div class=\"empty\">Carregando conversa…</div>` : (d2.state.peekThread.error ? `<div class=\"empty\">${esc(d2.state.peekThread.error)}</div>` : (d2.state.peekThread.messages.length ? `<div class=\"thread\">${d2.state.peekThread.messages.map(m => `<div class=\"thread-msg ${m.who}\"><div><div class=\"thread-bubble\">${esc(m.text)}</div><div class=\"thread-meta\">${esc(m.label)}${m.time ? ' · ' + esc(fmtClock(new Date(m.time))) : ''}</div></div></div>`).join('')}</div>` : `<div class=\"empty\">Nenhuma mensagem encontrada para esta sessão.</div>`))}\n            <div class=\"note\">Mídias (imagem/áudio/vídeo/documento/sticker) aparecem apenas como indicação, sem preview.</div>\n          </div>\n        </div>\n      </div>` : ''}\n    </div>`\n  },\n  async mounted(d2, root) {\n    d2.state.period = 'hoje'\n    d2.state.deselectedQueues = {}\n    d2.state.hiddenCls = {}\n    d2.state.hiddenStatus = {}\n    d2.state.hiddenQueue = {}\n    d2.state.hourlyMode = 'hour'\n    d2.state.queueMetric = 'wait'\n    d2.state.drillFilters = {}\n    d2.state.customStart = ''\n    d2.state.customEnd = ''\n    d2.state.peekId = null\n    d2.state.peekThread = null\n    d2.state.sessionPeekId = null\n    d2.state.sessionPeekRows = null\n\n    d2.on('.arow[data-peek]', 'click', (e, el) => {\n      const id = el.dataset.peek\n      d2.state.peekId = id\n      d2.state.peekThread = null\n      const rec = (d2.state.data && d2.state.data.recentRecords || []).find(r => r._id === id)\n      const sessionId = rec && rec.session\n      if (sessionId) loadPeekThread(sessionId)\n      else d2.state.peekThread = { error: 'Sessão não identificada para este atendimento.' }\n    })\n    d2.on('.sessrow[data-session]', 'click', (e, el) => {\n      const id = el.dataset.session\n      if (d2.state.sessionPeekId === id) { d2.state.sessionPeekId = null; d2.state.sessionPeekRows = null; return }\n      d2.state.sessionPeekId = id\n      d2.state.sessionPeekRows = null\n      loadSessionDetail(id)\n    })\n    d2.on('.peek-close', 'click', () => { d2.state.peekId = null; d2.state.peekThread = null })\n    d2.on('.peek-backdrop', 'click', (e, el) => { if (e.target === el) { d2.state.peekId = null; d2.state.peekThread = null } })\n    d2.state.chartTypes = { clsTotal: 'bar', clsHourly: 'line', statusTotal: 'bar', statusHourly: 'line' }\n\n    d2.on('.applyRangeBtn', 'click', () => {\n      const s = root.querySelector('.rangeStart') && root.querySelector('.rangeStart').value\n      const en = root.querySelector('.rangeEnd') && root.querySelector('.rangeEnd').value\n      if (!s || !en) return\n      d2.state.customStart = s\n      d2.state.customEnd = en\n      d2.state.period = 'custom'\n      d2.state.data = null\n      refreshAll()\n    })\n\n    d2.on('.dchip', 'click', (e, el) => {\n      const dim = el.dataset.dim\n      const cur = { ...(d2.state.drillFilters || {}) }\n      delete cur[dim]\n      d2.state.drillFilters = cur\n      d2.state.data = null\n      refreshAll()\n    })\n\n    d2.on('.hmode-btn', 'click', (e, el) => { d2.state.hourlyMode = el.dataset.mode })\n    d2.on('.qmetric-btn', 'click', (e, el) => { d2.state.queueMetric = el.dataset.metric })\n\n    d2.on('.legend-item', 'click', (e, el) => {\n      const domain = el.dataset.domain\n      const key = el.dataset.key\n      const stateKey = domain === 'cls' ? 'hiddenCls' : domain === 'status' ? 'hiddenStatus' : 'hiddenQueue'\n      const cur = d2.state[stateKey] || {}\n      d2.state[stateKey] = { ...cur, [key]: !cur[key] }\n    })\n    d2.on('.typebtn', 'click', (e, el) => {\n      const chartKey = el.dataset.chart\n      const type = el.dataset.type\n      d2.state.chartTypes = { ...(d2.state.chartTypes || {}), [chartKey]: type }\n    })\n\n    async function refreshAll() {\n      const kpiAll = d2.data('kpiAll')\n      const queueLive = d2.data('queueLive')\n      const queueDefs = d2.data('queueDefs')\n      if (!kpiAll || !queueLive) { d2.state.data = { error: 'Fontes de dado não conectadas.' }; return }\n      try {\n        let start, end\n        if (d2.state.period === 'custom' && d2.state.customStart && d2.state.customEnd) {\n          start = new Date(`${d2.state.customStart}T00:00:00-03:00`)\n          end = new Date(`${d2.state.customEnd}T23:59:59-03:00`)\n        } else {\n          const p = PERIODS.find(x => x.key === d2.state.period) || PERIODS[0]\n          start = new Date(); start.setHours(0, 0, 0, 0); start.setDate(start.getDate() - (p.days - 1))\n          end = new Date()\n        }\n        const durationMs = end.getTime() - start.getTime()\n        const deselected = Object.keys(d2.state.deselectedQueues || {}).filter(q => d2.state.deselectedQueues[q])\n        const drill = d2.state.drillFilters || {}\n        const filterObj = { env: 'PRD', createdAt: { $gte: start.toISOString(), $lte: end.toISOString() } }\n        if (deselected.length) filterObj.queue_label = { $nin: deselected }\n        Object.assign(filterObj, buildDrillMongoFilter(drill, true))\n\n        let agentNameMap = {}\n        const agentNames = d2.data('agentNames')\n        if (agentNames) {\n          try {\n            await agentNames.refresh()\n            for (const a of (agentNames.records() || [])) { if (a && a._id) agentNameMap[a._id] = a.name || a.email || a._id }\n          } catch (e) { agentNameMap = {} }\n        }\n\n        kpiAll.sort('createdAt', 'desc')\n        kpiAll.filter(filterObj)\n        await kpiAll.refresh()\n        const recentRecords = (kpiAll.records() || []).slice(0, 25).map(r => {\n          const rawAgent = (r.agent && r.agent._id) || r.agent\n          return {\n            _id: r._id,\n            session: (r.session && r.session._id) || r.session || null,\n            createdAt: r.createdAt,\n            queue_label: r.queue_label,\n            agentName: (rawAgent && (agentNameMap[rawAgent] || (r.agent && (r.agent.name || r.agent.email)))) || null,\n            transfer_status: r.transfer_status,\n            classification: r.classification,\n            queue_time: r.queue_time,\n            agent_time: r.agent_time,\n            env: r.env,\n            cpf: r.cpf,\n          }\n        })\n        const agg = (kpiAll.raw && kpiAll.raw.aggregated) || []\n        const pick = (row, ...keys) => { for (const k of keys) { if (row && row[k] !== undefined) return row[k] }; return undefined }\n        const groupKey = (row, ...keys) => pick(row, ...keys) ?? (row && row._id && pick(row._id, ...keys)) ?? (row && row._id)\n        const hourOf = (row) => { const id = row && row._id; if (id == null) return null; if (typeof id.hour === 'number') return id.hour; if (id.createdAt && typeof id.createdAt.hour === 'number') return id.createdAt.hour; return null }\n\n        const byStatusRaw = (agg[0] && agg[0].data) || []\n        const byClassificationRaw = (agg[1] && agg[1].data) || []\n        const hourlyStatusRaw = (agg[3] && agg[3].data) || []\n        const hourlyClassificationRaw = (agg[4] && agg[4].data) || []\n        const completedSumRow = ((agg[5] && agg[5].data) || [])[0] || {}\n        const cpfGroupRaw = (agg[7] && agg[7].data) || []\n\n        const byStatus = byStatusRaw.map(r => ({ key: groupKey(r, 'transfer_status'), total: r.total })).filter(r => STATUS_META[r.key])\n        const byClassification = byClassificationRaw.map(r => ({ key: groupKey(r, 'classification'), total: r.total })).filter(r => CLS_META[r.key])\n        const hourlyByStatus = {}\n        for (const r of hourlyStatusRaw) {\n          const h = hourOf(r)\n          const s = pick(r._id, 'transfer_status')\n          if (h == null || s == null) continue\n          hourlyByStatus[h] = hourlyByStatus[h] || {}; hourlyByStatus[h][s] = (hourlyByStatus[h][s] || 0) + (r.count ?? r.total ?? 0)\n        }\n        const hourlyByClassification = {}\n        for (const r of hourlyClassificationRaw) {\n          const h = hourOf(r)\n          const c = pick(r._id, 'transfer_status', 'classification')\n          if (h == null || c == null) continue\n          hourlyByClassification[h] = hourlyByClassification[h] || {}; hourlyByClassification[h][c] = (hourlyByClassification[h][c] || 0) + (r.count ?? r.total ?? 0)\n        }\n        const completedCount = completedSumRow.total ?? 0\n        const sumQT = pick(completedSumRow, 'sum_queue_time', 'sumQT', 'sumQueueTime') ?? 0\n        const distinctCpf = cpfGroupRaw.filter(r => { const v = groupKey(r, 'cpf'); return v != null && v !== '' }).length\n\n        const { data: extra } = await kpiAll.aggregate([{ $facet: {\n          slaOk: [{ $match: { transfer_status: 'completed', queue_time: { $lt: 60 } } }, { $count: 'total' }],\n          abandonedReal: [{ $match: { transfer_status: 'abandoned', queue_time: { $gt: 5 } } }, { $count: 'total' }],\n          byQueueLabel: [{ $group: { _id: '$queue_label', total: { $sum: 1 } } }],\n          queueWaitByDayHour: [\n            { $match: { queue_time: { $exists: true }, queue_label: { $exists: true, $ne: null } } },\n            { $group: { _id: { day: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d', timezone: '-03:00' } }, hour: { $hour: { date: '$createdAt', timezone: '-03:00' } }, queue: '$queue_label' }, maxWait: { $max: '$queue_time' } } },\n          ],\n          statusByDayHour: [\n            { $group: { _id: { day: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d', timezone: '-03:00' } }, hour: { $hour: { date: '$createdAt', timezone: '-03:00' } }, status: '$transfer_status' }, count: { $sum: 1 } } },\n          ],\n          classificationByDayHour: [\n            { $match: { classification: { $exists: true, $ne: null } } },\n            { $group: { _id: { day: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d', timezone: '-03:00' } }, hour: { $hour: { date: '$createdAt', timezone: '-03:00' } }, cls: '$classification' }, count: { $sum: 1 } } },\n          ],\n          queueSlaBreachByDayHour: [\n            { $match: { queue_time: { $gt: 60 }, queue_label: { $exists: true, $ne: null } } },\n            { $group: { _id: { day: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d', timezone: '-03:00' } }, hour: { $hour: { date: '$createdAt', timezone: '-03:00' } }, queue: '$queue_label' }, count: { $sum: 1 } } },\n          ],\n          abandonCurve: [\n            { $match: { transfer_status: 'abandoned', queue_time: { $exists: true, $ne: null } } },\n            { $bucket: { groupBy: '$queue_time', boundaries: [0, 10, 30, 60, 120, 300, 1000000], default: '5min+', output: { count: { $sum: 1 } } } },\n          ],\n          byAgent: [\n            { $match: { agent: { $exists: true, $ne: null } } },\n            { $group: { _id: '$agent', total: { $sum: 1 }, avgHandleTime: { $avg: '$agent_time' } } },\n            { $sort: { total: -1 } },\n            { $limit: 10 },\n          ],\n          bySession: [\n            { $match: { session: { $exists: true, $ne: null } } },\n            { $group: { _id: '$session', total: { $sum: 1 }, wasted: { $sum: { $cond: [{ $and: [{ $eq: ['$classification', 'ended_by_user'] }, { $eq: ['$agent_time', 0] }] }, 1, 0] } }, queues: { $addToSet: '$queue_label' }, agents: { $addToSet: '$agent_name' }, firstCreatedAt: { $min: '$createdAt' }, lastCreatedAt: { $max: '$createdAt' } } },\n            { $match: { total: { $gt: 1 } } },\n            { $sort: { total: -1 } },\n            { $limit: 15 },\n          ],\n        } }])\n        const ex = extra[0] || {}\n        const slaOkCount = (ex.slaOk && ex.slaOk[0] && ex.slaOk[0].total) || 0\n        const abandonedRealCount = (ex.abandonedReal && ex.abandonedReal[0] && ex.abandonedReal[0].total) || 0\n        const allQueues = (ex.byQueueLabel || []).map(r => r._id).filter(Boolean)\n        const waitByQueueHour = {}\n        const waitByQueueDayHour = {}\n        const dayHourSet = {}\n        for (const r of (ex.queueWaitByDayHour || [])) {\n          const day = r._id && r._id.day\n          const h = r._id && r._id.hour\n          const q = r._id && r._id.queue\n          if (day == null || h == null || !q) continue\n          const val = r.maxWait || 0\n          waitByQueueHour[q] = waitByQueueHour[q] || {}\n          waitByQueueHour[q][h] = Math.max(waitByQueueHour[q][h] || 0, val)\n          const dhKey = `${day} ${String(h).padStart(2,'0')}h`\n          waitByQueueDayHour[q] = waitByQueueDayHour[q] || {}\n          waitByQueueDayHour[q][dhKey] = val\n          dayHourSet[dhKey] = true\n        }\n        const hourlyByStatusDayHour = {}\n        for (const r of (ex.statusByDayHour || [])) {\n          const day = r._id && r._id.day\n          const h = r._id && r._id.hour\n          const s = r._id && r._id.status\n          if (day == null || h == null || !s) continue\n          const dhKey = `${day} ${String(h).padStart(2,'0')}h`\n          hourlyByStatusDayHour[s] = hourlyByStatusDayHour[s] || {}\n          hourlyByStatusDayHour[s][dhKey] = (hourlyByStatusDayHour[s][dhKey] || 0) + (r.count || 0)\n          dayHourSet[dhKey] = true\n        }\n        const hourlyByClassificationDayHour = {}\n        for (const r of (ex.classificationByDayHour || [])) {\n          const day = r._id && r._id.day\n          const h = r._id && r._id.hour\n          const c = r._id && r._id.cls\n          if (day == null || h == null || !c) continue\n          const dhKey = `${day} ${String(h).padStart(2,'0')}h`\n          hourlyByClassificationDayHour[c] = hourlyByClassificationDayHour[c] || {}\n          hourlyByClassificationDayHour[c][dhKey] = (hourlyByClassificationDayHour[c][dhKey] || 0) + (r.count || 0)\n          dayHourSet[dhKey] = true\n        }\n        const slaBreachByQueueHour = {}\n        const slaBreachByQueueDayHour = {}\n        for (const r of (ex.queueSlaBreachByDayHour || [])) {\n          const day = r._id && r._id.day\n          const h = r._id && r._id.hour\n          const q = r._id && r._id.queue\n          if (day == null || h == null || !q) continue\n          const val = r.count || 0\n          slaBreachByQueueHour[q] = slaBreachByQueueHour[q] || {}\n          slaBreachByQueueHour[q][h] = (slaBreachByQueueHour[q][h] || 0) + val\n          const dhKey = `${day} ${String(h).padStart(2,'0')}h`\n          slaBreachByQueueDayHour[q] = slaBreachByQueueDayHour[q] || {}\n          slaBreachByQueueDayHour[q][dhKey] = val\n          dayHourSet[dhKey] = true\n        }\n        const dayHourLabels = Object.keys(dayHourSet).sort()\n        const ABANDON_BUCKETS = [\n          { id: 0, label: '0–10s', range: { $gte: 0, $lt: 10 } }, { id: 10, label: '10–30s', range: { $gte: 10, $lt: 30 } }, { id: 30, label: '30–60s', range: { $gte: 30, $lt: 60 } },\n          { id: 60, label: '1–2min', range: { $gte: 60, $lt: 120 } }, { id: 120, label: '2–5min', range: { $gte: 120, $lt: 300 } }, { id: '5min+', label: '5min+', range: { $gte: 300 } },\n        ]\n        const abandonCurveRaw = ex.abandonCurve || []\n        const abandonCurveCountFor = (id) => { const row = abandonCurveRaw.find(r => r._id === id); return (row && row.count) || 0 }\n        const abandonCurveTotal = abandonCurveRaw.reduce((s, r) => s + (r.count || 0), 0)\n        let abandonCumulative = 0\n        const abandonCurve = ABANDON_BUCKETS.map(b => {\n          const count = abandonCurveCountFor(b.id)\n          abandonCumulative += count\n          return { label: b.label, count, range: b.range, cumulativePct: abandonCurveTotal > 0 ? Math.round((abandonCumulative / abandonCurveTotal) * 1000) / 10 : 0 }\n        })\n        const total = byStatus.reduce((s, r) => s + r.total, 0)\n        const abandonoBase = abandonedRealCount + completedCount\n\n        const topOperators = (ex.byAgent || []).map(r => ({\n          agentId: r._id,\n          name: agentNameMap[r._id] || (r._id ? String(r._id).slice(-6) : 'Desconhecido'),\n          total: r.total,\n          avgHandleTime: r.avgHandleTime || 0,\n        }))\n\n        const bouncingSessions = (ex.bySession || []).map(r => ({\n          sessionId: r._id,\n          total: r.total,\n          wasted: r.wasted || 0,\n          queues: (r.queues || []).filter(Boolean),\n          agents: (r.agents || []).filter(Boolean),\n          firstCreatedAt: r.firstCreatedAt,\n          lastCreatedAt: r.lastCreatedAt,\n        }))\n\n        let prevStats = null\n        const kpiPrev = d2.data('kpiPrev')\n        if (kpiPrev) {\n          try {\n            const prevStart = new Date(start.getTime() - durationMs)\n            const prevFilterObj = { env: 'PRD', createdAt: { $gte: prevStart.toISOString(), $lt: start.toISOString() } }\n            if (deselected.length) prevFilterObj.queue_label = { $nin: deselected }\n            Object.assign(prevFilterObj, buildDrillMongoFilter(drill, false))\n            kpiPrev.filter(prevFilterObj)\n            await kpiPrev.refresh()\n            const pAgg = (kpiPrev.raw && kpiPrev.raw.aggregated) || []\n            const pByStatusRaw = (pAgg[0] && pAgg[0].data) || []\n            const pCompletedSumRow = ((pAgg[5] && pAgg[5].data) || [])[0] || {}\n            const pTotal = pByStatusRaw.reduce((s, r) => s + (r.total || 0), 0)\n            const pCompletedCount = pCompletedSumRow.total ?? 0\n            const pSumQT = pick(pCompletedSumRow, 'sum_queue_time', 'sumQT', 'sumQueueTime') ?? 0\n            const { data: pExtra } = await kpiPrev.aggregate([{ $facet: {\n              slaOk: [{ $match: { transfer_status: 'completed', queue_time: { $lt: 60 } } }, { $count: 'total' }],\n              abandonedReal: [{ $match: { transfer_status: 'abandoned', queue_time: { $gt: 5 } } }, { $count: 'total' }],\n            } }])\n            const pex = pExtra[0] || {}\n            const pSlaOkCount = (pex.slaOk && pex.slaOk[0] && pex.slaOk[0].total) || 0\n            const pAbandonedRealCount = (pex.abandonedReal && pex.abandonedReal[0] && pex.abandonedReal[0].total) || 0\n            const pAbandonoBase = pAbandonedRealCount + pCompletedCount\n            prevStats = {\n              total: pTotal,\n              nivelServico: pCompletedCount > 0 ? Math.round((pSlaOkCount / pCompletedCount) * 100) : null,\n              abandonoPct: pAbandonoBase > 0 ? Math.round((pAbandonedRealCount / pAbandonoBase) * 1000) / 10 : null,\n              tme: pCompletedCount > 0 ? pSumQT / pCompletedCount : null,\n            }\n          } catch (e) { prevStats = null }\n        }\n\n        let queueLimits = {}\n        if (queueDefs) {\n          try {\n            await queueDefs.refresh()\n            const defs = queueDefs.records() || []\n            for (const d of defs) {\n              const key = d.label || d.name\n              if (key != null && d.queue_limit != null) queueLimits[key] = d.queue_limit\n            }\n          } catch (e) { queueLimits = {} }\n        }\n\n        const queueLiveFilter = { env: 'PRD' }\n        if (deselected.length) queueLiveFilter.queue_label = { $nin: deselected }\n        queueLive.filter(queueLiveFilter)\n        await queueLive.refresh()\n        const qRows = await queueLive.loadAll()\n        const waits = (Array.isArray(qRows) ? qRows : []).map(r => r.queued_at ? (Date.now() - new Date(r.queued_at).getTime()) / 1000 : 0)\n        const liveQMap = {}\n        for (const r of (Array.isArray(qRows) ? qRows : [])) { const ql = r.queue_label || 'Sem fila'; liveQMap[ql] = (liveQMap[ql] || 0) + 1 }\n\n        d2.state.data = {\n          total,\n          consumidores: distinctCpf,\n          completedCount, slaOkCount,\n          nivelServico: completedCount > 0 ? Math.round((slaOkCount / completedCount) * 100) : 0,\n          abandonedRealCount, abandonoBase,\n          abandonoPct: abandonoBase > 0 ? Math.round((abandonedRealCount / abandonoBase) * 1000) / 10 : 0,\n          tme: completedCount > 0 ? sumQT / completedCount : 0,\n          byStatus, byClassification, hourlyByStatus, hourlyByClassification,\n          hourlyByStatusDayHour, hourlyByClassificationDayHour, waitByQueueDayHour, dayHourLabels,\n          allQueues, waitByQueueHour, slaBreachByQueueHour, slaBreachByQueueDayHour, abandonCurve, prevStats, recentRecords, topOperators, bouncingSessions, queueLimits,\n          queueLiveCount: (Array.isArray(qRows) ? qRows.length : 0),\n          queueAvgWait: waits.length ? waits.reduce((a,b) => a+b, 0) / waits.length : 0,\n          queueMaxWait: waits.length ? Math.max(...waits) : 0,\n          liveQueueByLabel: Object.entries(liveQMap).map(([label, count]) => ({ label, count })),\n          hours: Array.from({ length: 24 }, (_, i) => i),\n        }\n      } catch (e) {\n        d2.state.data = { error: 'Erro ao carregar: ' + (e && e.message || String(e)) }\n      }\n    }\n\n    async function loadSessionDetail(sessionId) {\n      const kpiAll = d2.data('kpiAll')\n      if (!kpiAll) { d2.state.sessionPeekRows = []; return }\n      try {\n        const { data } = await kpiAll.aggregate([\n          { $match: { session: sessionId } },\n          { $sort: { createdAt: 1 } },\n          { $project: { createdAt: 1, agent_name: 1, transfer_status: 1, classification: 1, queue_time: 1, agent_time: 1 } },\n        ])\n        if (d2.state.sessionPeekId === sessionId) d2.state.sessionPeekRows = data || []\n      } catch (e) {\n        if (d2.state.sessionPeekId === sessionId) d2.state.sessionPeekRows = []\n      }\n    }\n\n    async function loadPeekThread(sessionId) {\n      const chatMessages = d2.data('chatMessages')\n      if (!chatMessages) { d2.state.peekThread = { error: 'Fonte de mensagens não conectada.' }; return }\n      try {\n        chatMessages.filter({ session: sessionId })\n        chatMessages.sort('time', 'asc')\n        await chatMessages.refresh()\n        const raw = await chatMessages.loadAll()\n        const messages = (Array.isArray(raw) ? raw : []).map(m => {\n          const who = m.sender === 'user' ? 'user' : ((m.sender === 'agent' || m.agent_name) ? 'agent' : 'bot')\n          const label = who === 'user' ? 'Cliente' : (who === 'agent' ? (m.agent_name || 'Operador') : 'Assistente')\n          return { who, label, time: m.time, text: extractMsgText(m) }\n        })\n        if (d2.state.peekId) d2.state.peekThread = { messages }\n      } catch (e) {\n        if (d2.state.peekId) d2.state.peekThread = { error: 'Erro ao carregar conversa: ' + (e && e.message || String(e)) }\n      }\n    }\n\n    async function refreshEngine() {\n      try {\n        const res = await d2.rush('distEngine').invoke({})\n        const rows = (res && res.data) || []\n        const success = rows.filter(r => r.result === 'success')\n        const durations = success.map(r => (new Date(r.updatedAt).getTime() - new Date(r.createdAt).getTime()) / 1000)\n        const avgDuration = durations.length ? durations.reduce((a,b) => a+b, 0) / durations.length : null\n        const lastFinished = success.length ? new Date(success[0].updatedAt) : null\n        d2.state.engine = {\n          running: rows.length > 0 && rows[0].result === 'running',\n          avgDuration, lastFinishedAt: lastFinished,\n          recent: rows.slice(0, 12).map(r => ({ running: r.result === 'running', duration: (new Date(r.updatedAt).getTime() - new Date(r.createdAt).getTime()) / 1000 })),\n        }\n      } catch (e) { /* engine health is best-effort, never block the dashboard */ }\n    }\n\n    d2.on('.pbtn', 'click', (e, el) => { d2.state.period = el.dataset.period; d2.state.customStart = ''; d2.state.customEnd = ''; d2.state.data = null; refreshAll() })\n    d2.on('.qchip', 'click', (e, el) => {\n      const q = el.dataset.queue\n      const cur = d2.state.deselectedQueues || {}\n      d2.state.deselectedQueues = { ...cur, [q]: !cur[q] }\n      d2.state.data = null\n      refreshAll()\n    })\n\n    root._refreshAll = refreshAll\n    refreshAll()\n    refreshEngine()\n    const engineId = setInterval(refreshEngine, 10000)\n    d2.onUnmount(() => { clearInterval(engineId) })\n  },\n  updated(d2, root) {\n    try {\n    const Chart = resolveChart(d2.lib(CHART_LIB))\n    const k = d2.state.data\n    if (!Chart || !k || k.error) return\n    root._lastPaintedK = k\n\n    const hiddenCls = d2.state.hiddenCls || {}\n    const hiddenStatus = d2.state.hiddenStatus || {}\n    const chartTypes = d2.state.chartTypes || {}\n    const niceMax = (arr) => { const m = Math.max(0, ...arr); return m === 0 ? 1 : Math.ceil(m * 1.15) }\n    const hourlyMode = d2.state.hourlyMode || 'hour'\n    const queueMetric = d2.state.queueMetric || 'wait'\n    const shortDayHour = (dhKey) => { const sp = dhKey.indexOf(' '); const day = dhKey.slice(0, sp); const hourPart = dhKey.slice(sp + 1); const parts = day.split('-'); return `${parts[2]}/${parts[1]} ${hourPart}` }\n    const xLabels = hourlyMode === 'dayhour' ? (k.dayHourLabels || []).map(shortDayHour) : k.hours.map(h => h + 'h')\n    const xTicksLimit = hourlyMode === 'dayhour' ? 12 : 8\n    const seriesValues = (hourMap, dayHourMap, key, hourFirst) => hourlyMode === 'dayhour'\n      ? (k.dayHourLabels || []).map(lbl => (dayHourMap[key] && dayHourMap[key][lbl]) || 0)\n      : k.hours.map(h => (hourFirst ? (hourMap[h] && hourMap[h][key]) : (hourMap[key] && hourMap[key][h])) || 0)\n\n    const setDrillFilter = (dim, value, label) => {\n      d2.state.drillFilters = { ...(d2.state.drillFilters || {}), [dim]: { value, label } }\n      d2.state.data = null\n      if (root._refreshAll) root._refreshAll()\n    }\n    const dayHourRange = (dhKey) => {\n      const sp = dhKey.indexOf(' ')\n      const day = dhKey.slice(0, sp)\n      const hour = parseInt(dhKey.slice(sp + 1), 10)\n      const start = `${day}T${String(hour).padStart(2, '0')}:00:00-03:00`\n      if (hour < 23) return { start, end: `${day}T${String(hour + 1).padStart(2, '0')}:00:00-03:00` }\n      const nextDay = new Date(`${day}T00:00:00-03:00`); nextDay.setDate(nextDay.getDate() + 1)\n      return { start, end: nextDay.toISOString() }\n    }\n    const hourClickFilter = (idx) => {\n      // Hour-of-day (collapsed across days) can't be expressed as a plain .filter() query (no $expr support) — only dayhour mode drills.\n      if (hourlyMode !== 'dayhour') return\n      const dhKey = (k.dayHourLabels || [])[idx]; if (!dhKey) return\n      setDrillFilter('dayHour', dayHourRange(dhKey), xLabels[idx])\n    }\n\n    const clsKeys = Object.keys(CLS_META).filter(key => !hiddenCls[key])\n    const clsItems = k.byClassification.filter(c => !hiddenCls[c.key]).map(c => ({ key: c.key, total: c.total, meta: CLS_META[c.key] || { label: c.key, color: GRAY } }))\n    upsertChart(root, Chart, 'clsBar', '#clsBar', {\n      type: chartTypes.clsTotal || 'bar',\n      data: { labels: clsItems.map(c => c.meta.label), datasets: [{ data: clsItems.map(c => c.total), backgroundColor: clsItems.map(c => c.meta.color), borderRadius: 4, maxBarThickness: 56 }] },\n      options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } }, plugins: { legend: { display: false } }, scales: { x: { ticks: { display: false } }, y: { beginAtZero: true, suggestedMax: niceMax(clsItems.map(c => c.total)) } },\n        onClick: (evt, elements) => { if (!elements.length) return; const item = clsItems[elements[0].index]; if (item) setDrillFilter('classification', item.key, item.meta.label) } },\n    })\n    upsertChart(root, Chart, 'clsLine', '#clsLine', {\n      type: chartTypes.clsHourly || 'line',\n      data: { labels: xLabels, datasets: clsKeys.map(key => ({ label: CLS_META[key].label, data: seriesValues(k.hourlyByClassification, k.hourlyByClassificationDayHour, key, true), borderColor: CLS_META[key].color, backgroundColor: CLS_META[key].color, borderWidth: 2, pointRadius: 4, pointHoverRadius: 6, tension: 0 })) },\n      options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } }, plugins: { legend: { display: false } }, scales: { x: { ticks: { maxTicksLimit: xTicksLimit } }, y: { beginAtZero: true, suggestedMax: niceMax(clsKeys.flatMap(key => seriesValues(k.hourlyByClassification, k.hourlyByClassificationDayHour, key, true))) } },\n        interaction: { mode: 'index', intersect: false },\n        onClick: (evt, elements) => { if (!elements.length) return; hourClickFilter(elements[0].index) } },\n    })\n\n    const statusKeys = Object.keys(STATUS_META).filter(key => !hiddenStatus[key])\n    const statusItems = k.byStatus.filter(c => !hiddenStatus[c.key]).map(c => ({ key: c.key, total: c.total, meta: STATUS_META[c.key] || { label: c.key, color: GRAY } }))\n    upsertChart(root, Chart, 'statusBar', '#statusBar', {\n      type: chartTypes.statusTotal || 'bar',\n      data: { labels: statusItems.map(c => c.meta.label), datasets: [{ data: statusItems.map(c => c.total), backgroundColor: statusItems.map(c => c.meta.color), borderRadius: 4, maxBarThickness: 56 }] },\n      options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } }, plugins: { legend: { display: false } }, scales: { x: { ticks: { display: false } }, y: { beginAtZero: true, suggestedMax: niceMax(statusItems.map(c => c.total)) } },\n        onClick: (evt, elements) => { if (!elements.length) return; const item = statusItems[elements[0].index]; if (item) setDrillFilter('status', item.key, item.meta.label) } },\n    })\n    upsertChart(root, Chart, 'statusLine', '#statusLine', {\n      type: chartTypes.statusHourly || 'line',\n      data: { labels: xLabels, datasets: statusKeys.map(key => ({ label: STATUS_META[key].label, data: seriesValues(k.hourlyByStatus, k.hourlyByStatusDayHour, key, true), borderColor: STATUS_META[key].color, backgroundColor: STATUS_META[key].color, borderWidth: 2, pointRadius: 4, pointHoverRadius: 6, tension: 0 })) },\n      options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } }, plugins: { legend: { display: false } }, scales: { x: { ticks: { maxTicksLimit: xTicksLimit } }, y: { beginAtZero: true, suggestedMax: niceMax(statusKeys.flatMap(key => seriesValues(k.hourlyByStatus, k.hourlyByStatusDayHour, key, true))) } },\n        interaction: { mode: 'index', intersect: false },\n        onClick: (evt, elements) => { if (!elements.length) return; hourClickFilter(elements[0].index) } },\n    })\n\n    const hiddenQueue = d2.state.hiddenQueue || {}\n    const queueListSorted = [...(k.allQueues || [])].sort()\n    const queueListVisible = queueListSorted.filter(q => !hiddenQueue[q])\n    const qHourMap = queueMetric === 'volume' ? (k.slaBreachByQueueHour || {}) : (k.waitByQueueHour || {})\n    const qDayHourMap = queueMetric === 'volume' ? (k.slaBreachByQueueDayHour || {}) : (k.waitByQueueDayHour || {})\n    const qFormat = queueMetric === 'volume' ? (v) => String(v) : (v) => fmtHMS(v)\n    upsertChart(root, Chart, 'queueWaitLine', '#queueWaitLine', {\n      type: 'line',\n      data: {\n        labels: xLabels,\n        datasets: queueListVisible.map(q => {\n          const color = QUEUE_PALETTE[queueListSorted.indexOf(q) % QUEUE_PALETTE.length]\n          return { label: q, data: seriesValues(qHourMap, qDayHourMap, q, false), borderColor: color, backgroundColor: color, borderWidth: 2, pointRadius: 4, pointHoverRadius: 6, tension: 0 }\n        }),\n      },\n      options: {\n        responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } },\n        interaction: { mode: 'index', intersect: false },\n        plugins: { legend: { display: false }, tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${qFormat(ctx.parsed.y)}` } } },\n        scales: {\n          x: { ticks: { maxTicksLimit: xTicksLimit } },\n          y: { beginAtZero: true, suggestedMax: niceMax(queueListVisible.flatMap(q => seriesValues(qHourMap, qDayHourMap, q, false))), ticks: { callback: (v) => qFormat(v) } },\n        },\n        onClick: (evt, elements) => { if (!elements.length) return; hourClickFilter(elements[0].index) },\n      },\n    })\n\n    const abandonCurve = k.abandonCurve || []\n    upsertChart(root, Chart, 'abandonCurve', '#abandonCurve', {\n      type: 'bar',\n      data: {\n        labels: abandonCurve.map(b => b.label),\n        datasets: [\n          { type: 'bar', label: 'Abandonos', data: abandonCurve.map(b => b.count), backgroundColor: RED, borderRadius: 4, maxBarThickness: 56, yAxisID: 'y' },\n          { type: 'line', label: '% acumulado', data: abandonCurve.map(b => b.cumulativePct), borderColor: DARK, backgroundColor: DARK, borderWidth: 2, pointRadius: 4, pointHoverRadius: 6, tension: 0, yAxisID: 'y2' },\n        ],\n      },\n      options: {\n        responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } },\n        plugins: { legend: { display: true, position: 'bottom' }, tooltip: { callbacks: { label: (ctx) => ctx.dataset.yAxisID === 'y2' ? `${ctx.dataset.label}: ${ctx.parsed.y}%` : `${ctx.dataset.label}: ${ctx.parsed.y}` } } },\n        scales: {\n          y: { type: 'linear', position: 'left', beginAtZero: true, suggestedMax: niceMax(abandonCurve.map(b => b.count)) },\n          y2: { type: 'linear', position: 'right', beginAtZero: true, max: 100, grid: { drawOnChartArea: false }, ticks: { callback: (v) => v + '%' } },\n        },\n        onClick: (evt, elements) => { if (!elements.length) return; const b = abandonCurve[elements[0].index]; if (b) setDrillFilter('abandonBucket', b.range, b.label) },\n      },\n    })\n    } catch (e) { /* fail-silent: a chart render error should never break the rest of the dashboard */ }\n  },\n}"}},"agentNames":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentNames"}}}},"distEngine":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"distEngine"}}}},"kpiAll":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"kpiAll"}}}},"kpiPrev":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"kpiPrev"}}}},"queueDefs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"queueDefs"}}}},"queueLive":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"queueLive"}}}},"chatMessages":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"chatMessages"}}}}}}}}}},{"_id":"6a8854a4de3b5a1eeeaf730b","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"xU0nF1iMuMjOIq3ZMDdOe","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;') }\n\nconst GREEN = '#3ddc5b', ORANGE = 'rgba(244,134,70,1)', RED = 'rgba(229,0,75,1)', DARK = 'rgba(54,54,54,1)', GRAY = 'rgba(150,150,150,1)'\nconst CHART_LIB = 'chart.js@4.4.1/auto'\nconst HB_OK_MAX = 30\nconst HB_WARN_MAX = 180\nconst ALLOC_OK_MAX = 0\nconst ALLOC_WARN_MAX = 1\nconst BOT_OK_MAX = 3000\nconst BOT_WARN_MAX = 30000\nconst HK_OK_MAX = 2000\nconst HK_WARN_MAX = 8000\nconst SYNC_OK_MAX = 40 * 60\nconst SYNC_WARN_MAX = 90 * 60\nconst PING_OK_MAX = 0\nconst PING_WARN_MAX = 1\nconst WINDOW_OPTIONS = [{ min: 15, label: '15min' }, { min: 30, label: '30min' }, { min: 60, label: '1h' }, { min: 180, label: '3h' }]\nconst SYNC_WINDOW_OPTIONS = [{ min: 180, label: '3h' }, { min: 360, label: '6h' }, { min: 720, label: '12h' }, { min: 1440, label: '24h' }]\nconst BUCKET_MIN_BY_WINDOW = { 15: 2, 30: 3, 60: 5, 180: 10 }\nconst BOT_BUCKET_MIN_BY_WINDOW = { 15: 1, 30: 1, 60: 2, 180: 5 }\nconst HK_BUCKET_MIN_BY_WINDOW = { 15: 1, 30: 1, 60: 1, 180: 2 }\nconst PING_BUCKET_MIN_BY_WINDOW = { 15: 1, 30: 1, 60: 1, 180: 2 }\nconst DEFAULT_WINDOW_MIN = 60\nconst DEFAULT_SYNC_WINDOW_MIN = 720\nconst DAILY_STATUS_DAYS = 14\n\nfunction resolveChart(lib) {\n  if (!lib) return null\n  if (typeof lib === 'function') return lib\n  return lib.Chart || lib.default || null\n}\n\nfunction upsertChart(root, Chart, key, canvasSel, config) {\n  const canvas = root.querySelector(canvasSel)\n  if (!canvas) return\n  const existing = (typeof Chart.getChart === 'function') ? Chart.getChart(canvas) : null\n  if (existing && existing.config.type === config.type) {\n    existing.data = config.data; existing.options = config.options\n    existing.resize()\n    existing.update('none')\n  } else {\n    if (existing) { try { existing.destroy() } catch (e) {} }\n    config.options = Object.assign({ animation: false }, config.options)\n    const chart = new Chart(canvas, config)\n    chart.resize()\n  }\n}\n\nfunction hbStatusOf(sec) {\n  if (sec == null || Number.isNaN(sec)) return 'unknown'\n  if (sec <= HB_OK_MAX) return 'ok'\n  if (sec <= HB_WARN_MAX) return 'warn'\n  return 'crit'\n}\nfunction allocStatusOf(ratePct) {\n  if (ratePct == null || Number.isNaN(ratePct)) return 'unknown'\n  if (ratePct <= ALLOC_OK_MAX) return 'ok'\n  if (ratePct <= ALLOC_WARN_MAX) return 'warn'\n  return 'crit'\n}\nfunction botStatusOf(ms) {\n  if (ms == null || Number.isNaN(ms)) return 'unknown'\n  if (ms <= BOT_OK_MAX) return 'ok'\n  if (ms <= BOT_WARN_MAX) return 'warn'\n  return 'crit'\n}\nfunction hkStatusOf(ms) {\n  if (ms == null || Number.isNaN(ms)) return 'unknown'\n  if (ms <= HK_OK_MAX) return 'ok'\n  if (ms <= HK_WARN_MAX) return 'warn'\n  return 'crit'\n}\nfunction syncStatusOf(sec) {\n  if (sec == null || Number.isNaN(sec)) return 'unknown'\n  if (sec <= SYNC_OK_MAX) return 'ok'\n  if (sec <= SYNC_WARN_MAX) return 'warn'\n  return 'crit'\n}\nfunction pingStatusOf(ratePct) {\n  if (ratePct == null || Number.isNaN(ratePct)) return 'unknown'\n  if (ratePct <= PING_OK_MAX) return 'ok'\n  if (ratePct <= PING_WARN_MAX) return 'warn'\n  return 'crit'\n}\nfunction colorOf(status) {\n  return status === 'ok' ? GREEN : status === 'warn' ? ORANGE : status === 'crit' ? RED : GRAY\n}\nfunction labelFor(status) {\n  return status === 'ok' ? 'SAUDÁVEL' : status === 'warn' ? 'ATENÇÃO' : status === 'crit' ? 'CRÍTICO' : 'SEM DADOS'\n}\nfunction fmtSecs(s) {\n  if (s == null || Number.isNaN(s)) return '--'\n  if (s < 60) return `${s.toFixed(1)}s`\n  const m = Math.floor(s / 60), r = Math.round(s % 60)\n  return `${m}m${String(r).padStart(2, '0')}s`\n}\nfunction fmtMs(ms) {\n  if (ms == null || Number.isNaN(ms)) return '--'\n  if (ms < 1000) return `${Math.round(ms)}ms`\n  return `${(ms / 1000).toFixed(1)}s`\n}\nfunction fmtAge(s) {\n  if (s == null || Number.isNaN(s)) return '--'\n  if (s < 60) return `${Math.round(s)}s atrás`\n  const m = Math.floor(s / 60)\n  if (m < 60) return `${m}min atrás`\n  const h = Math.floor(m / 60)\n  return `${h}h${String(m % 60).padStart(2, '0')} atrás`\n}\nfunction fmtMinFromSec(s) {\n  if (s == null || Number.isNaN(s)) return '--'\n  const m = s / 60\n  if (m < 60) return `${m.toFixed(1)}min`\n  const h = Math.floor(m / 60)\n  return `${h}h${String(Math.round(m % 60)).padStart(2, '0')}`\n}\nfunction fmtClock(iso) {\n  if (!iso) return '--'\n  return new Date(iso).toLocaleTimeString('pt-BR')\n}\nfunction fmtDateTime(iso) {\n  if (!iso) return '--'\n  return new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })\n}\nfunction fmtPct(p) {\n  if (p == null || Number.isNaN(p)) return '--'\n  if (p > 0 && p < 0.1) return '<0.1%'\n  return `${p.toFixed(1)}%`\n}\n\nfunction deriveHeartbeatWindow(fullPoints, windowMin) {\n  if (!fullPoints || !fullPoints.length) return { points: [], count: 0, mean: null, max: null, maxAt: null, windowStart: null, windowEnd: null }\n  const cutoff = Date.now() - windowMin * 60 * 1000\n  const points = fullPoints.filter(p => new Date(p.t).getTime() >= cutoff)\n  const deltas = points.map(p => p.delta)\n  const mean = deltas.length ? deltas.reduce((a, b) => a + b, 0) / deltas.length : null\n  const max = deltas.length ? Math.max(...deltas) : null\n  const maxAt = max != null ? points[deltas.indexOf(max)].t : null\n  return {\n    points, count: points.length, mean, max, maxAt,\n    windowStart: points.length ? points[0].t : null,\n    windowEnd: points.length ? points[points.length - 1].t : null,\n  }\n}\n\nfunction deriveAllocWindow(fullRows, windowMin, bucketMinByWindow) {\n  if (!fullRows || !fullRows.length) return { buckets: [], count: 0, errors: 0, overallRatePct: null, recentRatePct: null, lastErrorAt: null, windowStart: null, windowEnd: null }\n  const cutoff = Date.now() - windowMin * 60 * 1000\n  const rows = fullRows.filter(r => new Date(r.createdAt).getTime() >= cutoff)\n  const bucketMs = ((bucketMinByWindow || BUCKET_MIN_BY_WINDOW)[windowMin] || 15) * 60 * 1000\n\n  const bucketMap = new Map()\n  for (const r of rows) {\n    const t = new Date(r.createdAt).getTime()\n    const bKey = Math.floor(t / bucketMs) * bucketMs\n    const b = bucketMap.get(bKey) || { t: bKey, total: 0, errors: 0 }\n    b.total += 1\n    if (r.result === 'error') b.errors += 1\n    bucketMap.set(bKey, b)\n  }\n  const buckets = [...bucketMap.values()]\n    .sort((a, b) => a.t - b.t)\n    .map(b => ({ ...b, success: b.total - b.errors, ratePct: b.total ? (b.errors / b.total) * 100 : 0 }))\n\n  const count = rows.length\n  const errors = rows.filter(r => r.result === 'error').length\n  const overallRatePct = count ? (errors / count) * 100 : null\n  const lastErrorRec = [...rows].reverse().find(r => r.result === 'error')\n  const lastErrorAt = lastErrorRec ? lastErrorRec.createdAt : null\n  const lastBucket = buckets.length ? buckets[buckets.length - 1] : null\n  const recentRatePct = lastBucket ? lastBucket.ratePct : overallRatePct\n\n  return {\n    buckets, count, errors, overallRatePct, recentRatePct, lastErrorAt,\n    windowStart: rows.length ? rows[0].createdAt : null,\n    windowEnd: rows.length ? rows[rows.length - 1].createdAt : null,\n  }\n}\n\nfunction deriveDurationWindow(fullRows, windowMin, bucketMinByWindow) {\n  if (!fullRows || !fullRows.length) return { buckets: [], count: 0, errors: 0, mean: null, max: null, maxAt: null, lastAt: null, lastAgeSec: null, windowStart: null, windowEnd: null }\n  const cutoff = Date.now() - windowMin * 60 * 1000\n  const rows = fullRows.filter(r => new Date(r.createdAt).getTime() >= cutoff)\n  const bucketMs = ((bucketMinByWindow || BOT_BUCKET_MIN_BY_WINDOW)[windowMin] || 5) * 60 * 1000\n\n  const bucketMap = new Map()\n  for (const r of rows) {\n    const t = new Date(r.createdAt).getTime()\n    const bKey = Math.floor(t / bucketMs) * bucketMs\n    const b = bucketMap.get(bKey) || { t: bKey, total: 0, sumMs: 0, maxMs: 0 }\n    b.total += 1\n    b.sumMs += (r.durationInMs || 0)\n    b.maxMs = Math.max(b.maxMs, r.durationInMs || 0)\n    bucketMap.set(bKey, b)\n  }\n  const buckets = [...bucketMap.values()]\n    .sort((a, b) => a.t - b.t)\n    .map(b => ({ ...b, avgMs: b.total ? b.sumMs / b.total : 0 }))\n\n  const durations = rows.map(r => r.durationInMs).filter(v => typeof v === 'number')\n  const mean = durations.length ? durations.reduce((a, b) => a + b, 0) / durations.length : null\n  const max = durations.length ? Math.max(...durations) : null\n  const maxRow = max != null ? rows.find(r => r.durationInMs === max) : null\n  const errors = rows.filter(r => r.result === 'error').length\n  const last = rows[rows.length - 1]\n\n  return {\n    buckets, count: rows.length, errors, mean, max,\n    maxAt: maxRow ? maxRow.createdAt : null,\n    lastAt: last ? last.createdAt : null,\n    lastAgeSec: last ? (Date.now() - new Date(last.createdAt).getTime()) / 1000 : null,\n    windowStart: rows.length ? rows[0].createdAt : null,\n    windowEnd: rows.length ? rows[rows.length - 1].createdAt : null,\n  }\n}\n\nasync function fetchRecentRows(d2, paramName, limit, extraFields) {\n  const ds = d2.data(paramName)\n  await ds.refresh()\n  let rows = []\n  try {\n    const agg = await ds.aggregate([\n      { $sort: { createdAt: -1 } },\n      { $limit: limit },\n      { $project: Object.assign({ _id: 0, createdAt: 1, result: 1 }, extraFields || {}) },\n    ])\n    rows = Array.isArray(agg) ? agg : (agg && (agg.records || agg.data || agg.rows)) || []\n  } catch (aggErr) {\n    rows = ds.records() || []\n  }\n  return [...rows]\n    .filter(r => r && r.createdAt)\n    .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))\n}\n\nexport default {\n  css: `\n    .wrap { font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif; display:flex; flex-direction:column; gap:14px; padding:4px; box-sizing:border-box; color:#1a1a1a; }\n    .panel { background:#fff; border:1px solid rgba(0,0,0,0.08); border-radius:12px; padding:20px 24px; }\n    .header-panel { padding:16px 24px; }\n    .header-panel h2 { margin:0; font-size:18px; font-weight:800; }\n    .header-panel .sub { font-size:12.5px; color:#8a8a8a; margin-top:6px; max-width:920px; }\n    .refresh-btn { padding:6px 14px; border-radius:8px; border:1px solid rgba(0,0,0,0.12); background:#fff; font-size:12px; font-weight:700; color:#3a3a3a; cursor:pointer; }\n    .refresh-btn:hover { background:#f7f7f5; }\n    .panel-hd { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:14px; }\n    .panel-title { margin:0; font-size:15px; font-weight:800; color:#1a1a1a; }\n    .panel-sub { font-size:12px; color:#8a8a8a; margin-top:5px; max-width:820px; line-height:1.45; }\n    .badge { padding:6px 16px; border-radius:6px; font-weight:800; font-size:12.5px; color:#fff; letter-spacing:.02em; white-space:nowrap; }\n    .periods { display:flex; gap:4px; margin-bottom:14px; }\n    .pbtn { padding:6px 12px; border-radius:8px; border:1px solid rgba(0,0,0,0.1); background:#fff; font-size:12px; font-weight:600; color:#52514e; cursor:pointer; }\n    .pbtn.active { background:${DARK}; color:#fff; border-color:${DARK}; }\n    .kpirow { display:grid; grid-template-columns:repeat(4,1fr); gap:1px; background:rgba(0,0,0,0.06); border-radius:10px; overflow:hidden; }\n    .kpi { background:#fff; padding:16px; }\n    .kpi-label { font-size:11px; color:#898781; text-transform:uppercase; letter-spacing:.02em; }\n    .kpi-val { font-size:26px; font-weight:800; color:#1a1a1a; margin-top:4px; font-variant-numeric:tabular-nums; }\n    .kpi-sub { font-size:11px; color:#898781; margin-top:2px; }\n    .status-ok { color: ${GREEN}; }\n    .status-warn { color: ${ORANGE}; }\n    .status-crit { color: ${RED}; }\n    .status-unknown { color: ${GRAY}; }\n    .chart-box { height:220px; position:relative; overflow:hidden; margin-top:16px; }\n    .chart-box canvas { width:100% !important; height:100% !important; }\n    .legend { display:flex; flex-wrap:wrap; gap:14px; font-size:11px; color:#52514e; margin-top:10px; }\n    .legend .sw { width:8px; height:8px; border-radius:2px; display:inline-block; margin-right:5px; }\n    .note { font-size:11px; color:#b0aea8; margin-top:10px; line-height:1.5; }\n    .empty { padding:24px; text-align:center; color:#898781; font-size:13px; }\n    .skel { background:linear-gradient(90deg, #eee 25%, #f2f2f0 37%, #eee 63%); background-size:400% 100%; animation:shimmer 1.4s ease infinite; border-radius:6px; }\n    @keyframes shimmer { 0%{background-position:100% 50%;} 100%{background-position:0% 50%;} }\n    .skel-kpi-label { width:70%; height:9px; margin-bottom:8px; }\n    .skel-kpi-val { width:50%; height:24px; }\n    .skel-chart { width:100%; height:100%; }\n    .daydots { display:flex; gap:6px; flex-wrap:wrap; margin-top:16px; }\n    .daydot-cell { display:flex; flex-direction:column; align-items:center; gap:4px; }\n    .daydot { width:22px; height:22px; border-radius:6px; }\n    .daydot-lbl { font-size:9.5px; color:#b0aea8; }\n  `,\n\n  render(d2) {\n    const hbRaw = d2.state.heartbeat\n    const allocRaw = d2.state.alloc\n    const botRaw = d2.state.bot\n    const hkExpireRaw = d2.state.hkExpire\n    const hkAbandonRaw = d2.state.hkAbandon\n    const syncRaw = d2.state.sync\n    const pingRaw = d2.state.ping\n    const dailyRaw = d2.state.daily\n\n    const hbWindowMin = d2.state.hbWindowMin || DEFAULT_WINDOW_MIN\n    const allocWindowMin = d2.state.allocWindowMin || DEFAULT_WINDOW_MIN\n    const botWindowMin = d2.state.botWindowMin || DEFAULT_WINDOW_MIN\n    const hkWindowMin = d2.state.hkWindowMin || DEFAULT_WINDOW_MIN\n    const syncWindowMin = d2.state.syncWindowMin || DEFAULT_SYNC_WINDOW_MIN\n    const pingWindowMin = d2.state.pingWindowMin || DEFAULT_WINDOW_MIN\n\n    const hb = hbRaw && !hbRaw.error ? deriveHeartbeatWindow(hbRaw.points, hbWindowMin) : hbRaw\n    const alloc = allocRaw && !allocRaw.error ? deriveAllocWindow(allocRaw.rows, allocWindowMin) : allocRaw\n    const bot = botRaw && !botRaw.error ? deriveDurationWindow(botRaw.rows, botWindowMin, BOT_BUCKET_MIN_BY_WINDOW) : botRaw\n    const hkExpire = hkExpireRaw && !hkExpireRaw.error ? deriveDurationWindow(hkExpireRaw.rows, hkWindowMin, HK_BUCKET_MIN_BY_WINDOW) : hkExpireRaw\n    const hkAbandon = hkAbandonRaw && !hkAbandonRaw.error ? deriveDurationWindow(hkAbandonRaw.rows, hkWindowMin, HK_BUCKET_MIN_BY_WINDOW) : hkAbandonRaw\n    const sync = syncRaw && !syncRaw.error ? deriveHeartbeatWindow(syncRaw.points, syncWindowMin) : syncRaw\n    const ping = pingRaw && !pingRaw.error ? deriveAllocWindow(pingRaw.rows, pingWindowMin, PING_BUCKET_MIN_BY_WINDOW) : pingRaw\n\n    const hbLastAgeSec = hbRaw && !hbRaw.error ? hbRaw.lastAgeSec : null\n    const hbLastAt = hbRaw && !hbRaw.error ? hbRaw.lastAt : null\n    const syncLastAgeSec = syncRaw && !syncRaw.error ? syncRaw.lastAgeSec : null\n    const syncLastAt = syncRaw && !syncRaw.error ? syncRaw.lastAt : null\n\n    const hbStatus = hbRaw && !hbRaw.error ? hbStatusOf(hbLastAgeSec) : 'unknown'\n    const allocStatus = allocRaw && !allocRaw.error ? allocStatusOf(alloc.recentRatePct) : 'unknown'\n    const botStatus = botRaw && !botRaw.error ? botStatusOf(bot.mean) : 'unknown'\n    const hkStatus = (hkExpireRaw && !hkExpireRaw.error && hkAbandonRaw && !hkAbandonRaw.error)\n      ? [hkStatusOf(hkExpire.mean), hkStatusOf(hkAbandon.mean)].includes('crit') ? 'crit'\n        : [hkStatusOf(hkExpire.mean), hkStatusOf(hkAbandon.mean)].includes('warn') ? 'warn' : 'ok'\n      : 'unknown'\n    const syncStatus = syncRaw && !syncRaw.error ? syncStatusOf(syncLastAgeSec) : 'unknown'\n    const pingStatus = pingRaw && !pingRaw.error ? pingStatusOf(ping.recentRatePct) : 'unknown'\n    const dailyStatus = dailyRaw && !dailyRaw.error && dailyRaw.rows && dailyRaw.rows.length\n      ? (dailyRaw.rows[dailyRaw.rows.length - 1].result === 'error' ? 'crit'\n        : (Date.now() - new Date(dailyRaw.rows[dailyRaw.rows.length - 1].createdAt).getTime()) > 36 * 3600 * 1000 ? 'warn' : 'ok')\n      : dailyRaw && dailyRaw.error ? 'unknown' : 'unknown'\n\n    const kpi = (label, val, sub, statusCls) => `\n      <div class=\"kpi\">\n        <div class=\"kpi-label\">${label}</div>\n        <div class=\"kpi-val ${statusCls ? 'status-' + statusCls : ''}\">${val}</div>\n        ${sub ? `<div class=\"kpi-sub\">${sub}</div>` : ''}\n      </div>`\n\n    const periods = (namePrefix, current, options) => `\n      <div class=\"periods\">\n        ${(options || WINDOW_OPTIONS).map(o => `<button class=\"pbtn ${namePrefix}-win-btn ${current === o.min ? 'active' : ''}\" data-win=\"${o.min}\">${o.label}</button>`).join('')}\n      </div>`\n\n    const skeleton = () => `\n      <div class=\"kpirow\">${Array.from({ length: 4 }).map(() => `<div class=\"kpi\"><div class=\"skel skel-kpi-label\"></div><div class=\"skel skel-kpi-val\"></div></div>`).join('')}</div>\n      <div class=\"chart-box\"><div class=\"skel skel-chart\"></div></div>`\n\n    const hbBody = !hbRaw\n      ? skeleton()\n      : hbRaw.error\n      ? `<div class=\"empty\">Erro ao carregar: ${esc(hbRaw.error)}</div>`\n      : `\n        <div class=\"kpirow\">\n          ${kpi('Última execução', fmtAge(hbLastAgeSec), fmtClock(hbLastAt), hbStatus)}\n          ${kpi('Ciclo médio (janela)', fmtSecs(hb.mean), `delay configurado: 4.4s`)}\n          ${kpi('Maior pico (janela)', fmtSecs(hb.max), hb.maxAt ? `às ${fmtClock(hb.maxAt)}` : '--', hbStatusOf(hb.max))}\n          ${kpi('Execuções na janela', hb.count != null ? hb.count.toLocaleString('pt-BR') : '--', `${fmtClock(hb.windowStart)} – ${fmtClock(hb.windowEnd)}`)}\n        </div>\n        <div class=\"chart-box\"><canvas id=\"heartbeatChart\"></canvas></div>\n        <div class=\"legend\">\n          <span><span class=\"sw\" style=\"background:${GREEN}\"></span>Normal (≤${HB_OK_MAX}s)</span>\n          <span><span class=\"sw\" style=\"background:${ORANGE}\"></span>Atenção (${HB_OK_MAX}–${HB_WARN_MAX}s)</span>\n          <span><span class=\"sw\" style=\"background:${RED}\"></span>Crítico (>${HB_WARN_MAX}s)</span>\n        </div>\n        <div class=\"note\">Mesmo job do painel \"alarme-fila\" (Queue Monitoring) — aqui medido como intervalo entre execuções, não como duração por execução. Janela limitada ao que o log ainda retém.</div>\n      `\n\n    const allocBody = !allocRaw\n      ? skeleton()\n      : allocRaw.error\n      ? `<div class=\"empty\">Erro ao carregar: ${esc(allocRaw.error)}</div>`\n      : `\n        <div class=\"kpirow\">\n          ${kpi('Execuções na janela', alloc.count != null ? alloc.count.toLocaleString('pt-BR') : '--', `${fmtDateTime(alloc.windowStart)} – ${fmtDateTime(alloc.windowEnd)}`)}\n          ${kpi('Erros na janela', alloc.errors != null ? alloc.errors.toLocaleString('pt-BR') : '--', null, alloc.errors > 0 ? 'crit' : 'ok')}\n          ${kpi('Taxa de erro (janela)', fmtPct(alloc.overallRatePct), null, allocStatusOf(alloc.overallRatePct))}\n          ${kpi('Última falha', alloc.lastErrorAt ? fmtDateTime(alloc.lastErrorAt) : 'nenhuma no período', null, alloc.lastErrorAt ? 'crit' : 'ok')}\n        </div>\n        <div class=\"chart-box\"><canvas id=\"allocChart\"></canvas></div>\n        <div class=\"legend\">\n          <span><span class=\"sw\" style=\"background:${GREEN}\"></span>Chamadas com sucesso</span>\n          <span><span class=\"sw\" style=\"background:${RED}\"></span>Chamadas com erro</span>\n        </div>\n        <div class=\"note\">Cobre o endpoint compartilhado de transferência (alocação + encerramento + abandono + alerta) — o log ainda não separa por operação. Cada barra agrupa ${BUCKET_MIN_BY_WINDOW[allocWindowMin] || 15} minutos.</div>\n      `\n\n    const botBody = !botRaw\n      ? skeleton()\n      : botRaw.error\n      ? `<div class=\"empty\">Erro ao carregar: ${esc(botRaw.error)}</div>`\n      : `\n        <div class=\"kpirow\">\n          ${kpi('Última resposta', fmtAge(bot.lastAgeSec), fmtClock(bot.lastAt), botStatusOf(bot.mean))}\n          ${kpi('Latência média (janela)', fmtMs(bot.mean), null, botStatusOf(bot.mean))}\n          ${kpi('Pico de latência (janela)', fmtMs(bot.max), bot.maxAt ? `às ${fmtClock(bot.maxAt)}` : '--', botStatusOf(bot.max))}\n          ${kpi('Chamadas na janela', bot.count != null ? bot.count.toLocaleString('pt-BR') : '--', bot.errors ? `${bot.errors} erro(s)` : null)}\n        </div>\n        <div class=\"chart-box\"><canvas id=\"botChart\"></canvas></div>\n        <div class=\"legend\">\n          <span><span class=\"sw\" style=\"background:${GREEN}\"></span>Normal (≤${BOT_OK_MAX / 1000}s)</span>\n          <span><span class=\"sw\" style=\"background:${ORANGE}\"></span>Atenção (${BOT_OK_MAX / 1000}–${BOT_WARN_MAX / 1000}s)</span>\n          <span><span class=\"sw\" style=\"background:${RED}\"></span>Crítico (>${BOT_WARN_MAX / 1000}s)</span>\n        </div>\n        <div class=\"note\">Fonte: <code>Chat-Entrypoint-PRD</code> (~1 chamada a cada 4,3s) — cada ponto agrupa ${BOT_BUCKET_MIN_BY_WINDOW[botWindowMin] || 5} minuto(s). Limiares de latência são indicativos.</div>\n      `\n\n    const hkBody = (!hkExpireRaw || !hkAbandonRaw)\n      ? skeleton()\n      : (hkExpireRaw.error || hkAbandonRaw.error)\n      ? `<div class=\"empty\">Erro ao carregar: ${esc(hkExpireRaw.error || hkAbandonRaw.error)}</div>`\n      : `\n        <div class=\"kpirow\">\n          ${kpi('Expiração — última execução', fmtAge(hkExpire.lastAgeSec), fmtClock(hkExpire.lastAt), hkStatusOf(hkExpire.mean))}\n          ${kpi('Expiração — duração média', fmtMs(hkExpire.mean), hkExpire.errors ? `${hkExpire.errors} erro(s)` : null, hkStatusOf(hkExpire.mean))}\n          ${kpi('Abandono — última execução', fmtAge(hkAbandon.lastAgeSec), fmtClock(hkAbandon.lastAt), hkStatusOf(hkAbandon.mean))}\n          ${kpi('Abandono — duração média', fmtMs(hkAbandon.mean), hkAbandon.errors ? `${hkAbandon.errors} erro(s)` : null, hkStatusOf(hkAbandon.mean))}\n        </div>\n        <div class=\"chart-box\"><canvas id=\"hkChart\"></canvas></div>\n        <div class=\"legend\">\n          <span><span class=\"sw\" style=\"background:${GREEN}\"></span>Normal (≤${HK_OK_MAX / 1000}s)</span>\n          <span><span class=\"sw\" style=\"background:${ORANGE}\"></span>Atenção (${HK_OK_MAX / 1000}–${HK_WARN_MAX / 1000}s)</span>\n          <span><span class=\"sw\" style=\"background:${RED}\"></span>Crítico (>${HK_WARN_MAX / 1000}s)</span>\n          <span>— linha sólida: expiração · linha pontilhada: abandono</span>\n        </div>\n        <div class=\"note\">Dois crons de 1 em 1 minuto. O log não expõe quantos registros cada rodada tratou — só duração e resultado por execução.</div>\n      `\n\n    const syncBody = !syncRaw\n      ? skeleton()\n      : syncRaw.error\n      ? `<div class=\"empty\">Erro ao carregar: ${esc(syncRaw.error)}</div>`\n      : `\n        <div class=\"kpirow\">\n          ${kpi('Última sincronização', fmtAge(syncLastAgeSec), fmtClock(syncLastAt), syncStatus)}\n          ${kpi('Intervalo médio (janela)', fmtMinFromSec(sync.mean), `cron a cada 30min`)}\n          ${kpi('Maior intervalo (janela)', fmtMinFromSec(sync.max), sync.maxAt ? `às ${fmtClock(sync.maxAt)}` : '--', syncStatusOf(sync.max))}\n          ${kpi('Execuções na janela', sync.count != null ? sync.count.toLocaleString('pt-BR') : '--', `${fmtDateTime(sync.windowStart)} – ${fmtDateTime(sync.windowEnd)}`)}\n        </div>\n        <div class=\"chart-box\"><canvas id=\"syncChart\"></canvas></div>\n        <div class=\"legend\">\n          <span><span class=\"sw\" style=\"background:${GREEN}\"></span>Normal (≤${SYNC_OK_MAX / 60}min)</span>\n          <span><span class=\"sw\" style=\"background:${ORANGE}\"></span>Atenção (${SYNC_OK_MAX / 60}–${SYNC_WARN_MAX / 60}min)</span>\n          <span><span class=\"sw\" style=\"background:${RED}\"></span>Crítico (>${SYNC_WARN_MAX / 60}min)</span>\n        </div>\n        <div class=\"note\">Cron programado para rodar a cada 30 minutos.</div>\n      `\n\n    const pingBody = !pingRaw\n      ? skeleton()\n      : pingRaw.error\n      ? `<div class=\"empty\">Erro ao carregar: ${esc(pingRaw.error)}</div>`\n      : `\n        <div class=\"kpirow\">\n          ${kpi('Pings na janela', ping.count != null ? ping.count.toLocaleString('pt-BR') : '--', `${fmtDateTime(ping.windowStart)} – ${fmtDateTime(ping.windowEnd)}`)}\n          ${kpi('Erros na janela', ping.errors != null ? ping.errors.toLocaleString('pt-BR') : '--', null, ping.errors > 0 ? 'crit' : 'ok')}\n          ${kpi('Taxa de erro (janela)', fmtPct(ping.overallRatePct), null, pingStatusOf(ping.overallRatePct))}\n          ${kpi('Maior volume (janela)', ping.buckets.length ? Math.max(...ping.buckets.map(b => b.total)).toLocaleString('pt-BR') : '--', `agrupado de ${PING_BUCKET_MIN_BY_WINDOW[pingWindowMin] || 1} min`)}\n        </div>\n        <div class=\"chart-box\"><canvas id=\"pingChart\"></canvas></div>\n        <div class=\"legend\">\n          <span><span class=\"sw\" style=\"background:${GREEN}\"></span>Pings normais</span>\n          <span><span class=\"sw\" style=\"background:${RED}\"></span>Pings com erro</span>\n        </div>\n        <div class=\"note\">~1 ping a cada 2,1s na fonte — é uma proxy de concorrência, não uma contagem exata de sessões únicas.</div>\n      `\n\n    const dailyRows = dailyRaw && !dailyRaw.error ? dailyRaw.rows || [] : []\n    const dailyLast = dailyRows.length ? dailyRows[dailyRows.length - 1] : null\n    let dailyStreak = 0\n    for (let i = dailyRows.length - 1; i >= 0; i--) {\n      if (dailyRows[i].result === 'success') dailyStreak++\n      else break\n    }\n    const dailyBody = !dailyRaw\n      ? skeleton()\n      : dailyRaw.error\n      ? `<div class=\"empty\">Erro ao carregar: ${esc(dailyRaw.error)}</div>`\n      : `\n        <div class=\"kpirow\">\n          ${kpi('Última execução', dailyLast ? fmtDateTime(dailyLast.createdAt) : '--', dailyLast ? (dailyLast.result === 'success' ? 'sucesso' : dailyLast.result) : null, dailyLast ? (dailyLast.result === 'success' ? 'ok' : 'crit') : 'unknown')}\n          ${kpi('Execuções nos últimos 14 dias', dailyRows.length.toLocaleString('pt-BR'), 'cron diário')}\n          ${kpi('Falhas nos últimos 14 dias', dailyRows.filter(r => r.result === 'error').length.toLocaleString('pt-BR'), null, dailyRows.some(r => r.result === 'error') ? 'crit' : 'ok')}\n          ${kpi('Sequência de sucessos', `${dailyStreak} dia(s)`, null, dailyStreak > 0 ? 'ok' : 'crit')}\n        </div>\n        <div class=\"daydots\">\n          ${dailyRows.map(r => `<div class=\"daydot-cell\"><div class=\"daydot\" style=\"background:${r.result === 'success' ? GREEN : RED}\" title=\"${fmtDateTime(r.createdAt)} — ${esc(r.result)}\"></div><div class=\"daydot-lbl\">${new Date(r.createdAt).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })}</div></div>`).join('')}\n        </div>\n        <div class=\"note\">~04h diariamente. Cada célula é uma execução (verde = sucesso, vermelho = erro); \"atrasado\" quando a última tem mais de 36h.</div>\n      `\n\n    return `\n      <div class=\"wrap\">\n        <div class=\"panel header-panel\">\n          <h2>Saúde dos Sistemas — Motores do Atendimento</h2>\n          <div class=\"sub\">Sinais de saúde dos motores que sustentam o atendimento — da fila ao bot de IA, passando por housekeeping e sincronizações internas.</div>\n          <div style=\"margin-top:12px\"><button class=\"refresh-btn\">⟳ Atualizar</button></div>\n        </div>\n\n        <div class=\"panel\">\n          <div class=\"panel-hd\">\n            <div>\n              <h3 class=\"panel-title\">Motor de Distribuição da Fila (Autohealer)</h3>\n              <div class=\"panel-sub\">Intervalo real entre execuções do motor que redistribui chats da fila para agentes — se o ciclo atrasa ou trava, chats ficam esperando na fila sem que ninguém perceba.</div>\n            </div>\n            <span class=\"badge\" style=\"background:${colorOf(hbStatus)}\">${labelFor(hbStatus)}</span>\n          </div>\n          ${periods('hb', hbWindowMin)}\n          ${hbBody}\n        </div>\n\n        <div class=\"panel\">\n          <div class=\"panel-hd\">\n            <div>\n              <h3 class=\"panel-title\">Alocação de Agente — Sucesso vs Erro</h3>\n              <div class=\"panel-sub\">Volume e taxa de erro na etapa que efetivamente conecta o cliente a um agente — se essa chamada falha, o cliente fica preso na fila mesmo com agente disponível, sem gerar alerta nenhum.</div>\n            </div>\n            <span class=\"badge\" style=\"background:${colorOf(allocStatus)}\">${labelFor(allocStatus)}</span>\n          </div>\n          ${periods('alloc', allocWindowMin)}\n          ${allocBody}\n        </div>\n\n        <div class=\"panel\">\n          <div class=\"panel-hd\">\n            <div>\n              <h3 class=\"panel-title\">Performance do Bot de IA</h3>\n              <div class=\"panel-sub\">Latência de resposta do bot a cada mensagem do cliente — se subir muito, o cliente espera mais antes mesmo de chegar a um agente humano.</div>\n            </div>\n            <span class=\"badge\" style=\"background:${colorOf(botStatus)}\">${labelFor(botStatus)}</span>\n          </div>\n          ${periods('bot', botWindowMin)}\n          ${botBody}\n        </div>\n\n        <div class=\"panel\">\n          <div class=\"panel-hd\">\n            <div>\n              <h3 class=\"panel-title\">Housekeeping — Expiração e Abandono</h3>\n              <div class=\"panel-sub\">Detecta abandono de fila por timeout e expira transferências travadas — se demorar ou falhar, elas ficam presas em limbo sem liberar agente ou vaga. (O abandono manual do cliente já aparece no painel de Alocação.)</div>\n            </div>\n            <span class=\"badge\" style=\"background:${colorOf(hkStatus)}\">${labelFor(hkStatus)}</span>\n          </div>\n          ${periods('hk', hkWindowMin)}\n          ${hkBody}\n        </div>\n\n        <div class=\"panel\">\n          <div class=\"panel-hd\">\n            <div>\n              <h3 class=\"panel-title\">Sincronização de Metadados de Agente</h3>\n              <div class=\"panel-sub\">Recalcula quantas transferências cada agente tem em aberto — se atrasar, o motor de distribuição pode mandar mais um chat pra um agente que já está sem capacidade.</div>\n            </div>\n            <span class=\"badge\" style=\"background:${colorOf(syncStatus)}\">${labelFor(syncStatus)}</span>\n          </div>\n          ${periods('sync', syncWindowMin, SYNC_WINDOW_OPTIONS)}\n          ${syncBody}\n        </div>\n\n        <div class=\"panel\">\n          <div class=\"panel-hd\">\n            <div>\n              <h3 class=\"panel-title\">Pulso de Chats Ativos (chat-ping)</h3>\n              <div class=\"panel-sub\">Ping periódico de cada chat aberto no navegador do cliente — a contagem despenca se o site ou app parar de abrir/manter chats, mesmo com o backend saudável.</div>\n            </div>\n            <span class=\"badge\" style=\"background:${colorOf(pingStatus)}\">${labelFor(pingStatus)}</span>\n          </div>\n          ${periods('ping', pingWindowMin)}\n          ${pingBody}\n        </div>\n\n        <div class=\"panel\">\n          <div class=\"panel-hd\">\n            <div>\n              <h3 class=\"panel-title\">Sincronização Diária de Atendimentos</h3>\n              <div class=\"panel-sub\">Job diário que consolida os atendimentos do dia anterior — se falhar ou atrasar, dados de atendimento ficam desatualizados nos relatórios sem ninguém notar até perguntarem.</div>\n            </div>\n            <span class=\"badge\" style=\"background:${colorOf(dailyStatus)}\">${labelFor(dailyStatus)}</span>\n          </div>\n          ${dailyBody}\n        </div>\n      </div>\n    `\n  },\n\n  async mounted(d2, root) {\n    async function refreshHeartbeat() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'heartbeatLog', 2200)\n        const points = []\n        for (let i = 1; i < sorted.length; i++) {\n          const t1 = new Date(sorted[i - 1].createdAt).getTime()\n          const t2 = new Date(sorted[i].createdAt).getTime()\n          const delta = (t2 - t1) / 1000\n          if (delta > 0 && delta < 3600) points.push({ t: sorted[i].createdAt, delta })\n        }\n        const last = sorted[sorted.length - 1]\n        const lastAgeSec = last ? (Date.now() - new Date(last.createdAt).getTime()) / 1000 : null\n        d2.state.heartbeat = { points, lastAt: last ? last.createdAt : null, lastAgeSec }\n      } catch (e) {\n        d2.state.heartbeat = { error: String((e && e.message) || e) }\n      }\n    }\n\n    async function refreshAlloc() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'agentAllocLog', 1800)\n        d2.state.alloc = { rows: sorted }\n      } catch (e) {\n        d2.state.alloc = { error: String((e && e.message) || e) }\n      }\n    }\n\n    async function refreshBot() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'botReplyLog', 2800, { durationInMs: 1 })\n        d2.state.bot = { rows: sorted }\n      } catch (e) {\n        d2.state.bot = { error: String((e && e.message) || e) }\n      }\n    }\n\n    async function refreshHkExpire() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'expireInactiveLog', 900, { durationInMs: 1 })\n        d2.state.hkExpire = { rows: sorted }\n      } catch (e) {\n        d2.state.hkExpire = { error: String((e && e.message) || e) }\n      }\n    }\n\n    async function refreshHkAbandon() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'detectAbandonLog', 900, { durationInMs: 1 })\n        d2.state.hkAbandon = { rows: sorted }\n      } catch (e) {\n        d2.state.hkAbandon = { error: String((e && e.message) || e) }\n      }\n    }\n\n    async function refreshSync() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'syncAgentsLog', 200)\n        const points = []\n        for (let i = 1; i < sorted.length; i++) {\n          const t1 = new Date(sorted[i - 1].createdAt).getTime()\n          const t2 = new Date(sorted[i].createdAt).getTime()\n          const delta = (t2 - t1) / 1000\n          if (delta > 0 && delta < 24 * 3600) points.push({ t: sorted[i].createdAt, delta })\n        }\n        const last = sorted[sorted.length - 1]\n        const lastAgeSec = last ? (Date.now() - new Date(last.createdAt).getTime()) / 1000 : null\n        d2.state.sync = { points, lastAt: last ? last.createdAt : null, lastAgeSec }\n      } catch (e) {\n        d2.state.sync = { error: String((e && e.message) || e) }\n      }\n    }\n\n    async function refreshPing() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'chatPingLog', 6000)\n        d2.state.ping = { rows: sorted }\n      } catch (e) {\n        d2.state.ping = { error: String((e && e.message) || e) }\n      }\n    }\n\n    async function refreshDaily() {\n      try {\n        const sorted = await fetchRecentRows(d2, 'dailySyncLog', 60)\n        d2.state.daily = { rows: sorted.slice(-DAILY_STATUS_DAYS) }\n      } catch (e) {\n        d2.state.daily = { error: String((e && e.message) || e) }\n      }\n    }\n\n    function refreshAll() {\n      refreshHeartbeat(); refreshAlloc(); refreshBot()\n      refreshHkExpire(); refreshHkAbandon(); refreshSync(); refreshPing(); refreshDaily()\n    }\n\n    d2.on('.refresh-btn', 'click', refreshAll)\n    d2.on('.hb-win-btn', 'click', (e, el) => { d2.state.hbWindowMin = Number(el.dataset.win) })\n    d2.on('.alloc-win-btn', 'click', (e, el) => { d2.state.allocWindowMin = Number(el.dataset.win) })\n    d2.on('.bot-win-btn', 'click', (e, el) => { d2.state.botWindowMin = Number(el.dataset.win) })\n    d2.on('.hk-win-btn', 'click', (e, el) => { d2.state.hkWindowMin = Number(el.dataset.win) })\n    d2.on('.sync-win-btn', 'click', (e, el) => { d2.state.syncWindowMin = Number(el.dataset.win) })\n    d2.on('.ping-win-btn', 'click', (e, el) => { d2.state.pingWindowMin = Number(el.dataset.win) })\n\n    d2.state.hbWindowMin = DEFAULT_WINDOW_MIN\n    d2.state.allocWindowMin = DEFAULT_WINDOW_MIN\n    d2.state.botWindowMin = DEFAULT_WINDOW_MIN\n    d2.state.hkWindowMin = DEFAULT_WINDOW_MIN\n    d2.state.syncWindowMin = DEFAULT_SYNC_WINDOW_MIN\n    d2.state.pingWindowMin = DEFAULT_WINDOW_MIN\n\n    refreshAll()\n    const id = setInterval(refreshAll, 30000)\n    d2.onUnmount(() => clearInterval(id))\n  },\n\n  updated(d2, root) {\n    const Chart = resolveChart(d2.lib(CHART_LIB))\n    if (!Chart) return\n\n    const hbRaw = d2.state.heartbeat\n    if (hbRaw && !hbRaw.error && hbRaw.points) {\n      const hbWindowMin = d2.state.hbWindowMin || DEFAULT_WINDOW_MIN\n      const hb = deriveHeartbeatWindow(hbRaw.points, hbWindowMin)\n      if (hb.points.length) {\n        const labels = hb.points.map(p => new Date(p.t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }))\n        const data = hb.points.map(p => Number(p.delta.toFixed(2)))\n        const colorForVal = (v) => colorOf(hbStatusOf(v))\n\n        upsertChart(root, Chart, 'heartbeat', '#heartbeatChart', {\n          type: 'line',\n          data: {\n            labels,\n            datasets: [\n              {\n                label: 'Intervalo (s)',\n                data,\n                borderColor: DARK,\n                borderWidth: 1.4,\n                pointRadius: 0,\n                pointHoverRadius: 4,\n                pointBackgroundColor: data.map(colorForVal),\n                segment: { borderColor: (ctx) => colorForVal(Math.max(ctx.p0.parsed.y, ctx.p1.parsed.y)) },\n                tension: 0.1,\n                fill: false,\n              },\n              {\n                label: 'Delay configurado (4.4s)',\n                data: data.map(() => 4.4),\n                borderColor: 'rgba(150,150,150,0.55)',\n                borderWidth: 1,\n                borderDash: [4, 4],\n                pointRadius: 0,\n              },\n            ],\n          },\n          options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: { padding: { top: 10 } },\n            plugins: {\n              legend: { display: false },\n              tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(1)}s` } },\n            },\n            scales: {\n              x: { ticks: { maxTicksLimit: 8 } },\n              y: { beginAtZero: true, suggestedMax: Math.max(20, Math.ceil(Math.max(...data) * 1.2)), title: { display: true, text: 'segundos' } },\n            },\n          },\n        })\n      }\n    }\n\n    const allocRaw = d2.state.alloc\n    if (allocRaw && !allocRaw.error && allocRaw.rows) {\n      const allocWindowMin = d2.state.allocWindowMin || DEFAULT_WINDOW_MIN\n      const alloc = deriveAllocWindow(allocRaw.rows, allocWindowMin)\n      if (alloc.buckets.length) {\n        const labels = alloc.buckets.map(b => new Date(b.t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }))\n        const successData = alloc.buckets.map(b => b.success)\n        const errorData = alloc.buckets.map(b => b.errors)\n\n        upsertChart(root, Chart, 'alloc', '#allocChart', {\n          type: 'bar',\n          data: {\n            labels,\n            datasets: [\n              { label: 'Sucesso', data: successData, backgroundColor: GREEN, stack: 'v', borderRadius: { topLeft: 3, topRight: 3 }, maxBarThickness: 28 },\n              { label: 'Erro', data: errorData, backgroundColor: RED, stack: 'v', borderRadius: { topLeft: 3, topRight: 3 }, maxBarThickness: 28 },\n            ],\n          },\n          options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: { padding: { top: 10 } },\n            plugins: {\n              legend: { display: false },\n              tooltip: {\n                callbacks: {\n                  label: (ctx) => {\n                    const b = alloc.buckets[ctx.dataIndex]\n                    return ctx.dataset.label === 'Erro'\n                      ? `${b.errors} erro(s) de ${b.total} chamada(s) (${b.ratePct.toFixed(1)}%)`\n                      : `${b.success} com sucesso de ${b.total} chamada(s)`\n                  },\n                },\n              },\n            },\n            scales: {\n              x: { stacked: true, ticks: { maxTicksLimit: 10 } },\n              y: { stacked: true, beginAtZero: true, title: { display: true, text: 'chamadas' } },\n            },\n          },\n        })\n      }\n    }\n\n    const botRaw = d2.state.bot\n    if (botRaw && !botRaw.error && botRaw.rows) {\n      const botWindowMin = d2.state.botWindowMin || DEFAULT_WINDOW_MIN\n      const bot = deriveDurationWindow(botRaw.rows, botWindowMin, BOT_BUCKET_MIN_BY_WINDOW)\n      if (bot.buckets.length) {\n        const labels = bot.buckets.map(b => new Date(b.t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }))\n        const data = bot.buckets.map(b => Number((b.avgMs / 1000).toFixed(2)))\n        const colorForVal = (vSec) => colorOf(botStatusOf(vSec * 1000))\n\n        upsertChart(root, Chart, 'bot', '#botChart', {\n          type: 'line',\n          data: {\n            labels,\n            datasets: [\n              {\n                label: 'Latência média (s)',\n                data,\n                borderColor: DARK,\n                borderWidth: 1.4,\n                pointRadius: 0,\n                pointHoverRadius: 4,\n                pointBackgroundColor: data.map(colorForVal),\n                segment: { borderColor: (ctx) => colorForVal(Math.max(ctx.p0.parsed.y, ctx.p1.parsed.y)) },\n                tension: 0.15,\n                fill: false,\n              },\n            ],\n          },\n          options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: { padding: { top: 10 } },\n            plugins: {\n              legend: { display: false },\n              tooltip: {\n                callbacks: {\n                  label: (ctx) => {\n                    const b = bot.buckets[ctx.dataIndex]\n                    return `${ctx.parsed.y.toFixed(1)}s médio (${b.total} chamada(s), pico ${fmtMs(b.maxMs)})`\n                  },\n                },\n              },\n            },\n            scales: {\n              x: { ticks: { maxTicksLimit: 10 } },\n              y: { beginAtZero: true, suggestedMax: Math.max(BOT_OK_MAX / 1000, Math.ceil(Math.max(...data) * 1.2)), title: { display: true, text: 'segundos' } },\n            },\n          },\n        })\n      }\n    }\n\n    const hkExpireRaw = d2.state.hkExpire\n    const hkAbandonRaw = d2.state.hkAbandon\n    if (hkExpireRaw && !hkExpireRaw.error && hkAbandonRaw && !hkAbandonRaw.error) {\n      const hkWindowMin = d2.state.hkWindowMin || DEFAULT_WINDOW_MIN\n      const hkExpire = deriveDurationWindow(hkExpireRaw.rows, hkWindowMin, HK_BUCKET_MIN_BY_WINDOW)\n      const hkAbandon = deriveDurationWindow(hkAbandonRaw.rows, hkWindowMin, HK_BUCKET_MIN_BY_WINDOW)\n      if (hkExpire.buckets.length || hkAbandon.buckets.length) {\n        const allTimes = [...new Set([...hkExpire.buckets.map(b => b.t), ...hkAbandon.buckets.map(b => b.t)])].sort((a, b) => a - b)\n        const expireByT = new Map(hkExpire.buckets.map(b => [b.t, b.avgMs]))\n        const abandonByT = new Map(hkAbandon.buckets.map(b => [b.t, b.avgMs]))\n        const labels = allTimes.map(t => new Date(t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }))\n        const expireData = allTimes.map(t => expireByT.has(t) ? Number((expireByT.get(t) / 1000).toFixed(2)) : null)\n        const abandonData = allTimes.map(t => abandonByT.has(t) ? Number((abandonByT.get(t) / 1000).toFixed(2)) : null)\n        const allVals = [...expireData, ...abandonData].filter(v => v != null)\n\n        upsertChart(root, Chart, 'hk', '#hkChart', {\n          type: 'line',\n          data: {\n            labels,\n            datasets: [\n              { label: 'Expiração (s)', data: expireData, borderColor: DARK, borderWidth: 1.6, pointRadius: 0, tension: 0.15, spanGaps: true, segment: { borderColor: (ctx) => colorOf(hkStatusOf(Math.max(ctx.p0.parsed.y || 0, ctx.p1.parsed.y || 0) * 1000)) } },\n              { label: 'Abandono (s)', data: abandonData, borderColor: ORANGE, borderWidth: 1.6, borderDash: [6, 3], pointRadius: 0, tension: 0.15, spanGaps: true, segment: { borderColor: (ctx) => colorOf(hkStatusOf(Math.max(ctx.p0.parsed.y || 0, ctx.p1.parsed.y || 0) * 1000)) } },\n            ],\n          },\n          options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: { padding: { top: 10 } },\n            plugins: {\n              legend: { display: true, position: 'bottom', labels: { boxWidth: 10, font: { size: 10 } } },\n              tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y != null ? ctx.parsed.y.toFixed(2) + 's' : '--'}` } },\n            },\n            scales: {\n              x: { ticks: { maxTicksLimit: 10 } },\n              y: { beginAtZero: true, suggestedMax: Math.max(HK_WARN_MAX / 1000, allVals.length ? Math.ceil(Math.max(...allVals) * 1.2) : 4), title: { display: true, text: 'segundos' } },\n            },\n          },\n        })\n      }\n    }\n\n    const syncRaw = d2.state.sync\n    if (syncRaw && !syncRaw.error && syncRaw.points) {\n      const syncWindowMin = d2.state.syncWindowMin || DEFAULT_SYNC_WINDOW_MIN\n      const sync = deriveHeartbeatWindow(syncRaw.points, syncWindowMin)\n      if (sync.points.length) {\n        const labels = sync.points.map(p => new Date(p.t).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }))\n        const data = sync.points.map(p => Number((p.delta / 60).toFixed(1)))\n        const colorForVal = (min) => colorOf(syncStatusOf(min * 60))\n\n        upsertChart(root, Chart, 'sync', '#syncChart', {\n          type: 'line',\n          data: {\n            labels,\n            datasets: [\n              {\n                label: 'Intervalo (min)',\n                data,\n                borderColor: DARK,\n                borderWidth: 1.4,\n                pointRadius: 0,\n                pointHoverRadius: 4,\n                pointBackgroundColor: data.map(colorForVal),\n                segment: { borderColor: (ctx) => colorForVal(Math.max(ctx.p0.parsed.y, ctx.p1.parsed.y)) },\n                tension: 0.1,\n                fill: false,\n              },\n              {\n                label: 'Cron esperado (30min)',\n                data: data.map(() => 30),\n                borderColor: 'rgba(150,150,150,0.55)',\n                borderWidth: 1,\n                borderDash: [4, 4],\n                pointRadius: 0,\n              },\n            ],\n          },\n          options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: { padding: { top: 10 } },\n            plugins: {\n              legend: { display: false },\n              tooltip: { callbacks: { label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y.toFixed(1)}min` } },\n            },\n            scales: {\n              x: { ticks: { maxTicksLimit: 8 } },\n              y: { beginAtZero: true, suggestedMax: Math.max(60, Math.ceil(Math.max(...data) * 1.2)), title: { display: true, text: 'minutos' } },\n            },\n          },\n        })\n      }\n    }\n\n    const pingRaw = d2.state.ping\n    if (pingRaw && !pingRaw.error && pingRaw.rows) {\n      const pingWindowMin = d2.state.pingWindowMin || DEFAULT_WINDOW_MIN\n      const ping = deriveAllocWindow(pingRaw.rows, pingWindowMin, PING_BUCKET_MIN_BY_WINDOW)\n      if (ping.buckets.length) {\n        const labels = ping.buckets.map(b => new Date(b.t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }))\n        const successData = ping.buckets.map(b => b.success)\n        const errorData = ping.buckets.map(b => b.errors)\n\n        upsertChart(root, Chart, 'ping', '#pingChart', {\n          type: 'bar',\n          data: {\n            labels,\n            datasets: [\n              { label: 'Pings OK', data: successData, backgroundColor: GREEN, stack: 'v', borderRadius: { topLeft: 2, topRight: 2 }, maxBarThickness: 14 },\n              { label: 'Pings com erro', data: errorData, backgroundColor: RED, stack: 'v', borderRadius: { topLeft: 2, topRight: 2 }, maxBarThickness: 14 },\n            ],\n          },\n          options: {\n            responsive: true,\n            maintainAspectRatio: false,\n            layout: { padding: { top: 10 } },\n            plugins: {\n              legend: { display: false },\n              tooltip: {\n                callbacks: {\n                  label: (ctx) => {\n                    const b = ping.buckets[ctx.dataIndex]\n                    return ctx.dataset.label === 'Pings com erro'\n                      ? `${b.errors} erro(s) de ${b.total} ping(s)`\n                      : `${b.success} ok de ${b.total} ping(s)`\n                  },\n                },\n              },\n            },\n            scales: {\n              x: { stacked: true, ticks: { maxTicksLimit: 10 } },\n              y: { stacked: true, beginAtZero: true, title: { display: true, text: 'pings/min' } },\n            },\n          },\n        })\n      }\n    }\n  },\n}\n"}},"heartbeatLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"heartbeatLog"}}}},"agentAllocLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentAllocLog"}}}},"botReplyLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"botReplyLog"}}}},"expireInactiveLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"expireInactiveLog"}}}},"detectAbandonLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"detectAbandonLog"}}}},"syncAgentsLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"syncAgentsLog"}}}},"chatPingLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"chatPingLog"}}}},"dailySyncLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"dailySyncLog"}}}}}}}}}},{"_id":"6a88623257b1a6c110f112ec","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"5l0cUIKhSmoUukTmy8UU7","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\\"/g, '&quot;') }\nfunction fmtHMS(sec) {\n  sec = Math.max(0, Math.round(sec || 0))\n  const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = sec%60\n  return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`\n}\n\nconst RED = 'rgba(229,0,75,1)', DARK = 'rgba(54,54,54,1)', GRAY = 'rgba(150,150,150,1)'\n// Real status_type enum + sort/whitelist rule verified directly from the production artboard\n// \"[EC] Supervisor - Agentes\" (rND6FccX7QYPyoEG5KMnb) -> \"table agents online\" (sAwsBV4mnaeQeyiDE7Dg1).\n// Default view = status_type IN [online, pause]; stop/offline excluded unless \"mostrar indisponíveis\" is on.\nconst STATUS_TYPE_META = {\n  online: { label: 'Online', color: 'rgba(46,204,113,1)' },\n  pause:  { label: 'Pausa',  color: 'rgba(243,191,58,1)' },\n  stop:   { label: 'Parado', color: 'rgba(150,150,150,1)' },\n  offline:{ label: 'Offline', color: 'rgba(180,180,180,1)' },\n}\nconst STATUS_SORT_INDEX = { online: 0, pause: 2, stop: 3, offline: 4 }\nfunction statusIndex(a) {\n  if (a.status_type === 'online') return a.in_service ? 0 : 1\n  return STATUS_SORT_INDEX[a.status_type] != null ? STATUS_SORT_INDEX[a.status_type] : 99\n}\n\nconst CLS_META = {\n  ended_by_agent:   { label: 'Encerrado pelo agente',   color: 'rgba(104,218,90,1)' },\n  ended_by_user:    { label: 'Encerrado pelo usuário',  color: 'rgba(52,166,238,1)' },\n  inactivity_agent: { label: 'Inatividade do agente',   color: 'rgba(167,4,4,1)' },\n  inactivity_user:  { label: 'Inatividade do cliente',  color: 'rgba(9,255,220,1)' },\n}\nconst STATUS_META = {\n  completed:        { label: 'Atendido',              color: 'rgba(0,255,93,1)' },\n  outside_hours:    { label: 'Fora de horário',        color: 'rgba(255,171,0,1)' },\n  abandoned:        { label: 'Abandonado',             color: 'rgba(255,0,0,1)' },\n  unavailable_agent:{ label: 'Operador indisponível',  color: 'rgba(134,8,8,1)' },\n}\nconst PERIODS = [\n  { key: 'hoje', label: 'Hoje', days: 1 },\n  { key: '3d',   label: '-3D',  days: 3 },\n  { key: '7d',   label: '-7D',  days: 7 },\n  { key: '15d',  label: '-15D', days: 15 },\n  { key: '30d',  label: '-30D', days: 30 },\n]\n\nfunction resolveChart(lib) {\n  if (!lib) return null\n  if (typeof lib === 'function') return lib\n  return lib.Chart || lib.default || null\n}\nfunction upsertChart(root, Chart, key, canvasSel, config) {\n  const canvas = root.querySelector(canvasSel)\n  if (!canvas) return\n  const existing = (typeof Chart.getChart === 'function') ? Chart.getChart(canvas) : null\n  if (existing && existing.config.type === config.type) {\n    existing.data = config.data; existing.options = config.options\n    existing.resize()\n    existing.update('none')\n  } else {\n    if (existing) { try { existing.destroy() } catch (e) {} }\n    config.options = Object.assign({ animation: false }, config.options)\n    const chart = new Chart(canvas, config)\n    chart.resize()\n  }\n}\nfunction periodRange(state) {\n  let start, end\n  if (state.period === 'custom' && state.customStart && state.customEnd) {\n    start = new Date(`${state.customStart}T00:00:00-03:00`)\n    end = new Date(`${state.customEnd}T23:59:59-03:00`)\n  } else {\n    const p = PERIODS.find(x => x.key === state.period) || PERIODS[0]\n    start = new Date(); start.setHours(0, 0, 0, 0); start.setDate(start.getDate() - (p.days - 1))\n    end = new Date()\n  }\n  return { start, end }\n}\n\nexport default {\n  css: `\n    .wrap { font-family: system-ui, -apple-system, \"Segoe UI\", sans-serif; display:flex; flex-direction:column; gap:14px; padding:4px; box-sizing:border-box; color:#1a1a1a; }\n    .panel { background:#fff; border:1px solid rgba(0,0,0,0.08); border-radius:12px; padding:20px 24px; }\n    .panel h3 { margin:0 0 14px; font-size:15px; font-weight:700; color:#1a1a1a; }\n    .hd-row { display:flex; align-items:center; gap:16px; flex-wrap:wrap; }\n    .hd-row h2 { margin:0; font-size:18px; font-weight:800; }\n    .modebtns { display:flex; gap:6px; }\n    .modebtn { padding:8px 16px; border-radius:8px; border:1px solid rgba(0,0,0,0.1); background:#fff; font-size:13px; font-weight:700; color:#52514e; cursor:pointer; }\n    .modebtn.active { background:${DARK}; color:#fff; border-color:${DARK}; }\n    .periods { display:flex; gap:4px; }\n    .pbtn { padding:6px 12px; border-radius:8px; border:1px solid rgba(0,0,0,0.1); background:#fff; font-size:12px; font-weight:600; color:#52514e; cursor:pointer; }\n    .pbtn.active { background:${DARK}; color:#fff; border-color:${DARK}; }\n    .customrange { display:flex; align-items:center; gap:6px; margin-left:8px; }\n    .customrange input[type=date] { font-size:12px; padding:5px 8px; border-radius:8px; border:1px solid rgba(0,0,0,0.15); color:#52514e; }\n    .rangesep { font-size:11px; color:#898781; }\n    .qchips-row { display:flex; align-items:center; gap:8px; margin-top:14px; padding-top:14px; border-top:1px solid rgba(0,0,0,0.06); flex-wrap:wrap; }\n    .qchips-label { font-size:11.5px; font-weight:700; color:#8a8a8a; text-transform:uppercase; letter-spacing:.02em; }\n    .qchips { display:flex; gap:6px; flex-wrap:wrap; }\n    .qchip { padding:5px 11px; border-radius:999px; border:1px solid rgba(0,0,0,0.12); background:#fff; font-size:11.5px; color:#52514e; cursor:pointer; }\n    .qchip.on { background:rgba(229,0,75,0.08); border-color:${RED}; color:${RED}; font-weight:700; }\n    .toggle-row { display:flex; align-items:center; gap:8px; font-size:12.5px; color:#52514e; }\n    .toggle-row input { cursor:pointer; }\n    .kpirow { display:grid; grid-template-columns:repeat(4,1fr); gap:1px; background:rgba(0,0,0,0.06); border-radius:10px; overflow:hidden; margin-bottom:14px; }\n    .kpi { background:#fff; padding:16px; }\n    .kpi-label { font-size:11px; color:#898781; text-transform:uppercase; letter-spacing:.02em; }\n    .kpi-val { font-size:24px; font-weight:800; color:#1a1a1a; margin-top:4px; font-variant-numeric:tabular-nums; }\n    .kpi-sub { font-size:11px; color:#898781; margin-top:2px; }\n    .qslotsgrid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:10px; }\n    .qslotbox { background:#f7f7f5; border-radius:10px; padding:12px 16px; }\n    .qslotbox .qname { font-size:12px; font-weight:700; color:#1a1a1a; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n    .qslotbox .qbig { font-size:24px; font-weight:800; margin-top:4px; font-variant-numeric:tabular-nums; }\n    .qslotbox .qlbl { font-size:10px; color:#898781; text-transform:uppercase; }\n    .otable { display:flex; flex-direction:column; }\n    .orow { display:grid; grid-template-columns:1.6fr 1fr 1fr 1fr 1.6fr; gap:8px; align-items:center; padding:10px 8px; font-size:12.5px; border-bottom:1px solid rgba(0,0,0,0.05); cursor:pointer; }\n    .orow:hover { background:#f7f7f5; }\n    .orow.selected { background:rgba(229,0,75,0.06); }\n    .orow.ohead { font-size:10.5px; font-weight:700; color:#898781; text-transform:uppercase; letter-spacing:.02em; border-bottom:2px solid rgba(0,0,0,0.08); cursor:default; }\n    .orow.ohead:hover { background:transparent; }\n    .obadge { padding:3px 10px; border-radius:999px; font-size:11px; font-weight:700; display:inline-flex; align-items:center; gap:5px; }\n    .obadge .dot { width:7px; height:7px; border-radius:50%; background:currentColor; }\n    .oslots { display:flex; gap:3px; }\n    .oslot { width:22px; height:16px; border-radius:4px; background:#eee; }\n    .oslot.filled { background:${RED}; }\n    .oqueues { color:#898781; font-size:11.5px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n    .empty { padding:24px; text-align:center; color:#898781; font-size:13px; }\n    .note { font-size:11px; color:#c4c2bc; margin-top:10px; }\n    .minidash { margin-top:10px; border-top:2px solid rgba(0,0,0,0.06); padding-top:16px; }\n    .minidash-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; }\n    .minidash-head h4 { margin:0; font-size:14px; font-weight:700; }\n    .closeMini { cursor:pointer; color:#898781; font-size:18px; }\n    .charts2 { display:grid; grid-template-columns:1fr 1fr; gap:20px; }\n    .chart-box { height:180px; position:relative; overflow:hidden; }\n    .chart-box canvas { width:100% !important; height:100% !important; }\n    .idletable { display:flex; flex-direction:column; }\n    .idlerow { display:grid; grid-template-columns:1.6fr 1fr 1fr 1fr 1fr; gap:8px; align-items:center; padding:10px 8px; font-size:12.5px; border-bottom:1px solid rgba(0,0,0,0.05); }\n    .idlerow.ihead { font-size:10.5px; font-weight:700; color:#898781; text-transform:uppercase; letter-spacing:.02em; border-bottom:2px solid rgba(0,0,0,0.08); }\n    .idlebar-track { display:flex; height:16px; border-radius:4px; overflow:hidden; background:#eee; }\n    .idlebar { height:100%; }\n    .skel { background:linear-gradient(90deg, #eee 25%, #f2f2f0 37%, #eee 63%); background-size:400% 100%; animation:shimmer 1.4s ease infinite; border-radius:6px; }\n    @keyframes shimmer { 0%{background-position:100% 50%;} 100%{background-position:0% 50%;} }\n  `,\n  render(d2) {\n    const viewMode = d2.state.viewMode || 'realtime'\n    const periodBtns = PERIODS.map(p => `<button class=\"pbtn ${d2.state.period === p.key ? 'active' : ''}\" data-period=\"${p.key}\">${p.label}</button>`).join('')\n\n    const header = `\n      <div class=\"panel\">\n        <div class=\"hd-row\">\n          <h2>Monitoramento de Operadores</h2>\n          <div class=\"modebtns\">\n            <button class=\"modebtn ${viewMode === 'realtime' ? 'active' : ''}\" data-mode=\"realtime\">Tempo real</button>\n            <button class=\"modebtn ${viewMode === 'period' ? 'active' : ''}\" data-mode=\"period\">Por período (ociosidade)</button>\n          </div>\n        </div>\n        ${viewMode === 'period' ? `\n        <div class=\"qchips-row\">\n          <div class=\"periods\">${periodBtns}</div>\n          <div class=\"customrange\">\n            <input type=\"date\" class=\"rangeStart\" value=\"${esc(d2.state.customStart || '')}\" />\n            <span class=\"rangesep\">até</span>\n            <input type=\"date\" class=\"rangeEnd\" value=\"${esc(d2.state.customEnd || '')}\" />\n            <button class=\"applyRangeBtn pbtn ${d2.state.period === 'custom' ? 'active' : ''}\">Aplicar</button>\n          </div>\n        </div>` : ''}\n      </div>`\n\n    const k = d2.state.rt\n    const idle = d2.state.idle\n\n    if (viewMode === 'realtime') {\n      if (!k) {\n        return `<div class=\"wrap\">${header}\n          <div class=\"panel\"><h3>Slots disponíveis por fila</h3><div class=\"qslotsgrid\">${Array.from({length:4}).map(() => `<div class=\"qslotbox\"><div class=\"skel\" style=\"width:60%;height:12px;margin-bottom:8px;\"></div><div class=\"skel\" style=\"width:40%;height:24px;\"></div></div>`).join('')}</div></div>\n          <div class=\"panel\"><h3>Operadores ativos</h3>${Array.from({length:6}).map(() => `<div class=\"skel\" style=\"width:100%;height:16px;margin-bottom:10px;\"></div>`).join('')}</div>\n        </div>`\n      }\n      if (k.error) return `<div class=\"wrap\">${header}<div class=\"panel\"><div class=\"empty\">${esc(k.error)}</div></div></div>`\n\n      const qdeselected = d2.state.queueDeselected || {}\n      const qchips = k.allQueues.map(q => `<span class=\"qchip ${qdeselected[q] ? '' : 'on'}\" data-queue=\"${esc(q)}\">${esc(q)}</span>`).join('')\n      const showInactive = !!d2.state.showInactive\n      const selectedQueue = d2.state.selectedQueueFilter || null\n      let opList = k.operators.filter(o => showInactive || (o.status_type === 'online' || o.status_type === 'pause'))\n      if (selectedQueue) opList = opList.filter(o => o.queues.includes(selectedQueue))\n      opList = [...opList].sort((a, b) => statusIndex(a) - statusIndex(b))\n\n      const opRows = opList.map(o => {\n        const meta = STATUS_TYPE_META[o.status_type] || { label: o.status_type || '—', color: GRAY }\n        const label = o.status_type === 'online' ? (o.in_service ? 'Em atendimento' : 'Disponível') : meta.label\n        const color = o.status_type === 'online' ? (o.in_service ? RED : STATUS_TYPE_META.online.color) : meta.color\n        return `<div class=\"orow ${d2.state.selectedAgentId === o.agentId ? 'selected' : ''}\" data-agent=\"${esc(o.agentId)}\">\n          <span>${esc(o.name)}</span>\n          <span><span class=\"obadge\" style=\"background:${color}22; color:${color}\"><span class=\"dot\"></span>${esc(label)}</span></span>\n          <span class=\"oslots\">${[0,1].map(i => `<span class=\"oslot ${i < o.occupiedSlots ? 'filled' : ''}\"></span>`).join('')}</span>\n          <span>${o.transfersToday}</span>\n          <span class=\"oqueues\" title=\"${esc(o.queues.join(', '))}\">${esc(o.queues.join(', ') || '—')}</span>\n        </div>`\n      }).join('')\n\n      const queueList = [...k.allQueues].sort()\n      const slotsHtml = queueList.length ? queueList.map(q => {\n        const info = k.slotsByQueue[q] || { available: 0, agents: 0 }\n        return `<div class=\"qslotbox\"><div class=\"qname\" title=\"${esc(q)}\">${esc(q)}</div><div class=\"qbig\">${info.available}</div><div class=\"qlbl\">slots livres · ${info.agents} operador(es)</div></div>`\n      }).join('') : `<div class=\"empty\">Sem dados de fila no momento.</div>`\n\n      const selected = d2.state.selectedAgentId ? k.operators.find(o => o.agentId === d2.state.selectedAgentId) : null\n      const opKpi = d2.state.operatorKpi\n      const miniDash = selected ? `\n        <div class=\"minidash\">\n          <div class=\"minidash-head\">\n            <h4>Qualidade — ${esc(selected.name)} (hoje)</h4>\n            <span class=\"closeMini\">×</span>\n          </div>\n          ${!opKpi ? `<div class=\"kpirow\" style=\"grid-template-columns:repeat(3,1fr)\">${Array.from({length:3}).map(() => `<div class=\"kpi\"><div class=\"skel\" style=\"width:60%;height:9px;margin-bottom:8px;\"></div><div class=\"skel\" style=\"width:40%;height:20px;\"></div></div>`).join('')}</div>` :\n            opKpi.error ? `<div class=\"empty\">${esc(opKpi.error)}</div>` : `\n            <div class=\"kpirow\" style=\"grid-template-columns:repeat(3,1fr)\">\n              <div class=\"kpi\"><div class=\"kpi-label\">Atendimentos</div><div class=\"kpi-val\">${opKpi.total}</div><div class=\"kpi-sub\">hoje</div></div>\n              <div class=\"kpi\"><div class=\"kpi-label\">Nível de serviço</div><div class=\"kpi-val\">${opKpi.completedCount > 0 ? opKpi.nivelServico + '%' : '—'}</div><div class=\"kpi-sub\">${opKpi.slaOkCount}/${opKpi.completedCount} em até 60s</div></div>\n              <div class=\"kpi\"><div class=\"kpi-label\">Duração média</div><div class=\"kpi-val\">${fmtHMS(opKpi.avgHandleTime)}</div><div class=\"kpi-sub\">por atendimento</div></div>\n            </div>\n            <div class=\"charts2\">\n              <div class=\"chart-box\"><canvas id=\"opClsChart\" width=\"400\" height=\"180\"></canvas></div>\n              <div class=\"chart-box\"><canvas id=\"opStatusChart\" width=\"400\" height=\"180\"></canvas></div>\n            </div>`}\n        </div>` : ''\n\n      return `<div class=\"wrap\">${header}\n        <div class=\"panel\">\n          <h3>Slots disponíveis por fila</h3>\n          <div class=\"qslotsgrid\">${slotsHtml}</div>\n          <div class=\"note\">Aproximação: soma de (2 − ocupados) apenas dos operadores <b>online</b> (pausa não conta como slot livre) que atenderam aquela fila nas últimas 24h. \"Operador(es)\" inclui também quem está em pausa, para dar contexto do time atrás da fila. Capacidade é por operador (2 slots), não exclusiva de uma fila — um operador pode contar para mais de uma.</div>\n        </div>\n        <div class=\"panel\">\n          <h3>Operadores ativos</h3>\n          <div class=\"qchips-row\"><span class=\"qchips-label\">Filtrar por fila:</span><div class=\"qchips\">${qchips}</div></div>\n          <div class=\"toggle-row\" style=\"margin-top:12px;\"><input type=\"checkbox\" class=\"showInactiveCk\" id=\"showInactiveCk\" ${showInactive ? 'checked' : ''} /><label for=\"showInactiveCk\">Mostrar operadores indisponíveis (pausa longa/parado/offline)</label></div>\n          <div class=\"otable\" style=\"margin-top:12px;\">\n            <div class=\"orow ohead\"><span>Operador</span><span>Status</span><span>Slots</span><span>Atend. hoje</span><span>Filas (24h)</span></div>\n            ${opRows || `<div class=\"empty\">Nenhum operador ${showInactive ? '' : 'ativo '}encontrado${selectedQueue ? ` para a fila \"${esc(selectedQueue)}\"` : ''}.</div>`}\n          </div>\n          ${miniDash}\n        </div>\n      </div>`\n    }\n\n    // viewMode === 'period' → idleness report\n    if (!idle) {\n      return `<div class=\"wrap\">${header}<div class=\"panel\"><h3>Relatório de ociosidade</h3>${Array.from({length:6}).map(() => `<div class=\"skel\" style=\"width:100%;height:16px;margin-bottom:10px;\"></div>`).join('')}</div></div>`\n    }\n    if (idle.error) return `<div class=\"wrap\">${header}<div class=\"panel\"><div class=\"empty\">${esc(idle.error)}</div></div></div>`\n\n    const selectedIdle = d2.state.selectedAgentId ? idle.rows.find(r => r.agentId === d2.state.selectedAgentId) : null\n    const opKpi2 = d2.state.operatorKpi\n    const miniDash2 = selectedIdle ? `\n      <div class=\"minidash\">\n        <div class=\"minidash-head\">\n          <h4>Qualidade — ${esc(selectedIdle.name)} (no período selecionado)</h4>\n          <span class=\"closeMini\">×</span>\n        </div>\n        ${!opKpi2 ? `<div class=\"kpirow\" style=\"grid-template-columns:repeat(3,1fr)\">${Array.from({length:3}).map(() => `<div class=\"kpi\"><div class=\"skel\" style=\"width:60%;height:9px;margin-bottom:8px;\"></div><div class=\"skel\" style=\"width:40%;height:20px;\"></div></div>`).join('')}</div>` :\n          opKpi2.error ? `<div class=\"empty\">${esc(opKpi2.error)}</div>` : `\n          <div class=\"kpirow\" style=\"grid-template-columns:repeat(3,1fr)\">\n            <div class=\"kpi\"><div class=\"kpi-label\">Atendimentos</div><div class=\"kpi-val\">${opKpi2.total}</div><div class=\"kpi-sub\">no período</div></div>\n            <div class=\"kpi\"><div class=\"kpi-label\">Nível de serviço</div><div class=\"kpi-val\">${opKpi2.completedCount > 0 ? opKpi2.nivelServico + '%' : '—'}</div><div class=\"kpi-sub\">${opKpi2.slaOkCount}/${opKpi2.completedCount} em até 60s</div></div>\n            <div class=\"kpi\"><div class=\"kpi-label\">Duração média</div><div class=\"kpi-val\">${fmtHMS(opKpi2.avgHandleTime)}</div><div class=\"kpi-sub\">por atendimento</div></div>\n          </div>\n          <div class=\"charts2\">\n            <div class=\"chart-box\"><canvas id=\"opClsChart\" width=\"400\" height=\"180\"></canvas></div>\n            <div class=\"chart-box\"><canvas id=\"opStatusChart\" width=\"400\" height=\"180\"></canvas></div>\n          </div>`}\n      </div>` : ''\n\n    const idleRows = idle.rows.map(r => {\n      const totalSec = Object.values(r.byType).reduce((s, v) => s + v, 0) || 1\n      const bars = ['online', 'pause', 'stop', 'offline'].map(t => {\n        const sec = r.byType[t] || 0\n        const pct = Math.round((sec / totalSec) * 1000) / 10\n        return pct > 0 ? `<div class=\"idlebar\" style=\"width:${pct}%; background:${STATUS_TYPE_META[t].color}\" title=\"${STATUS_TYPE_META[t].label}: ${fmtHMS(sec)} (${pct}%)\"></div>` : ''\n      }).join('')\n      return `<div class=\"idlerow\" data-agent=\"${esc(r.agentId)}\" style=\"cursor:pointer;\">\n        <span>${esc(r.name)}</span>\n        <span>${fmtHMS(r.byType.online || 0)}</span>\n        <span>${fmtHMS(r.byType.pause || 0)}</span>\n        <span>${fmtHMS((r.byType.stop || 0) + (r.byType.offline || 0))}</span>\n        <span><div class=\"idlebar-track\">${bars}</div></span>\n      </div>`\n    }).join('')\n\n    return `<div class=\"wrap\">${header}\n      <div class=\"panel\">\n        <h3>Relatório de ociosidade — tempo por status, no período</h3>\n        <div class=\"idletable\">\n          <div class=\"idlerow ihead\"><span>Operador</span><span>Online</span><span>Pausa</span><span>Parado/Offline</span><span>Distribuição</span></div>\n          ${idleRows || `<div class=\"empty\">Nenhum registro de status no período selecionado.</div>`}\n        </div>\n        <div class=\"note\">Baseado no log de intervalos de status (agent_status_log). Clique num operador para ver a qualidade dele no mesmo período.</div>\n        ${miniDash2}\n      </div>\n    </div>`\n  },\n  async mounted(d2, root) {\n    d2.state.viewMode = 'realtime'\n    d2.state.period = 'hoje'\n    d2.state.customStart = ''\n    d2.state.customEnd = ''\n    d2.state.queueDeselected = {}\n    d2.state.selectedQueueFilter = null\n    d2.state.showInactive = false\n    d2.state.selectedAgentId = null\n\n    d2.on('.modebtn', 'click', (e, el) => {\n      d2.state.viewMode = el.dataset.mode\n      d2.state.selectedAgentId = null\n      d2.state.operatorKpi = null\n      if (el.dataset.mode === 'period') refreshIdle()\n    })\n    d2.on('.pbtn[data-period]', 'click', (e, el) => { d2.state.period = el.dataset.period; d2.state.customStart = ''; d2.state.customEnd = ''; refreshIdle(); if (d2.state.selectedAgentId) refreshOperatorKpi() })\n    d2.on('.applyRangeBtn', 'click', () => {\n      const s = root.querySelector('.rangeStart') && root.querySelector('.rangeStart').value\n      const en = root.querySelector('.rangeEnd') && root.querySelector('.rangeEnd').value\n      if (!s || !en) return\n      d2.state.customStart = s; d2.state.customEnd = en; d2.state.period = 'custom'\n      refreshIdle()\n      if (d2.state.selectedAgentId) refreshOperatorKpi()\n    })\n    d2.on('.qchip', 'click', (e, el) => {\n      const q = el.dataset.queue\n      d2.state.selectedQueueFilter = (d2.state.selectedQueueFilter === q) ? null : q\n    })\n    d2.on('.showInactiveCk', 'change', (e, el) => { d2.state.showInactive = !!el.checked })\n    d2.on('.orow[data-agent]', 'click', (e, el) => {\n      const id = el.dataset.agent\n      d2.state.selectedAgentId = (d2.state.selectedAgentId === id) ? null : id\n      d2.state.operatorKpi = null\n      if (d2.state.selectedAgentId) refreshOperatorKpi()\n    })\n    d2.on('.idlerow[data-agent]', 'click', (e, el) => {\n      const id = el.dataset.agent\n      d2.state.selectedAgentId = (d2.state.selectedAgentId === id) ? null : id\n      d2.state.operatorKpi = null\n      if (d2.state.selectedAgentId) refreshOperatorKpi()\n    })\n    d2.on('.closeMini', 'click', () => { d2.state.selectedAgentId = null; d2.state.operatorKpi = null })\n\n    async function refreshRealtime() {\n      const agentsList = d2.data('agentsList')\n      const opsActivity = d2.data('opsActivity')\n      if (!agentsList) { d2.state.rt = { error: 'Fonte de dados de operadores não conectada.' }; return }\n      try {\n        await agentsList.refresh()\n        const agents = (agentsList.records() || []).map(a => ({\n          agentId: (a.agent && a.agent._id) || a.agent || a._id,\n          name: (a.agent && (a.agent.name || a.agent.email)) || 'Desconhecido',\n          status_type: a.status_type,\n          in_service: !!a.in_service,\n        })).filter(a => a.agentId)\n\n        let queuesByAgent = {}\n        let inProgressByAgent = {}\n        let transfersTodayByAgent = {}\n        if (opsActivity) {\n          const dayAgo = new Date(Date.now() - 24 * 3600 * 1000)\n          const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0)\n\n          try {\n            opsActivity.filter({ env: 'PRD', createdAt: { $gte: todayStart.toISOString() }, agent: { $exists: true, $ne: null } })\n            await opsActivity.refresh()\n            const { data: todayData } = await opsActivity.aggregate([\n              { $group: { _id: '$agent', count: { $sum: 1 } } },\n            ])\n            for (const r of (todayData || [])) { if (r._id) transfersTodayByAgent[r._id] = r.count }\n          } catch (e) { transfersTodayByAgent = {} }\n\n          opsActivity.filter({ env: 'PRD', createdAt: { $gte: dayAgo.toISOString() } })\n          await opsActivity.refresh()\n          const { data: extra } = await opsActivity.aggregate([{ $facet: {\n            queuesByAgent: [\n              { $match: { queue_label: { $exists: true, $ne: null }, agent: { $exists: true, $ne: null } } },\n              { $group: { _id: { agent: '$agent', queue: '$queue_label' }, count: { $sum: 1 } } },\n            ],\n            inProgressByAgent: [\n              { $match: { transfer_status: 'in_progress', agent: { $exists: true, $ne: null } } },\n              { $group: { _id: '$agent', count: { $sum: 1 } } },\n            ],\n          } }])\n          const ex = extra[0] || {}\n          for (const r of (ex.queuesByAgent || [])) {\n            const aid = r._id && r._id.agent\n            const q = r._id && r._id.queue\n            if (!aid || !q) continue\n            queuesByAgent[aid] = queuesByAgent[aid] || new Set()\n            queuesByAgent[aid].add(q)\n          }\n          for (const r of (ex.inProgressByAgent || [])) { if (r._id) inProgressByAgent[r._id] = r.count }\n        }\n\n        const operators = agents.map(a => ({\n          ...a,\n          queues: Array.from(queuesByAgent[a.agentId] || []),\n          occupiedSlots: Math.min(2, inProgressByAgent[a.agentId] || 0),\n          transfersToday: transfersTodayByAgent[a.agentId] || 0,\n        }))\n\n        const allQueuesSet = new Set()\n        operators.forEach(o => o.queues.forEach(q => allQueuesSet.add(q)))\n        const allQueues = Array.from(allQueuesSet)\n\n        const slotsByQueue = {}\n        for (const q of allQueues) {\n          const servingActive = operators.filter(o => (o.status_type === 'online' || o.status_type === 'pause') && o.queues.includes(q))\n          const servingAvailable = servingActive.filter(o => o.status_type === 'online')\n          const available = servingAvailable.reduce((s, o) => s + (2 - o.occupiedSlots), 0)\n          slotsByQueue[q] = { available, agents: servingActive.length }\n        }\n\n        d2.state.rt = { operators, allQueues, slotsByQueue }\n      } catch (e) {\n        d2.state.rt = { error: 'Erro ao carregar operadores: ' + (e && e.message || String(e)) }\n      }\n    }\n\n    async function refreshOperatorKpi() {\n      const operatorKpi = d2.data('operatorKpi')\n      const agentId = d2.state.selectedAgentId\n      if (!operatorKpi || !agentId) { d2.state.operatorKpi = null; return }\n      d2.state.operatorKpi = null\n      try {\n        const { start, end } = periodRange(d2.state)\n        operatorKpi.filter({ env: 'PRD', agent: agentId, createdAt: { $gte: start.toISOString(), $lte: end.toISOString() } })\n        await operatorKpi.refresh()\n        const agg = (operatorKpi.raw && operatorKpi.raw.aggregated) || []\n        const pick = (row, ...keys) => { for (const k of keys) { if (row && row[k] !== undefined) return row[k] }; return undefined }\n        const groupKey = (row, ...keys) => pick(row, ...keys) ?? (row && row._id && pick(row._id, ...keys)) ?? (row && row._id)\n        const byStatusRaw = (agg[0] && agg[0].data) || []\n        const byClassificationRaw = (agg[1] && agg[1].data) || []\n        const completedSumRow = ((agg[5] && agg[5].data) || [])[0] || {}\n        const byStatus = byStatusRaw.map(r => ({ key: groupKey(r, 'transfer_status'), total: r.total })).filter(r => STATUS_META[r.key])\n        const byClassification = byClassificationRaw.map(r => ({ key: groupKey(r, 'classification'), total: r.total })).filter(r => CLS_META[r.key])\n        const completedCount = completedSumRow.total ?? 0\n        const sumQT = pick(completedSumRow, 'sum_queue_time', 'sumQT', 'sumQueueTime') ?? 0\n        const sumAgentTime = pick(completedSumRow, 'sum_agent_time', 'sumAgentTime') ?? 0\n        const total = byStatus.reduce((s, r) => s + r.total, 0)\n        const { data: extra } = await operatorKpi.aggregate([{ $facet: {\n          slaOk: [{ $match: { transfer_status: 'completed', queue_time: { $lt: 60 } } }, { $count: 'total' }],\n        } }])\n        const ex = extra[0] || {}\n        const slaOkCount = (ex.slaOk && ex.slaOk[0] && ex.slaOk[0].total) || 0\n        d2.state.operatorKpi = {\n          total, completedCount, slaOkCount,\n          nivelServico: completedCount > 0 ? Math.round((slaOkCount / completedCount) * 100) : 0,\n          tme: completedCount > 0 ? sumQT / completedCount : 0,\n          avgHandleTime: completedCount > 0 ? sumAgentTime / completedCount : 0,\n          byStatus, byClassification,\n        }\n      } catch (e) {\n        d2.state.operatorKpi = { error: 'Erro ao carregar qualidade do operador: ' + (e && e.message || String(e)) }\n      }\n    }\n\n    async function refreshIdle() {\n      const statusLog = d2.data('statusLog')\n      const statusDefs = d2.data('statusDefs')\n      const agentNames = d2.data('agentNames')\n      if (!statusLog || !statusDefs) { d2.state.idle = { error: 'Fontes de log de status não conectadas.' }; return }\n      d2.state.idle = null\n      try {\n        const { start, end } = periodRange(d2.state)\n        await statusDefs.refresh()\n        const typeById = {}\n        for (const s of (statusDefs.records() || [])) { if (s && s._id) typeById[s._id] = s.type }\n\n        let nameById = {}\n        if (agentNames) {\n          await agentNames.refresh()\n          for (const a of (agentNames.records() || [])) { if (a && a._id) nameById[a._id] = a.name || a.email || a._id }\n        }\n\n        statusLog.filter({ finished: { $gte: start.toISOString(), $lte: end.toISOString() } })\n        await statusLog.refresh()\n        const { data: grouped } = await statusLog.aggregate([\n          { $match: { finished: { $gte: start.toISOString(), $lte: end.toISOString() } } },\n          { $group: { _id: { agent: '$agent', status: '$status' }, totalDuration: { $sum: '$duration' } } },\n        ])\n        const byAgent = {}\n        for (const r of grouped) {\n          const aid = r._id && r._id.agent\n          const sid = r._id && r._id.status\n          if (!aid || !sid) continue\n          const type = typeById[sid] || 'stop'\n          byAgent[aid] = byAgent[aid] || { online: 0, pause: 0, stop: 0, offline: 0 }\n          byAgent[aid][type] = (byAgent[aid][type] || 0) + (r.totalDuration || 0)\n        }\n        const rows = Object.entries(byAgent).map(([agentId, byType]) => ({\n          agentId, name: nameById[agentId] || String(agentId).slice(-6), byType,\n        })).sort((a, b) => (b.byType.online || 0) - (a.byType.online || 0))\n        d2.state.idle = { rows }\n      } catch (e) {\n        d2.state.idle = { error: 'Erro ao carregar relatório de ociosidade: ' + (e && e.message || String(e)) }\n      }\n    }\n\n    root._refreshRealtime = refreshRealtime\n    refreshRealtime()\n    const rtId = setInterval(refreshRealtime, 10000)\n    d2.onUnmount(() => { clearInterval(rtId) })\n  },\n  updated(d2, root) {\n    try {\n      const Chart = resolveChart(d2.lib('chart.js@4.4.1/auto'))\n      const opKpi = d2.state.operatorKpi\n      if (!Chart || !opKpi || opKpi.error) return\n      const niceMax = (arr) => { const m = Math.max(0, ...arr); return m === 0 ? 1 : Math.ceil(m * 1.15) }\n\n      const clsItems = opKpi.byClassification.map(c => ({ total: c.total, meta: CLS_META[c.key] || { label: c.key, color: GRAY } }))\n      upsertChart(root, Chart, 'opCls', '#opClsChart', {\n        type: 'bar',\n        data: { labels: clsItems.map(c => c.meta.label), datasets: [{ data: clsItems.map(c => c.total), backgroundColor: clsItems.map(c => c.meta.color), borderRadius: 4, maxBarThickness: 40 }] },\n        options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } }, plugins: { legend: { display: false } }, scales: { x: { ticks: { display: false } }, y: { beginAtZero: true, suggestedMax: niceMax(clsItems.map(c => c.total)) } } },\n      })\n      const statusItems = opKpi.byStatus.map(c => ({ total: c.total, meta: STATUS_META[c.key] || { label: c.key, color: GRAY } }))\n      upsertChart(root, Chart, 'opStatus', '#opStatusChart', {\n        type: 'bar',\n        data: { labels: statusItems.map(c => c.meta.label), datasets: [{ data: statusItems.map(c => c.total), backgroundColor: statusItems.map(c => c.meta.color), borderRadius: 4, maxBarThickness: 40 }] },\n        options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 10 } }, plugins: { legend: { display: false } }, scales: { x: { ticks: { display: false } }, y: { beginAtZero: true, suggestedMax: niceMax(statusItems.map(c => c.total)) } } },\n      })\n    } catch (e) { /* fail-silent: chart render error should never break the dashboard */ }\n  },\n}"}},"agentsList":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentsList"}}}},"agentNames":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentNames"}}}},"opsActivity":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"opsActivity"}}}},"operatorKpi":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"operatorKpi"}}}},"statusLog":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"statusLog"}}}},"statusDefs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"statusDefs"}}}}}}}}}},{"_id":"6a88804562727a90a8f88705","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"u1ehKTIeoLPe5jOOIczp7","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const css = `\n    .wrap { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1a1a1a; }\n    .panel { background: #fff; border-radius: 14px; padding: 20px 22px; margin-bottom: 14px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }\n    .hd-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }\n    h2 { font-size: 19px; margin: 0; }\n    h3 { font-size: 14.5px; margin: 0 0 12px; }\n    .periods { display: flex; gap: 6px; flex-wrap: wrap; }\n    .pbtn { font-size: 12px; padding: 6px 13px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); background: #fff; color: #52514e; cursor: pointer; }\n    .pbtn.active { background: #363636; color: #fff; border-color: #363636; }\n    .customrange { display: flex; align-items: center; gap: 8px; }\n    .customrange input { font-size: 12px; padding: 5px 8px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); }\n    .rangesep { font-size: 11.5px; color: #898781; }\n    .applyRangeBtn { font-size: 12px; padding: 6px 13px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.12); background: #fff; color: #52514e; cursor: pointer; }\n    .applyRangeBtn.active { background: #363636; color: #fff; border-color: #363636; }\n    .kpirow { display: grid; grid-template-columns: repeat(4,1fr); gap: 1px; background: rgba(0,0,0,0.06); border-radius: 10px; overflow: hidden; }\n    .kpi { background: #fff; padding: 16px; }\n    .kpi-label { font-size: 11px; color: #898781; text-transform: uppercase; letter-spacing: .02em; }\n    .kpi-val { font-size: 24px; font-weight: 800; color: #1a1a1a; margin-top: 4px; font-variant-numeric: tabular-nums; }\n    .kpi-sub { font-size: 11px; color: #898781; margin-top: 2px; }\n    .sesstable { display: flex; flex-direction: column; }\n    .sessrow { display: grid; grid-template-columns: 0.7fr 0.9fr 1.3fr 1.8fr 1.6fr 0.9fr; gap: 8px; align-items: center; padding: 10px 6px; font-size: 12.5px; border-bottom: 1px solid rgba(0,0,0,0.05); cursor: pointer; }\n    .sessrow:hover { background: #f7f7f5; }\n    .sessrow.shead { font-size: 10.5px; font-weight: 700; color: #898781; text-transform: uppercase; letter-spacing: .02em; border-bottom: 2px solid rgba(0,0,0,0.08); cursor: default; }\n    .sessrow.shead:hover { background: transparent; }\n    .sess-badge { background: rgba(229,0,75,0.1); color: #e5004b; border-radius: 6px; padding: 2px 7px; font-size: 11px; font-weight: 600; }\n    .sess-badge.warn { background: rgba(243,191,58,0.15); color: #a3781a; }\n    .sess-detail { background: #faf9f7; border-radius: 8px; padding: 8px 12px; margin: 2px 0 8px; }\n    .sess-detail-row { display: grid; grid-template-columns: 1fr 1.6fr 1.6fr 1fr 1fr; gap: 8px; font-size: 11.5px; color: #52514e; padding: 5px 0; border-bottom: 1px solid rgba(0,0,0,0.04); }\n    .sess-detail-row:last-child { border-bottom: none; }\n    .sess-detail-row.resolver { color: #1a1a1a; font-weight: 600; }\n    .empty { padding: 24px; text-align: center; color: #898781; font-size: 13px; }\n    .note { font-size: 11px; color: #c4c2bc; margin-top: 10px; }\n    .skel { background: linear-gradient(90deg,#eee,#f5f5f5,#eee); border-radius: 6px; }\n`\n\nfunction esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;') }\nfunction fmtHMS(sec) {\n  sec = Math.max(0, Math.round(sec || 0))\n  const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60\n  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`\n}\nfunction fmtDT(d) {\n  const dt = new Date(d)\n  if (isNaN(dt.getTime())) return '—'\n  const pad = n => String(n).padStart(2, '0')\n  return `${pad(dt.getDate())}/${pad(dt.getMonth() + 1)} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`\n}\nconst CLS_LABEL = {\n  ended_by_agent: 'Encerrado pelo operador',\n  ended_by_user: 'Encerrado pelo usuário',\n  abandoned: 'Abandonado',\n  outside_hours: 'Fora do horário',\n  inactivity_agent: 'Inatividade do operador',\n  inactivity_user: 'Inatividade do usuário',\n  unavailable_agent: 'Operador indisponível',\n  unavailable_queue: 'Fila indisponível',\n  transferred: 'Transferido',\n}\nconst PERIODS = [\n  { key: 'hoje', label: 'Hoje', days: 1 },\n  { key: '3d', label: '-3D', days: 3 },\n  { key: '7d', label: '-7D', days: 7 },\n  { key: '15d', label: '-15D', days: 15 },\n  { key: '30d', label: '-30D', days: 30 },\n]\nfunction periodRange(state) {\n  if (state.period === 'custom' && state.customStart && state.customEnd) {\n    return { start: new Date(`${state.customStart}T00:00:00-03:00`), end: new Date(`${state.customEnd}T23:59:59-03:00`) }\n  }\n  const p = PERIODS.find(x => x.key === state.period) || PERIODS[2]\n  const start = new Date(); start.setHours(0, 0, 0, 0); start.setDate(start.getDate() - (p.days - 1))\n  return { start, end: new Date() }\n}\n\nexport default {\n  css,\n  render(d2) {\n    const periodBtns = PERIODS.map(p => `<button class=\"pbtn ${d2.state.period === p.key ? 'active' : ''}\" data-period=\"${p.key}\">${p.label}</button>`).join('')\n    const header = `<div class=\"panel\">\n      <div class=\"hd-row\">\n        <h2>Sessões com múltiplas transferências</h2>\n        <div class=\"hd-row\" style=\"gap:10px\">\n          <div class=\"periods\">${periodBtns}</div>\n          <div class=\"customrange\">\n            <input type=\"date\" class=\"rangeStart\" value=\"${esc(d2.state.customStart || '')}\" />\n            <span class=\"rangesep\">até</span>\n            <input type=\"date\" class=\"rangeEnd\" value=\"${esc(d2.state.customEnd || '')}\" />\n            <button class=\"applyRangeBtn ${d2.state.period === 'custom' ? 'active' : ''}\">Aplicar</button>\n          </div>\n        </div>\n      </div>\n    </div>`\n\n    const k = d2.state.data\n    if (!k) {\n      return `<div class=\"wrap\">${header}\n        <div class=\"panel\"><div class=\"kpirow\">${Array.from({ length: 4 }).map(() => `<div class=\"kpi\"><div class=\"skel\" style=\"width:60%;height:9px;margin-bottom:8px;\"></div><div class=\"skel\" style=\"width:40%;height:20px;\"></div></div>`).join('')}</div></div>\n        <div class=\"panel\">${Array.from({ length: 6 }).map(() => `<div class=\"skel\" style=\"width:100%;height:16px;margin-bottom:10px;\"></div>`).join('')}</div>\n      </div>`\n    }\n    if (k.error) return `<div class=\"wrap\">${header}<div class=\"panel\"><div class=\"empty\">${esc(k.error)}</div></div></div>`\n\n    const rows = k.sessions.map(s => {\n      const isOpen = d2.state.sessionPeekId === s.sessionId\n      const rowHtml = `<div class=\"sessrow\" data-session=\"${esc(s.sessionId)}\">\n        <span>${esc(String(s.sessionId).slice(-6))}</span>\n        <span><b>${s.total}</b></span>\n        <span>${s.wasted > 0 ? `<span class=\"sess-badge\">${s.wasted} sem atendimento</span>` : '—'}</span>\n        <span title=\"${esc(s.queues.join(', '))}\">${esc(s.queues.slice(0, 2).join(', '))}${s.queues.length > 2 ? '…' : ''}</span>\n        <span>${s.resolverAgent ? esc(s.resolverAgent) : '<i style=\"color:#c4c2bc\">nenhum</i>'} ${!s.resolved ? `<span class=\"sess-badge warn\" title=\"Nem a última tentativa foi encerrada pelo operador\">não resolvido</span>` : ''}</span>\n        <span>${esc(fmtDT(s.lastCreatedAt))}</span>\n      </div>`\n      const detailHtml = isOpen ? `<div class=\"sess-detail\">\n        ${s.events.map(ev => `<div class=\"sess-detail-row ${(s.resolverAgent && ev === s.resolverEvent) ? 'resolver' : ''}\">\n          <span>${esc(fmtDT(ev.createdAt))}</span>\n          <span>${esc(ev.agent_name || '—')}</span>\n          <span>${esc(CLS_LABEL[ev.classification] || ev.classification || '—')}</span>\n          <span>fila ${fmtHMS(ev.queue_time)}</span>\n          <span>atend. ${fmtHMS(ev.agent_time)}</span>\n        </div>`).join('')}\n      </div>` : ''\n      return rowHtml + detailHtml\n    }).join('')\n\n    return `<div class=\"wrap\">${header}\n      <div class=\"panel\">\n        <div class=\"kpirow\">\n          <div class=\"kpi\"><div class=\"kpi-label\">Sessões no período</div><div class=\"kpi-val\">${k.totalSessions}</div></div>\n          <div class=\"kpi\"><div class=\"kpi-label\">Com +1 transferência</div><div class=\"kpi-val\">${k.sessionsWithMultiple}</div><div class=\"kpi-sub\">${k.totalSessions > 0 ? Math.round((k.sessionsWithMultiple / k.totalSessions) * 1000) / 10 : 0}% das sessões</div></div>\n          <div class=\"kpi\"><div class=\"kpi-label\">Transferências totais</div><div class=\"kpi-val\">${k.totalTransfers}</div></div>\n          <div class=\"kpi\"><div class=\"kpi-label\">Zero-engajamento</div><div class=\"kpi-val\">${k.wastedOverall}</div><div class=\"kpi-sub\">${k.totalTransfers > 0 ? Math.round((k.wastedOverall / k.totalTransfers) * 1000) / 10 : 0}% das transferências</div></div>\n        </div>\n      </div>\n      <div class=\"panel\">\n        <h3>Sessões com mais de uma transferência no período</h3>\n        ${k.sessions.length ? `<div class=\"sesstable\">\n          <div class=\"sessrow shead\"><span>Sessão</span><span>Transf.</span><span>Zero-engaj.</span><span>Filas</span><span>Último operador (não encerrado pelo usuário)</span><span>Última em</span></div>\n          ${rows}\n        </div>` : `<div class=\"empty\">Nenhuma sessão com mais de uma transferência no período selecionado.</div>`}\n        ${k.truncated ? `<div class=\"note\">Mostrando as ${k.sessions.length} sessões com mais transferências no período (lista truncada — refine o período para ver o restante).</div>` : ''}\n        <div class=\"note\">\"Zero-engajamento\" = transferência que terminou sem o operador chegar a atender (encerrado pelo usuário, com 0s de atendimento). \"Último operador\" = o último a atender essa sessão numa transferência que não terminou encerrada pelo usuário — ou seja, quem efetivamente resolveu (ou pelo menos não foi o cliente desistindo). Clique numa sessão para ver a linha do tempo completa.</div>\n      </div>\n    </div>`\n  },\n  async mounted(d2, root) {\n    d2.state.period = '7d'\n    d2.state.customStart = ''\n    d2.state.customEnd = ''\n    d2.state.sessionPeekId = null\n    d2.state.data = null\n\n    d2.on('.pbtn[data-period]', 'click', (e, el) => { d2.state.period = el.dataset.period; d2.state.customStart = ''; d2.state.customEnd = ''; d2.state.data = null; refreshAll() })\n    d2.on('.applyRangeBtn', 'click', () => {\n      const s = root.querySelector('.rangeStart') && root.querySelector('.rangeStart').value\n      const en = root.querySelector('.rangeEnd') && root.querySelector('.rangeEnd').value\n      if (!s || !en) return\n      d2.state.customStart = s\n      d2.state.customEnd = en\n      d2.state.period = 'custom'\n      d2.state.data = null\n      refreshAll()\n    })\n    d2.on('.sessrow[data-session]', 'click', (e, el) => {\n      const id = el.dataset.session\n      d2.state.sessionPeekId = (d2.state.sessionPeekId === id) ? null : id\n    })\n\n    async function refreshAll() {\n      const transfers = d2.data('transfers')\n      if (!transfers) { d2.state.data = { error: 'Fonte de dados não conectada.' }; return }\n      try {\n        const { start, end } = periodRange(d2.state)\n        transfers.filter({ env: 'PRD', session: { $exists: true, $ne: null }, createdAt: { $gte: start.toISOString(), $lte: end.toISOString() } })\n        await transfers.refresh()\n        const { data } = await transfers.aggregate([{ $facet: {\n          bySession: [\n            { $group: { _id: '$session', total: { $sum: 1 }, wasted: { $sum: { $cond: [{ $and: [{ $eq: ['$classification', 'ended_by_user'] }, { $eq: ['$agent_time', 0] }] }, 1, 0] } }, queues: { $addToSet: '$queue_label' }, lastCreatedAt: { $max: '$createdAt' }, events: { $push: { createdAt: '$createdAt', agent_name: '$agent_name', classification: '$classification', agent_time: '$agent_time', queue_time: '$queue_time' } } } },\n            { $match: { total: { $gt: 1 } } },\n            { $sort: { total: -1 } },\n            { $limit: 300 },\n          ],\n          overallStats: [\n            { $group: { _id: '$session', total: { $sum: 1 } } },\n            { $group: { _id: null, totalSessions: { $sum: 1 }, sessionsWithMultiple: { $sum: { $cond: [{ $gt: ['$total', 1] }, 1, 0] } }, totalTransfers: { $sum: '$total' } } },\n          ],\n          wastedOverall: [\n            { $match: { classification: 'ended_by_user', agent_time: 0 } },\n            { $count: 'total' },\n          ],\n        } }])\n        const ex = data[0] || {}\n        const sessions = (ex.bySession || []).map(r => {\n          const events = (r.events || []).slice().sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))\n          let resolverEvent = null\n          for (let i = events.length - 1; i >= 0; i--) { if (events[i].classification !== 'ended_by_user') { resolverEvent = events[i]; break } }\n          return {\n            sessionId: r._id,\n            total: r.total,\n            wasted: r.wasted || 0,\n            queues: (r.queues || []).filter(Boolean),\n            lastCreatedAt: r.lastCreatedAt,\n            events,\n            resolverEvent,\n            resolverAgent: resolverEvent ? (resolverEvent.agent_name || null) : null,\n            resolved: !!resolverEvent,\n          }\n        })\n        const st = (ex.overallStats && ex.overallStats[0]) || {}\n        d2.state.data = {\n          sessions,\n          truncated: sessions.length >= 300,\n          totalSessions: st.totalSessions || 0,\n          sessionsWithMultiple: st.sessionsWithMultiple || 0,\n          totalTransfers: st.totalTransfers || 0,\n          wastedOverall: (ex.wastedOverall && ex.wastedOverall[0] && ex.wastedOverall[0].total) || 0,\n        }\n      } catch (e) {\n        d2.state.data = { error: 'Erro ao carregar: ' + (e && e.message || String(e)) }\n      }\n    }\n\n    root._refreshAll = refreshAll\n    refreshAll()\n  },\n}\n"}},"transfers":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"transfers"}}}}}}}}}},{"_id":"6aa2fe079019f2d0560a6838","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"OpsCF_Login_Component","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"export default {\n  css: `\n    :host{all:initial}\n    *{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}\n    .wrap{display:flex;align-items:center;justify-content:center;height:100%;min-height:100%;padding:24px;background:#faf9f7;color:#1a1714;font-size:14px}\n    .card{width:100%;max-width:400px;background:#fff;border:1px solid #e6e1db;border-radius:14px;padding:32px 30px;box-shadow:0 8px 30px rgba(26,23,20,.06)}\n    .brand{display:flex;align-items:center;gap:12px;margin-bottom:24px}\n    .logo{width:40px;height:40px;border-radius:10px;background:#EB6619;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:12px;letter-spacing:.04em;flex:0 0 40px}\n    .brand h1{margin:0;font-size:18px;line-height:1.2}\n    .brand p{margin:3px 0 0;font-size:12px;color:#6b625a}\n    .fld{display:flex;flex-direction:column;gap:5px;margin-bottom:14px}\n    .fld label{font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:#6b625a;font-weight:600}\n    .fld input{padding:10px 12px;border:1px solid #e6e1db;border-radius:8px;font-size:14px;background:#fff;color:#1a1714;width:100%;font-family:inherit}\n    .fld input:focus{outline:2px solid #EB6619;outline-offset:-1px;border-color:transparent}\n    .btn{width:100%;padding:11px 16px;border:none;border-radius:8px;background:#EB6619;color:#fff;font-size:14px;font-weight:600;cursor:pointer;margin-top:4px;font-family:inherit}\n    .btn:hover{background:#d25a13}\n    .btn[disabled]{opacity:.6;cursor:default}\n    .btn.sec{background:#fff;color:#6b625a;border:1px solid #e6e1db;font-weight:500;margin-top:8px}\n    .msg{margin:0 0 14px;padding:9px 12px;border-radius:8px;font-size:12.5px;line-height:1.45}\n    .msg.err{background:#fdecec;color:#a8321f}\n    .msg.ok{background:#fdf0e7;color:#8a4a1c}\n    .foot{margin-top:18px;font-size:11px;color:#8a8078;text-align:center;line-height:1.5}\n  `,\n\n  render(d2) {\n    const st = d2.state\n    const auth = d2.auth(d2.param('auth'))\n    const user = pickUser(auth)\n\n    if (user && user.email && !st.err && !st.forcarForm) {\n      return `<div class=\"wrap\"><div class=\"card\">\n        ${brand()}\n        <div class=\"msg ok\">Voce ja esta conectado como <b>${esc(user.email)}</b>.</div>\n        <button class=\"btn b-in\">Entrar no Ops CF</button>\n        <button class=\"btn sec b-outro\">Entrar com outra conta</button>\n      </div></div>`\n    }\n\n    return `<div class=\"wrap\"><div class=\"card\">\n      ${brand()}\n      ${st.err ? `<div class=\"msg err\">${esc(st.err)}</div>` : ''}\n      <form class=\"frm\" novalidate>\n        <div class=\"fld\"><label>E-mail</label><input class=\"f-email\" type=\"email\" autocomplete=\"username\" value=\"${esc(st.email || '')}\"></div>\n        <div class=\"fld\"><label>Senha</label><input class=\"f-pass\" type=\"password\" autocomplete=\"current-password\"></div>\n        <button class=\"btn b-go\" type=\"submit\" ${st.busy ? 'disabled' : ''}>${st.busy ? 'Entrando...' : 'Entrar'}</button>\n      </form>\n      <div class=\"foot\">Acesso restrito a equipe CF.<br>Sem cadastro? Solicite ao administrador.</div>\n    </div></div>`\n  },\n\n  mounted(d2) {\n    const home = () => d2.param('homePath') || '/'\n    const nav = () => d2.router(d2.param('routerRef'))\n\n    // A reference da estrategia e obrigatoria no body do login (runtime exige\n    // authenticationStrategyReference + credentials). Vem pelo param authRef;\n    // como fallback tenta ler do proprio modulo.\n    const strategyRef = (auth) => {\n      const p = d2.param('authRef')\n      if (p) return String(p)\n      const raw = auth && auth.raw\n      return (raw && (raw.reference || (raw.params && raw.params.reference))) || ''\n    }\n\n    const entrar = async (e) => {\n      if (e && e.preventDefault) e.preventDefault()\n      const g = c => { const el = d2.$('.' + c); return el ? String(el.value).trim() : '' }\n      const email = g('f-email')\n      const password = g('f-pass')\n      d2.state.email = email\n      if (!email || !password) { d2.state.err = 'Informe e-mail e senha.'; return }\n\n      d2.setState({ busy: true, err: '' })\n      const auth = d2.auth(d2.param('auth'))\n      if (!auth) { d2.setState({ busy: false, err: 'Estrategia de autenticacao nao configurada neste componente.' }); return }\n      const ref = strategyRef(auth)\n      if (!ref) { d2.setState({ busy: false, err: 'Reference da estrategia (authRef) nao configurada.' }); return }\n\n      let erro = ''\n      let res = null\n      try {\n        res = await auth.login({\n          authenticationStrategyReference: ref,\n          credentials: { email: email, password: password },\n        })\n      } catch (err) {\n        erro = mensagemErro(err)\n      }\n      if (!erro && res && typeof res === 'object') {\n        if (res.error || res.name === 'ValidationError' || res.name === 'NotAuthenticated' || (res.code && res.code >= 400)) erro = mensagemErro(res)\n      }\n\n      if (erro) { d2.setState({ busy: false, err: erro }); return }\n      d2.setState({ busy: false, err: '', forcarForm: false })\n      nav().goTo(home())\n    }\n\n    d2.on('.frm', 'submit', entrar)\n    d2.on('.b-go', 'click', entrar)\n    d2.on('.b-in', 'click', () => nav().goTo(home()))\n    d2.on('.b-outro', 'click', async () => {\n      await sair(d2.auth(d2.param('auth')))\n      d2.setState({ forcarForm: true, email: '' })\n    })\n  },\n}\n\nfunction pickUser(auth) {\n  if (!auth) return null\n  const bases = []\n  try { bases.push(auth.user) } catch (e) { /* noop */ }\n  try { bases.push(auth.session) } catch (e) { /* noop */ }\n  const cands = []\n  for (const b of bases) {\n    if (!b || typeof b !== 'object') continue\n    cands.push(b.user, b.credentials && b.credentials.user, b.credentials, b.data, b)\n  }\n  for (const c of cands) {\n    if (c && typeof c === 'object' && (c.email || c.name)) return c\n  }\n  return null\n}\n\nasync function sair(auth) {\n  if (!auth) return\n  try { await auth.logout(auth.session); return } catch (e) { /* tenta sem argumento */ }\n  try { await auth.logout() } catch (e) { /* segue */ }\n}\n\nfunction mensagemErro(err) {\n  const code = err && (err.code || (err.error && err.error.code))\n  const name = err && (err.name || (err.error && err.error.name)) || ''\n  if (code === 401 || /NotAuthenticated/i.test(name)) return 'E-mail ou senha invalidos.'\n  const msg = err && (err.message || (err.error && err.error.message))\n  return msg ? `Falha no login: ${msg}` : 'E-mail ou senha invalidos.'\n}\n\nfunction brand() {\n  return `<div class=\"brand\"><div class=\"logo\">CF</div><div><h1>Ops CF</h1><p>Ferramentas internas de atendimento</p></div></div>`\n}\n\nfunction esc(s) {\n  return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n"}},"auth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"auth"}}}},"authRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"authRef"}}}},"routerRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"routerRef"}}}},"homePath":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"homePath"}}}}}}}}}},{"_id":"6aa2fe08e9f79ba536d1aebf","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"OpsCF_Menu_Component","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Fonte unica dos itens do menu. Para adicionar uma tela nova:\n// 1) crie o artboard + a rota no Router; 2) inclua uma linha aqui.\n// A restricao de acesso fica na policy da rota, nao aqui.\nconst LINKS = [\n  { href: '/', label: 'Historico de Mensagens', glyph: '&#128172;' },\n]\n\nexport default {\n  css: `\n    :host{all:initial}\n    *{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}\n    .side{display:flex;flex-direction:column;height:100%;background:#1a1714;color:#f0ebe4;font-size:14px}\n    .brand{display:flex;align-items:center;gap:10px;padding:18px 16px 14px;border-bottom:1px solid rgba(255,255,255,.08)}\n    .logo{width:32px;height:32px;border-radius:9px;background:#EB6619;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:11px;letter-spacing:.04em;flex:0 0 32px}\n    .brand b{font-size:15px;display:block;line-height:1.1}\n    .brand small{font-size:10.5px;color:#a89e94;letter-spacing:.06em;text-transform:uppercase}\n    .sec{padding:14px 16px 6px;font-size:10px;letter-spacing:.09em;text-transform:uppercase;color:#8a8078;font-weight:700}\n    .nav{flex:1;overflow:auto;padding:0 10px}\n    .item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:9px;cursor:pointer;color:#d8d1c9;font-size:13.5px;margin-bottom:2px}\n    .item:hover{background:rgba(255,255,255,.06);color:#fff}\n    .item[data-active]{background:#EB6619;color:#fff;font-weight:600}\n    .item .g{width:20px;text-align:center;font-size:15px}\n    .foot{padding:14px 16px;border-top:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:space-between;gap:10px}\n    .foot small{font-size:10.5px;color:#8a8078}\n    .b-out{background:transparent;border:1px solid rgba(255,255,255,.18);color:#d8d1c9;border-radius:7px;padding:6px 12px;font-size:11.5px;cursor:pointer;font-family:inherit}\n    .b-out:hover{border-color:#EB6619;color:#fff}\n  `,\n\n  render(d2) {\n    const nav = d2.router(d2.param('routerRef'))\n    const here = normalize((nav && nav.path) || '/')\n    const ativo = pickActive(LINKS, here)\n\n    return `<div class=\"side\">\n      <div class=\"brand\"><div class=\"logo\">CF</div><div><b>Ops CF</b><small>Ferramentas internas</small></div></div>\n      <div class=\"sec\">Atendimento</div>\n      <div class=\"nav\">${LINKS.map(l => `\n        <div class=\"item\" data-key=\"${esc(l.href)}\" data-href=\"${esc(l.href)}\" ${ativo === l.href ? 'data-active' : ''}>\n          <span class=\"g\">${l.glyph || ''}</span><span>${esc(l.label)}</span>\n        </div>`).join('')}\n      </div>\n      <div class=\"foot\"><small>Natura Brasil CF</small><button class=\"b-out\" title=\"Encerrar sessao\">Sair</button></div>\n    </div>`\n  },\n\n  mounted(d2) {\n    const nav = () => d2.router(d2.param('routerRef'))\n    d2.on('.item', 'click', (e, el) => nav().goTo(el.dataset.href))\n    d2.on('.b-out', 'click', async () => {\n      await sair(d2.auth(d2.param('auth')))\n      nav().goTo('/login')\n    })\n  },\n}\n\n// logout recebe a sessao (`@auth.logout(@auth.session)`); tenta com e sem\nasync function sair(auth) {\n  if (!auth) return\n  try { await auth.logout(auth.session); return } catch (e) { /* tenta sem argumento */ }\n  try { await auth.logout() } catch (e) { /* segue para o login mesmo assim */ }\n}\n\n// remove barra final e query; mantem '/' para a home\nfunction normalize(p) {\n  let s = String(p || '/').split('?')[0]\n  if (s.length > 1 && s.endsWith('/')) s = s.slice(0, -1)\n  return s || '/'\n}\n\n// o item ativo e o link mais longo que casa com o fim do path\n// (o player pode prefixar segmento de preview, por isso endsWith e nao ===)\nfunction pickActive(items, here) {\n  let best = null\n  for (const l of items) {\n    if (l.href === '/') continue\n    if (here === l.href || here.endsWith(l.href)) {\n      if (!best || l.href.length > best.length) best = l.href\n    }\n  }\n  if (best) return best\n  return items.some(l => l.href === '/') ? '/' : null\n}\n\nfunction esc(s) {\n  return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n"}},"auth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"auth"}}}},"routerRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"routerRef"}}}}}}}}}},{"_id":"6aa2fe8026cc0165e0d1eeb5","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"OpsCF_Not_Allowed_Component","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"export default {\n  css: `\n    :host{all:initial}\n    *{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}\n    .wrap{display:flex;align-items:center;justify-content:center;height:100%;min-height:100%;padding:24px;background:#faf9f7;color:#1a1714;font-size:14px}\n    .card{width:100%;max-width:440px;background:#fff;border:1px solid #e6e1db;border-radius:14px;padding:32px 30px;text-align:center;box-shadow:0 8px 30px rgba(26,23,20,.06)}\n    .ic{width:56px;height:56px;border-radius:50%;background:#fdecec;color:#a8321f;display:flex;align-items:center;justify-content:center;font-size:26px;margin:0 auto 16px}\n    h1{margin:0 0 8px;font-size:19px}\n    p{margin:0 0 20px;color:#6b625a;line-height:1.55;font-size:13.5px}\n    .btn{padding:10px 18px;border:none;border-radius:8px;background:#EB6619;color:#fff;font-size:13.5px;font-weight:600;cursor:pointer;font-family:inherit;margin:0 4px}\n    .btn.sec{background:#fff;color:#6b625a;border:1px solid #e6e1db;font-weight:500}\n  `,\n  render(d2) {\n    const user = pickUser(d2.auth(d2.param('auth')))\n    return `<div class=\"wrap\"><div class=\"card\">\n      <div class=\"ic\">&#9888;</div>\n      <h1>Acesso nao permitido</h1>\n      <p>${user && user.email ? `A conta <b>${esc(user.email)}</b> (papel <b>${esc(user.role || 'sem papel')}</b>) nao tem permissao para esta area.` : 'Voce nao tem permissao para esta area.'}<br>Se precisar de acesso, fale com o administrador do Ops CF.</p>\n      <button class=\"btn b-home\">Ir para o inicio</button>\n      <button class=\"btn sec b-out\">Trocar de conta</button>\n    </div></div>`\n  },\n  mounted(d2) {\n    const nav = () => d2.router(d2.param('routerRef'))\n    d2.on('.b-home', 'click', () => nav().goTo('/'))\n    d2.on('.b-out', 'click', async () => {\n      await sair(d2.auth(d2.param('auth')))\n      nav().goTo('/login')\n    })\n  },\n}\n\nfunction pickUser(auth) {\n  if (!auth) return null\n  const bases = []\n  try { bases.push(auth.user) } catch (e) { /* noop */ }\n  try { bases.push(auth.session) } catch (e) { /* noop */ }\n  const cands = []\n  for (const b of bases) {\n    if (!b || typeof b !== 'object') continue\n    cands.push(b.user, b.credentials && b.credentials.user, b.credentials, b.data, b)\n  }\n  for (const c of cands) {\n    if (c && typeof c === 'object' && (c.email || c.name)) return c\n  }\n  return null\n}\n\nasync function sair(auth) {\n  if (!auth) return\n  try { await auth.logout(auth.session); return } catch (e) { /* tenta sem argumento */ }\n  try { await auth.logout() } catch (e) { /* segue */ }\n}\n\nfunction esc(s) {\n  return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n"}},"auth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"auth"}}}},"routerRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"routerRef"}}}}}}}}}},{"_id":"6af1cff5d169681f5e7c8a98","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"agentDeskTopbar","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Barra do topo. Estado de tela (aba, 1/2 conversas) fica no store `data.*`.\n// A troca de status de verdade sai por onStatusChange (UC na próxima etapa).\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst initials = (n) => String(n || '').trim().split(/\\s+/).slice(0, 2).map((p) => p[0] || '').join('').toUpperCase()\nconst STATUS_COLOR = { online: '#3aa65c', pause: '#e0962b', offline: '#a39a90', stop: '#a39a90' }\nconst DEFAULT_STATUS = [\n  { title: 'Disponível', type: 'online' }, { title: 'Pausa Descanso', type: 'pause' }, { title: 'Pausa Lanche', type: 'pause' },\n  { title: 'Pausa Treinamento', type: 'pause' }, { title: 'Pausa Backoffice', type: 'pause' }, { title: 'Pausa Feedback', type: 'pause' },\n  { title: 'Pausa Particular', type: 'pause' }, { title: 'Indisponível', type: 'offline' },\n]\n// leitura/escrita no store sempre pelo caminho completo data.<chave>\nconst field = (d2, key) => d2.field(d2.params.store, 'data.' + key)\n// opções de status: do app de fora (store data.statusOptions, com _id) > param da instância > lista padrão\nconst statusOptions = (d2) => { const s = field(d2, 'statusOptions').value; return (Array.isArray(s) && s.length ? s : null) || d2.param('statusOptions') || DEFAULT_STATUS }\nconst ROLE_LABEL = { agent: 'Atendente', supervisor: 'Supervisor', admin: 'Administrador' }\n\nconst ICON_CHAT = `<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"5\" width=\"18\" height=\"14\" rx=\"3\"/><path d=\"M3 8l9 6 9-6\"/></svg>`\nconst ICON_BOLT = `<svg viewBox=\"0 0 24 24\" width=\"15\" height=\"15\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M13 2L4 14h7l-1 8 9-12h-7z\"/></svg>`\nconst ICON_ONE = `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"3\"/></svg>`\nconst ICON_TWO = `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"3\" y=\"4\" width=\"8\" height=\"16\" rx=\"2.5\"/><rect x=\"13\" y=\"4\" width=\"8\" height=\"16\" rx=\"2.5\"/></svg>`\nconst ICON_CHEV = `<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M6 9l6 6 6-6\"/></svg>`\n\nexport default {\n  css: `\n    :host{display:block;height:100%;overflow:visible}\n    .bar{height:100%;box-sizing:border-box;display:flex;align-items:center;gap:16px;padding:0 20px;background:#f7f5f2;border-bottom:1px solid #ece7df;font-family:'NaturaCo','Inter',system-ui,sans-serif;color:#2a2622;position:relative;z-index:2}\n    .logo{height:40px;display:flex;align-items:center}\n    .logo img{height:100%;width:auto;display:block}\n    .logo.ph{width:96px;height:30px;border-radius:6px;justify-content:center;font-size:11px;color:#8a8178;background:repeating-linear-gradient(45deg,#efe9e1,#efe9e1 4px,#f7f3ee 4px,#f7f3ee 8px);border:1px solid #e6dfd5}\n    .tabs{display:flex;gap:4px;background:#efe9e1;border-radius:12px;padding:4px}\n    .tab{display:flex;align-items:center;gap:8px;border:0;background:transparent;border-radius:9px;padding:7px 14px;font:inherit;font-size:13px;color:#5d554d;cursor:pointer}\n    .tab.on{background:#e0962b;color:#fff;box-shadow:0 1px 2px rgba(0,0,0,.08)}\n    .count{min-width:18px;height:18px;border-radius:9px;background:rgba(255,255,255,.3);font-size:11px;display:inline-flex;align-items:center;justify-content:center;padding:0 5px}\n    .tab:not(.on) .count{background:#e0d6c9;color:#5d554d}\n    .sp{flex:1}\n    .view{display:flex;align-items:center;background:#fff;border:1px solid #e6dfd5;border-radius:10px;padding:3px;gap:2px}\n    .view button{display:flex;align-items:center;gap:6px;border:0;background:transparent;border-radius:7px;padding:5px 9px;color:#8a8178;font:inherit;font-size:12px;cursor:pointer}\n    .view button.on{background:#fdf1df;color:#b8741b}\n    .stwrap{position:relative}\n    .status{display:flex;align-items:center;gap:8px;background:#fff;border:1px solid #e6dfd5;border-radius:20px;padding:6px 12px;font:inherit;font-size:13px;cursor:pointer;color:#2a2622}\n    .stmenu{position:absolute;right:0;top:42px;z-index:100;min-width:220px;max-height:340px;overflow:auto;background:#fff;border:1px solid #ece7df;border-radius:12px;box-shadow:0 10px 28px rgba(42,38,34,.16);padding:6px}\n    .stmenu button{display:flex;align-items:center;gap:8px;width:100%;text-align:left;border:0;background:transparent;border-radius:8px;padding:8px 10px;font:inherit;font-size:13px;color:#2a2622;cursor:pointer}\n    .stmenu button:hover{background:#fdf1df}\n    .stmenu button.on{font-weight:600}\n    .dot{flex:none;width:8px;height:8px;border-radius:50%}\n    .user{display:flex;align-items:center;gap:10px}\n    .av{position:relative;width:34px;height:34px;border-radius:50%;background:#e0962b;color:#fff;font-size:13px;font-weight:600;display:flex;align-items:center;justify-content:center}\n    .av i{position:absolute;right:-1px;bottom:-1px;width:10px;height:10px;border-radius:50%;border:2px solid #f7f5f2}\n    .name{font-size:13px;font-weight:600;line-height:1.2}\n    .role{font-size:11px;color:#8a8178}\n  `,\n  render(d2) {\n    const viewMode = field(d2, 'viewMode').value || 'single'\n    const tab = field(d2, 'activeTab').value || 'conversations'\n    // enquanto a troca de status não está ligada a um UC, mostra a escolha localmente\n    // status escolhido fica no store (data.agentStatus); com a ponte, quem manda é o app de fora\n    const picked = field(d2, 'agentStatus').value\n    const statusType = picked ? picked.type : d2.param('statusType', 'online')\n    const statusLabel = picked ? picked.title : d2.param('statusLabel', 'Disponível')\n    const statusColor = STATUS_COLOR[statusType] || '#a39a90'\n    const options = statusOptions(d2)\n    // agente vindo do app de fora (store data.agent); sem ele, usa os params da instância\n    const ag = field(d2, 'agent').value || {}\n    const agentName = ag.name || d2.param('agentName', 'Agente')\n    const agentRole = ROLE_LABEL[ag.role] || d2.param('agentRole', 'Atendente')\n    const logo = d2.param('logoUrl')\n    // logo, status e avatar já aparecem na barra da casca do portal (iNSec6AsghH0IZ1sO7Ga4).\n    // Ficam escondidos aqui; ligar showIdentity só se a barra da casca sair da rota /.\n    const showId = d2.param('showIdentity', false) === true\n    return `<div class=\"bar\">\n      ${showId ? (logo ? `<div class=\"logo\"><img src=\"${esc(logo)}\" alt=\"Natura\"/></div>` : '<div class=\"logo ph\">logo da empresa</div>') : ''}\n      <div class=\"tabs\">\n        <button class=\"tab ${tab === 'conversations' ? 'on' : ''}\" data-tab=\"conversations\">${ICON_CHAT} Atendimentos <span class=\"count\">${esc((field(d2, 'conversations').value || []).length || d2.param('conversationsCount', 0))}</span></button>\n        <button class=\"tab ${tab === 'quick' ? 'on' : ''}\" data-tab=\"quick\">${ICON_BOLT} Gerenciar Mensagens Rápidas</button>\n      </div>\n      <div class=\"sp\"></div>\n      <div class=\"view\" title=\"Visualização das conversas\">\n        <button class=\"${viewMode !== 'dual' ? 'on' : ''}\" data-view=\"single\">${ICON_ONE} 1 conversa</button>\n        <button class=\"${viewMode === 'dual' ? 'on' : ''}\" data-view=\"dual\">${ICON_TWO} 2 conversas</button>\n      </div>\n      ${showId ? `<div class=\"stwrap\">\n        <button class=\"status\" data-act=\"status\"><span class=\"dot\" style=\"background:${statusColor}\"></span>${esc(statusLabel)} ${ICON_CHEV}</button>\n        ${d2.state.statusOpen ? `<div class=\"stmenu\">${options.map((o, i) => `<button class=\"${o.title === statusLabel ? 'on' : ''}\" data-st=\"${i}\"><span class=\"dot\" style=\"background:${STATUS_COLOR[o.type] || '#a39a90'}\"></span>${esc(o.title)}</button>`).join('')}</div>` : ''}\n      </div>\n      <div class=\"user\">\n        <div class=\"av\">${esc(initials(agentName))}<i style=\"background:${statusColor}\"></i></div>\n        <div><div class=\"name\">${esc(agentName)}</div><div class=\"role\">${esc(agentRole)}</div></div>\n      </div>` : ''}\n    </div>`\n  },\n  mounted(d2) {\n    d2.on('[data-view]', 'click', (e, el) => {\n      field(d2, 'viewMode').set(el.dataset.view)\n      field(d2, 'focusedSlot').set('1')\n    })\n    d2.on('[data-tab]', 'click', (e, el) => field(d2, 'activeTab').set(el.dataset.tab))\n    d2.on('[data-act=\"status\"]', 'click', (e) => { e.stopPropagation(); d2.state.statusOpen = !d2.state.statusOpen })\n    d2.on('[data-st]', 'click', (e, el) => {\n      e.stopPropagation()\n      const o = statusOptions(d2)[Number(el.dataset.st)]\n      d2.setState({ statusOpen: false })\n      if (!o) return\n      field(d2, 'agentStatus').set(o)\n      d2.emit('onStatusChange', o)\n      // ponte (tela dentro de iframe): o agentDeskBridgeGuest repassa para o app de fora\n      try { window.dispatchEvent(new CustomEvent('agentDesk:action', { detail: { type: 'onStatusChange', payload: { _id: o._id, title: o.title, type: o.type } } })) } catch (err) {}\n    })\n    d2.on(document, 'click', () => { if (d2.state.statusOpen) d2.state.statusOpen = false })\n  },\n}\n"}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"logoUrl":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"logoUrl"}}}},"agentName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentName"}}}},"agentRole":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"agentRole"}}}},"statusLabel":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"statusLabel"}}}},"statusType":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"statusType"}}}},"statusOptions":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"statusOptions"}}}},"conversationsCount":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"conversationsCount"}}}},"onStatusChange":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onStatusChange"}}}},"showIdentity":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"showIdentity"}}}}}}}}}},{"_id":"6af1d00bd169681f5e7c8ac5","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"agentDeskConversationList","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Lista \"Conversas em andamento\". Lê tudo do store (data.conversations, seleção, foco e modo).\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst initials = (n) => String(n || '').trim().split(/\\s+/).slice(0, 2).map((p) => p[0] || '').join('').toUpperCase()\nconst AVATAR = ['#e0962b', '#d0706a', '#7c9a6a', '#6a85b8', '#b07cc6']\nconst SLOT_COLOR = { 1: '#e0962b', 2: '#c45f3c' }\nconst ICON_CLOCK = `<svg viewBox=\"0 0 24 24\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>`\nconst clock = (since, now) => {\n  if (!since) return '--:--'\n  const s = Math.max(0, Math.floor((now - new Date(since).getTime()) / 1000))\n  const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), x = s % 60\n  const p = (n) => String(n).padStart(2, '0')\n  return h ? `${h}:${p(m)}:${p(x)}` : `${p(m)}:${p(x)}`\n}\nconst field = (d2, key) => d2.field(d2.params.store, 'data.' + key)\n// mesma regra do workspace para saber qual conversa está em destaque\nconst activeId = (d2, convs, dual) => {\n  if (dual) { const c = convs[Number(field(d2, 'focusedSlot').value || 1) - 1]; return c ? c._id : '' }\n  const sel = field(d2, 'selectedConversationId').value\n  const c = convs.find((x) => x._id === sel) || convs[0]\n  return c ? c._id : ''\n}\n\nexport default {\n  css: `\n    :host{display:block;height:100%}\n    .wrap{height:100%;box-sizing:border-box;display:flex;flex-direction:column;font-family:'NaturaCo','Inter',system-ui,sans-serif;color:#2a2622;background:#fff}\n    .head{display:flex;align-items:center;justify-content:space-between;padding:16px 16px 10px}\n    .title{font-size:15px;font-weight:600}\n    .chip{font-size:11px;color:#8a8178;background:#f3eee8;border-radius:10px;padding:2px 8px}\n    .list{display:flex;flex-direction:column;gap:4px;padding:0 8px 10px;overflow:auto}\n    .row{display:flex;align-items:center;gap:12px;padding:10px;border-radius:12px;cursor:pointer;border:1px solid transparent}\n    .row:hover{background:#faf6f0}\n    .row.on{background:var(--on-bg,#fdf1df);border-color:var(--on-line,#f6dfb8)}\n    .av{position:relative;flex:none;width:36px;height:36px;border-radius:50%;color:#fff;font-size:12px;font-weight:600;display:flex;align-items:center;justify-content:center}\n    .av i{position:absolute;right:-1px;bottom:-1px;width:10px;height:10px;border-radius:50%;background:#3aa65c;border:2px solid #fff}\n    .slot{position:absolute;left:-4px;top:-4px;min-width:16px;height:16px;border-radius:8px;font-size:10px;font-weight:700;color:#fff;display:flex;align-items:center;justify-content:center;border:2px solid #fff}\n    .name{flex:1;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n    .time{display:flex;align-items:center;gap:4px;font-size:11px;color:#5d554d;background:#fff;border:1px solid #ece7df;border-radius:10px;padding:2px 7px}\n    .empty{padding:18px 16px;font-size:13px;color:#8a8178}\n    .q{margin-left:6px;font-size:10px;font-weight:600;color:#b8741b;background:#fdf1df;border-radius:8px;padding:2px 6px;vertical-align:middle}\n  `,\n  render(d2) {\n    const convs = field(d2, 'conversations').value || []\n    const dual = field(d2, 'viewMode').value === 'dual'\n    const active = activeId(d2, convs, dual)\n    const max = d2.param('maxConversations', 2)\n    const now = Date.now()\n    const rows = convs.map((c, i) => {\n      const slot = dual && i < 2 ? String(i + 1) : ''\n      // no modo 2 conversas o destaque usa a cor da conversa em foco\n      const style = c._id === active && slot === '2' ? 'style=\"--on-bg:#fbe8e1;--on-line:#f1c9b9\"' : ''\n      return `<div class=\"row ${c._id === active ? 'on' : ''}\" ${style} data-key=\"${esc(c._id)}\" data-id=\"${esc(c._id)}\">\n        <div class=\"av\" style=\"background:${AVATAR[i % AVATAR.length]}\">${esc(initials(c.name))}<i></i>${slot ? `<span class=\"slot\" style=\"background:${SLOT_COLOR[slot]}\">${slot}</span>` : ''}</div>\n        <div class=\"name\">${esc(c.name)}${c.transfer && c.transfer.status === 'in_queue' ? '<span class=\"q\">Na fila</span>' : ''}</div>\n        <div class=\"time\">${ICON_CLOCK}<span data-since=\"${esc(c.since || '')}\">${clock(c.since, now)}</span></div>\n      </div>`\n    }).join('')\n    return `<div class=\"wrap\">\n      <div class=\"head\"><div class=\"title\">Conversas em andamento</div><div class=\"chip\">${convs.length}/${max}</div></div>\n      <div class=\"list\">${rows || '<div class=\"empty\">Nenhuma conversa em andamento.</div>'}</div>\n    </div>`\n  },\n  mounted(d2) {\n    d2.on('.row', 'click', (e, el) => d2.emit('onSelect', el.dataset.id))\n    // relógio: atualiza só o texto, sem re-render\n    const t = setInterval(() => {\n      const now = Date.now()\n      d2.$$('[data-since]').forEach((el) => { el.textContent = clock(el.dataset.since, now) })\n    }, 1000)\n    d2.onUnmount(() => clearInterval(t))\n  },\n}\n"}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"maxConversations":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"maxConversations"}}}},"onSelect":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onSelect"}}}}}}}}}},{"_id":"6af1d01ac3bb7547e8619e52","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"agentDeskQuickMessages","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Mensagens rápidas. Lê do store `data.quickMessages`: [{ _id, title, text, scope: 'mine' | 'general' }].\n// - mine: mensagens do agente; ele define a ordem arrastando pelo puxador (⋮⋮).\n// - general: mensagens da equipe; ordem definida pela gestão (sem arraste).\n// mode 'panel' (lateral do atendimento) ou 'manage' (aba Gerenciar Mensagens Rápidas).\n// A nova ordem sai por onReorder({ scope:'mine', ids }) — quem grava é o UC.\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst norm = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')\nconst field = (d2, key) => d2.field(d2.params.store, 'data.' + key)\n// fila da conversa em foco (mesma regra do workspace): no painel só aparecem as mensagens dessa fila\n// (mensagem sem fila marcada vale para todas)\nconst focusedQueue = (d2) => {\n  const convs = field(d2, 'conversations').value || []\n  let c\n  if (field(d2, 'viewMode').value === 'dual') c = convs[Number(field(d2, 'focusedSlot').value || '1') - 1]\n  else { const sel = field(d2, 'selectedConversationId').value; c = convs.find((x) => x._id === sel) || convs[0] }\n  return c && c.transfer && c.transfer.queueId ? String(c.transfer.queueId) : ''\n}\nconst forQueue = (m, qid) => !qid || !m.queues || !m.queues.length || m.queues.map(String).includes(qid)\nconst I = {\n  bolt: `<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M13 2L4 14h7l-1 8 9-12h-7z\"/></svg>`,\n  send: `<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 2L11 13\"/><path d=\"M22 2l-7 20-4-9-9-4z\"/></svg>`,\n  search: `<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"M20 20l-3.5-3.5\"/></svg>`,\n  grip: `<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"currentColor\"><circle cx=\"9\" cy=\"6\" r=\"1.6\"/><circle cx=\"15\" cy=\"6\" r=\"1.6\"/><circle cx=\"9\" cy=\"12\" r=\"1.6\"/><circle cx=\"15\" cy=\"12\" r=\"1.6\"/><circle cx=\"9\" cy=\"18\" r=\"1.6\"/><circle cx=\"15\" cy=\"18\" r=\"1.6\"/></svg>`,\n  lock: `<svg viewBox=\"0 0 24 24\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><rect x=\"5\" y=\"11\" width=\"14\" height=\"10\" rx=\"2\"/><path d=\"M8 11V7a4 4 0 018 0v4\"/></svg>`,\n}\n\n// ---- arraste: move o item com o ponteiro e desliza os vizinhos; ao soltar, emite a nova ordem ----\nconst startDrag = (d2, e, grip) => {\n  const item = grip.closest('.item'); if (!item) return\n  const items = d2.$$('.item[data-scope=\"mine\"]')\n  const from = items.indexOf(item); if (from < 0) return\n  e.preventDefault()\n  const rects = items.map((el) => el.getBoundingClientRect())\n  const gap = items.length > 1 ? Math.max(0, rects[1].top - rects[0].bottom) : 0\n  const step = rects[from].height + gap\n  const mids = rects.map((r) => r.top + r.height / 2)\n  const list = d2.$('.list')\n  let dy = 0, to = from, lastY = e.clientY, scrollBase = list ? list.scrollTop : 0\n  item.classList.add('dragging')\n  items.forEach((el) => { if (el !== item) el.classList.add('shifting') })\n  const paint = () => {\n    const scrolled = list ? list.scrollTop - scrollBase : 0\n    const center = mids[from] + dy + scrolled\n    to = 0\n    mids.forEach((m, i) => { if (i !== from && m < center) to += 1 })\n    item.style.transform = `translateY(${dy + scrolled}px)`\n    items.forEach((el, i) => {\n      if (el === item) return\n      let shift = 0\n      if (from < to && i > from && i <= to) shift = -step\n      if (to < from && i >= to && i < from) shift = step\n      el.style.transform = shift ? `translateY(${shift}px)` : ''\n    })\n  }\n  const move = (ev) => {\n    lastY = ev.clientY\n    dy = ev.clientY - e.clientY\n    // rola a lista quando o ponteiro chega perto das bordas\n    if (list) {\n      const lr = list.getBoundingClientRect()\n      if (lastY < lr.top + 30) list.scrollTop -= 8\n      else if (lastY > lr.bottom - 30) list.scrollTop += 8\n    }\n    paint()\n  }\n  const end = () => {\n    window.removeEventListener('pointermove', move)\n    window.removeEventListener('pointerup', end)\n    window.removeEventListener('pointercancel', end)\n    items.forEach((el) => { el.classList.remove('dragging', 'shifting'); el.style.transform = '' })\n    if (to !== from) {\n      const ids = items.map((el) => el.dataset.id)\n      const [moved] = ids.splice(from, 1)\n      ids.splice(to, 0, moved)\n      d2.emit('onReorder', { scope: 'mine', ids })\n    }\n  }\n  window.addEventListener('pointermove', move)\n  window.addEventListener('pointerup', end)\n  window.addEventListener('pointercancel', end)\n  d2.onUnmount(end)\n}\n\nexport default {\n  css: `\n    :host{display:block;height:100%}\n    .wrap{height:100%;box-sizing:border-box;display:flex;flex-direction:column;font-family:'NaturaCo','Inter',system-ui,sans-serif;color:#2a2622;background:#fff;border-top:1px solid #ece7df}\n    .wrap.manage{border-top:0;background:#f7f5f2;padding:24px}\n    .box{display:flex;flex-direction:column;min-height:0;flex:1}\n    .manage .box{max-width:760px;width:100%;margin:0 auto;background:#fff;border:1px solid #ece7df;border-radius:16px;padding:8px 0}\n    .head{display:flex;align-items:center;gap:8px;padding:14px 16px 8px;font-size:13px;font-weight:600}\n    .manage .head{font-size:16px;padding:16px 20px 4px}\n    .head .b{color:#e0962b;display:flex}\n    .chip{margin-left:auto;font-size:11px;color:#8a8178;background:#f3eee8;border-radius:10px;padding:2px 8px;font-weight:400}\n    .sub{padding:0 20px 10px;font-size:12.5px;color:#8a8178}\n    .search{margin:0 12px 8px;display:flex;align-items:center;gap:8px;border:1px solid #e6dfd5;border-radius:10px;padding:8px 10px;color:#8a8178;background:#faf8f5}\n    .manage .search{margin:4px 20px 10px}\n    .search input{border:0;outline:0;background:transparent;font:inherit;font-size:13px;flex:1;color:#2a2622}\n    .list{flex:1;overflow:auto;padding:0 8px 12px}\n    .manage .list{padding:0 12px 12px}\n    .group{display:flex;align-items:center;gap:6px;padding:10px 8px 6px;font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#9a9088}\n    .group small{font-weight:400;letter-spacing:0;text-transform:none;display:flex;align-items:center;gap:4px}\n    .item{position:relative;padding:12px 40px 12px 26px;margin-bottom:9px;border-radius:10px;cursor:pointer;background:#fff}\n    .manage .item{cursor:default}\n    .item:hover{background:#fdf1df}\n    /* separador desligado a pedido (09/11); para voltar: .item:not(:last-child)::after{content:'';position:absolute;left:50%;bottom:-5px;transform:translateX(-50%);width:40%;height:1px;background:#ebe4da} */\n    .item:last-child{margin-bottom:0}\n    .item.shifting{transition:transform .16s ease}\n    .item.dragging{z-index:5;background:#fff;box-shadow:0 10px 24px rgba(42,38,34,.18);cursor:grabbing}\n    .item.dragging::after,.item.shifting::after{opacity:0}\n    .grip{position:absolute;left:4px;top:50%;transform:translateY(-50%);width:18px;height:28px;border:0;background:transparent;color:#b3a99d;display:flex;align-items:center;justify-content:center;cursor:grab;opacity:0;border-radius:6px;padding:0;touch-action:none}\n    .item:hover .grip,.manage .grip,.dragging .grip{opacity:1}\n    .grip:hover{background:#f3eee8;color:#8a8178}\n    .cat{display:flex;align-items:center;gap:6px;font-size:10px;font-weight:700;letter-spacing:.04em;color:#b8741b;text-transform:uppercase;margin-bottom:3px}\n    .txt{font-size:12.5px;line-height:1.4;color:#4a433c}\n    .manage .txt{font-size:13.5px}\n    .qs{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:28px;height:28px;border:0;border-radius:8px;background:#fff;color:#b8741b;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transition:opacity .12s;box-shadow:0 1px 3px rgba(42,38,34,.12)}\n    .item:hover .qs,.qs:focus{opacity:1}\n    .qs:hover{background:#e0962b;color:#fff}\n    .lockgrip{position:absolute;left:8px;top:50%;transform:translateY(-50%);color:#cfc6bb;display:flex}\n    .empty{padding:12px 8px;font-size:12px;color:#8a8178}\n    .new{margin-left:12px;border:0;border-radius:9px;background:#e0962b;color:#fff;font:inherit;font-size:12.5px;font-weight:600;padding:7px 12px;cursor:pointer}\n    .acts{display:flex;gap:6px;margin-top:8px}\n    .acts button{border:1px solid #e6dfd5;background:#fff;border-radius:8px;padding:4px 10px;font:inherit;font-size:12px;color:#5d554d;cursor:pointer}\n    .acts button:hover{background:#fdf1df}\n    .acts .del:hover{background:#fbe8e1;color:#c45f3c;border-color:#f1c9b9}\n    .qtags{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}\n    .qtags span{font-size:10.5px;color:#6a85b8;background:#eef2fa;border-radius:8px;padding:2px 7px}\n    .form{margin:4px 8px 14px;padding:14px;border:1px solid #f1d9b0;background:#fffaf2;border-radius:12px;display:flex;flex-direction:column;gap:8px}\n    .form .ft{font-size:13px;font-weight:600}\n    .form input,.form textarea{border:1px solid #e6dfd5;border-radius:8px;padding:8px 10px;font:inherit;font-size:13px;background:#fff;color:#2a2622;resize:vertical}\n    .form input:focus,.form textarea:focus{outline:0;border-color:#e0962b}\n    .fq{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12.5px;color:#4a433c}\n    .fq span{width:100%;font-size:11.5px;color:#8a8178}\n    .fq label{display:flex;align-items:center;gap:5px;cursor:pointer}\n    .fa{display:flex;justify-content:flex-end;gap:8px}\n    .fa button{border:0;border-radius:9px;padding:7px 14px;font:inherit;font-size:13px;cursor:pointer}\n    .fa .cancel{background:#f3eee8;color:#5d554d}\n    .fa .save{background:#e0962b;color:#fff;font-weight:600}\n  `,\n  render(d2) {\n    const manage = d2.param('mode') === 'manage'\n    const all = field(d2, 'quickMessages').value || []\n    const q = norm(d2.state.q)\n    const match = (m) => !q || norm(m.title).includes(q) || norm(m.text).includes(q)\n    const qid = manage ? '' : focusedQueue(d2)\n    const opts = field(d2, 'queueOptions').value || []\n    const qname = (id) => { const o = opts.find((x) => String(x._id) === String(id)); return o ? o.name : '' }\n    const mine = all.filter((m) => m.scope !== 'general' && match(m) && forQueue(m, qid))\n    const general = all.filter((m) => m.scope === 'general' && match(m) && forQueue(m, qid))\n    // com filtro (texto ou fila) a lista é parcial: arraste desligado\n    const canDrag = !q && mine.length === all.filter((m) => m.scope !== 'general').length\n    const ed = manage ? d2.state.editing : null\n    const form = ed ? `<div class=\"form\" data-key=\"form-${esc(ed._id || 'new')}\">\n        <div class=\"ft\">${ed._id ? 'Editar mensagem' : 'Nova mensagem'}</div>\n        <input class=\"f-title\" data-d2-static placeholder=\"Título (ex.: Saudação)\" value=\"${esc(ed.title || '')}\"/>\n        <textarea class=\"f-text\" data-d2-static rows=\"4\" placeholder=\"Texto da mensagem\">${esc(ed.text || '')}</textarea>\n        ${opts.length ? `<div class=\"fq\"><span>Filas onde aparece (nenhuma marcada = todas)</span>${opts.map((o) => `<label><input type=\"checkbox\" class=\"f-q\" data-d2-static value=\"${esc(o._id)}\" ${(ed.queues || []).map(String).includes(String(o._id)) ? 'checked' : ''}/> ${esc(o.name)}</label>`).join('')}</div>` : ''}\n        <div class=\"fa\"><button class=\"cancel\" data-act=\"cancel\">Cancelar</button><button class=\"save\" data-act=\"save\">Salvar</button></div>\n      </div>` : ''\n    const item = (m, draggable) => `<div class=\"item\" data-key=\"${esc(m._id)}\" data-id=\"${esc(m._id)}\" data-scope=\"${draggable ? 'mine' : 'general'}\">\n        ${draggable && canDrag ? `<button class=\"grip\" title=\"Arraste para mudar a ordem\">${I.grip}</button>` : ''}\n        ${!draggable && manage ? `<span class=\"lockgrip\" title=\"Ordem definida pela gestão\">${I.lock}</span>` : ''}\n        <div class=\"cat\">${I.bolt}${esc(m.title)}</div>\n        <div class=\"txt\">${esc(m.text)}</div>\n        ${manage && m.queues && m.queues.length ? `<div class=\"qtags\">${m.queues.map((id) => `<span>${esc(qname(id) || 'fila')}</span>`).join('')}</div>` : ''}\n        ${manage && draggable ? `<div class=\"acts\"><button data-edit=\"${esc(m._id)}\">Editar</button><button class=\"del\" data-del=\"${esc(m._id)}\">Apagar</button></div>` : ''}\n        ${manage ? '' : `<button class=\"qs\" data-send=\"${esc(m._id)}\" title=\"Enviar agora\">${I.send}</button>`}\n      </div>`\n    const groups = `\n      ${mine.length ? `<div class=\"group\">Minhas mensagens</div>${mine.map((m) => item(m, true)).join('')}` : ''}\n      ${general.length ? `<div class=\"group\">Da equipe <small>${I.lock} ordem definida pela gestão</small></div>${general.map((m) => item(m, false)).join('')}` : ''}`\n    return `<div class=\"wrap ${manage ? 'manage' : ''}\"><div class=\"box\">\n      <div class=\"head\"><span class=\"b\">${I.bolt}</span>${manage ? 'Gerenciar mensagens rápidas' : 'Mensagens rápidas'}<span class=\"chip\">${all.length}</span>${manage && !ed ? '<button class=\"new\" data-act=\"new\">+ Nova mensagem</button>' : ''}</div>\n      ${manage ? '<div class=\"sub\">Arraste pelo ⋮⋮ para definir a ordem das suas mensagens. As mensagens da equipe seguem a ordem definida pela gestão.</div>' : ''}\n      <label class=\"search\">${I.search}<input data-d2-static placeholder=\"Filtrar mensagens...\" value=\"${esc(d2.state.q || '')}\"/></label>\n      <div class=\"list\">${form}${mine.length || general.length ? groups : '<div class=\"empty\">Nenhuma mensagem encontrada.</div>'}</div>\n    </div></div>`\n  },\n  mounted(d2) {\n    const byId = (id) => (field(d2, 'quickMessages').value || []).find((m) => m._id === id)\n    d2.on('.search input', 'input', (e, el) => { d2.state.q = el.value })\n    // cadastro (aba Gerenciar): nova, editar, apagar. Quem grava é o app de fora (UCs agentDeskHost*QuickMessage).\n    d2.on('[data-act=\"new\"]', 'click', () => d2.setState({ editing: { _id: '', title: '', text: '', queues: [] } }))\n    d2.on('[data-edit]', 'click', (e, el) => {\n      const m = byId(el.dataset.edit)\n      if (m) d2.setState({ editing: { _id: m._id, title: m.title || '', text: m.text || '', queues: m.queues || [] } })\n    })\n    d2.on('[data-act=\"cancel\"]', 'click', () => d2.setState({ editing: null }))\n    d2.on('[data-act=\"save\"]', 'click', () => {\n      const ed = d2.state.editing\n      if (!ed) return\n      const title = ((d2.$('.f-title') || {}).value || '').trim()\n      const text = ((d2.$('.f-text') || {}).value || '').trim()\n      if (!text) { if (d2.dialog && d2.dialog.alert) d2.dialog.alert('Escreva o texto da mensagem.'); return }\n      const queues = d2.$$('.f-q').filter((i) => i.checked).map((i) => i.value)\n      d2.emit('onSaveQuickMessage', { _id: ed._id || '', title, text, queues })\n      d2.setState({ editing: null })\n    })\n    d2.on('[data-del]', 'click', async (e, el) => {\n      const m = byId(el.dataset.del)\n      if (!m) return\n      const ok = d2.dialog && d2.dialog.confirm ? await d2.dialog.confirm(`Apagar a mensagem \"${m.title}\"?`) : true\n      if (ok) d2.emit('onDeleteQuickMessage', { _id: m._id })\n    })\n    d2.on('.grip', 'pointerdown', (e, el) => startDrag(d2, e, el))\n    // clicar no texto: vai para o input do chat em foco (só no painel lateral)\n    d2.on('.item', 'click', (e, el) => {\n      if (d2.param('mode') === 'manage') return\n      if (e.target.closest && e.target.closest('.qs, .grip')) return\n      const m = byId(el.dataset.id)\n      if (m) d2.emit('onPick', m.text)\n    })\n    // envio rápido: manda direto para o chat em foco\n    d2.on('.qs', 'click', (e, el) => {\n      e.stopPropagation()\n      const m = byId(el.dataset.send)\n      if (m) d2.emit('onQuickSend', m.text)\n    })\n  },\n}\n"}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"mode":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"mode"}}}},"onPick":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onPick"}}}},"onQuickSend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onQuickSend"}}}},"onReorder":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onReorder"}}}},"onSaveQuickMessage":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onSaveQuickMessage"}}}},"onDeleteQuickMessage":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onDeleteQuickMessage"}}}}}}}}}},{"_id":"6af1d04cd5edd5024925507e","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"agentDeskChatPanel","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Painel de uma conversa. Lê do store qual conversa mostrar:\n// modo 2 conversas -> conversations[slot-1]; modo 1 -> a selecionada.\n// As ações saem pelos handlers (on*), que o workspace repassa aos UCs.\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst initials = (n) => String(n || '').trim().split(/\\s+/).slice(0, 2).map((p) => p[0] || '').join('').toUpperCase()\nconst hhmm = (t) => { const d = new Date(t); return isNaN(d) ? '' : d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) }\nconst dayLabel = (t) => {\n  const d = new Date(t); if (isNaN(d)) return ''\n  const today = new Date(); const y = new Date(); y.setDate(today.getDate() - 1)\n  const dm = d.toLocaleDateString('pt-BR', { day: 'numeric', month: 'short' }).replace('.', '').toUpperCase()\n  if (d.toDateString() === today.toDateString()) return `HOJE · ${dm}`\n  if (d.toDateString() === y.toDateString()) return `ONTEM · ${dm}`\n  return dm\n}\nconst clock = (since, now) => {\n  if (!since) return '--:--'\n  const s = Math.max(0, Math.floor((now - new Date(since).getTime()) / 1000))\n  const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), x = s % 60\n  const p = (n) => String(n).padStart(2, '0')\n  return h ? `${h}:${p(m)}:${p(x)}` : `${p(m)}:${p(x)}`\n}\nconst SLOT = { 1: { accent: '#e0962b', soft: '#fdf1df' }, 2: { accent: '#c45f3c', soft: '#fbe8e1' } }\nconst EMOJIS = ['😊', '🙂', '😉', '😃', '🙏', '👍', '👏', '💛', '✅', '🎉', '😟', '🤔', '📦', '🚚', '⏳', '✨']\n// Transferir fica escondido até o Fernando validar se vai ser usado. Para mostrar: SHOW_TRANSFER = true.\nconst SHOW_TRANSFER = false\nconst MENU = [\n  ...(SHOW_TRANSFER ? [{ id: 'transfer', label: 'Transferir atendimento' }] : []),\n  { id: 'download', label: 'Baixar conversa (PDF)' },\n  { id: 'history_cpf', label: 'Histórico deste CPF' },\n  { id: 'history_ip', label: 'Histórico do IP' },\n]\nconst I = {\n  clock: `<svg viewBox=\"0 0 24 24\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 7v5l3 2\"/></svg>`,\n  check: `<svg viewBox=\"0 0 24 24\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8 12l3 3 5-6\"/></svg>`,\n  dots: `<svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"currentColor\"><circle cx=\"5\" cy=\"12\" r=\"1.8\"/><circle cx=\"12\" cy=\"12\" r=\"1.8\"/><circle cx=\"19\" cy=\"12\" r=\"1.8\"/></svg>`,\n  clip: `<svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12l-8.5 8.5a5 5 0 01-7-7L14 5a3.3 3.3 0 014.7 4.7l-8.5 8.5a1.7 1.7 0 01-2.4-2.4L15.5 8\"/></svg>`,\n  smile: `<svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8.5 14a4 4 0 007 0\"/><path d=\"M9 9.5h.01M15 9.5h.01\"/></svg>`,\n  send: `<svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 2L11 13\"/><path d=\"M22 2l-7 20-4-9-9-4z\"/></svg>`,\n  tick: `<svg viewBox=\"0 0 24 24\" width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\"><path d=\"M5 12l4 4 10-10\"/></svg>`,\n  star: `<svg viewBox=\"0 0 24 24\" width=\"12\" height=\"12\" fill=\"currentColor\"><path d=\"M12 2l2.4 6.6L21 9l-5 4.6L17.5 21 12 17.3 6.5 21 8 13.6 3 9l6.6-.4z\"/></svg>`,\n  mic: `<svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0014 0M12 18v3\"/></svg>`,\n  x: `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"><path d=\"M6 6l12 12M18 6L6 18\"/></svg>`,\n}\n\n// ---- gravação de áudio: grava aqui na tela, converte para MP3 (mesmo formato da tela atual) e manda como anexo ----\nconst MAX_REC_SECS = 180\nconst LAME = { url: 'https://cdn.jsdelivr.net/npm/lamejs@1.2.1/lame.min.js', global: 'lamejs' }\nconst waitLib = async (d2, spec) => {\n  for (let i = 0; i < 100; i++) {\n    const lib = d2.lib(spec)\n    if (lib) return lib\n    if (d2.lib.state && d2.lib.state(spec) === 'error') return null\n    await new Promise((r) => setTimeout(r, 100))\n  }\n  return null\n}\nconst toMp3 = async (d2, blob) => {\n  const lame = await waitLib(d2, LAME)\n  if (!lame) throw new Error('lamejs indisponível')\n  const Ctx = window.AudioContext || window.webkitAudioContext\n  const ctx = new Ctx()\n  const buf = await ctx.decodeAudioData(await blob.arrayBuffer())\n  if (ctx.close) ctx.close()\n  const data = buf.getChannelData(0)\n  const pcm = new Int16Array(data.length)\n  for (let i = 0; i < data.length; i++) { const s = Math.max(-1, Math.min(1, data[i])); pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff }\n  const enc = new lame.Mp3Encoder(1, buf.sampleRate, 64)\n  const out = []\n  for (let i = 0; i < pcm.length; i += 1152) { const b = enc.encodeBuffer(pcm.subarray(i, i + 1152)); if (b.length) out.push(new Uint8Array(b)) }\n  const end = enc.flush()\n  if (end.length) out.push(new Uint8Array(end))\n  return new Blob(out, { type: 'audio/mpeg' })\n}\nconst fmtSecs = (s) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`\nconst stopRec = (d2, cancel) => {\n  const rec = d2.root._rec\n  if (!rec || rec.state === 'inactive') return\n  d2.root._recCancel = !!cancel\n  rec.stop()\n}\nconst startRec = async (d2) => {\n  const alert = (m) => { if (d2.dialog && d2.dialog.alert) d2.dialog.alert(m) }\n  if (!navigator.mediaDevices || !window.MediaRecorder) { alert('Este navegador não grava áudio.'); return }\n  let stream\n  try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }) } catch (err) { alert(`Não foi possível usar o microfone (${(err && err.name) || err}).`); return }\n  const rec = new MediaRecorder(stream)\n  const chunks = []\n  rec.ondataavailable = (ev) => { if (ev.data && ev.data.size) chunks.push(ev.data) }\n  rec.onstop = async () => {\n    stream.getTracks().forEach((t) => t.stop())\n    clearInterval(d2.root._recTimer)\n    const cancel = d2.root._recCancel\n    d2.root._recCancel = false\n    d2.setState({ rec: false, recSecs: 0 })\n    if (cancel || !chunks.length) return\n    d2.setState({ recBusy: true })\n    try {\n      const mp3 = await toMp3(d2, new Blob(chunks, { type: rec.mimeType || 'audio/webm' }))\n      const file = new File([mp3], `audio-${Date.now()}.mp3`, { type: 'audio/mpeg' })\n      d2.emit('onAttach', { slot: slotOf(d2), file })\n    } catch (err) {\n      alert('Não foi possível preparar o áudio.')\n    } finally {\n      d2.setState({ recBusy: false })\n    }\n  }\n  d2.root._rec = rec\n  rec.start()\n  const t0 = Date.now()\n  d2.setState({ rec: true, recSecs: 0 })\n  d2.root._recTimer = setInterval(() => {\n    const s = Math.floor((Date.now() - t0) / 1000)\n    if (s !== d2.state.recSecs) d2.state.recSecs = s\n    if (s >= MAX_REC_SECS) stopRec(d2, false)\n  }, 500)\n}\n\nconst field = (d2, key) => d2.field(d2.params.store, 'data.' + key)\n// texto da mensagem + link do anexo (imagem, vídeo, áudio, documento)\nconst msgBody = (m) => esc(m.text) + (m.link ? `${m.text ? '<br/>' : ''}<a class=\"att\" href=\"${esc(m.link)}\" target=\"_blank\" rel=\"noopener\">${m.type === 'image' ? '🖼️' : m.type === 'audio' ? '🎧' : m.type === 'video' ? '🎬' : '📎'} abrir anexo</a>` : '')\nconst MAX_FILE = 10 * 1024 * 1024\nconst ACCEPT = '.csv,.pdf,.xls,.xlsx,.mp4,.txt,.jpeg,.jpg,.png,.doc,.docx'\nconst slotOf = (d2) => String(d2.param('slot', '1'))\n// estado da conversa deste painel, sempre lido na hora\nconst ctx = (d2) => {\n  const convs = field(d2, 'conversations').value || []\n  const dual = field(d2, 'viewMode').value === 'dual'\n  const slot = slotOf(d2)\n  let conv\n  if (dual) conv = convs[Number(slot) - 1] || null\n  else { const sel = field(d2, 'selectedConversationId').value; conv = convs.find((c) => c._id === sel) || convs[0] || null }\n  const focused = !dual || String(field(d2, 'focusedSlot').value || '1') === slot\n  const colors = SLOT[dual ? slot : '1'] || SLOT[1]\n  return { conv, dual, slot, focused, colors }\n}\n\n// sincroniza input/placeholder e rolagem com a conversa exibida (chamado no mounted e no updated)\nconst syncView = (d2) => {\n  const panel = d2.$('.panel'); if (!panel) return\n  const cid = panel.dataset.cid || ''\n  const count = d2.$$('.msgs [data-key]').length\n  // trocou de conversa: limpa o rascunho e atualiza o placeholder (input é data-d2-static)\n  if (cid !== d2.root.dataset.cid) {\n    d2.root.dataset.cid = cid\n    const i = d2.$('.composer .txt')\n    if (i) { i.value = ''; i.placeholder = `Escreva uma mensagem para ${panel.dataset.first || ''}...` }\n  }\n  // chegou mensagem nova ou trocou de conversa: rola para o fim\n  const sig = `${cid}:${count}`\n  if (sig !== d2.root.dataset.sig) {\n    d2.root.dataset.sig = sig\n    const box = d2.$('.msgs'); if (box) box.scrollTop = box.scrollHeight\n  }\n}\n\nexport default {\n  css: `\n    :host{display:block;height:100%}\n    .panel{height:100%;box-sizing:border-box;display:flex;flex-direction:column;background:#f4f1ec;font-family:'NaturaCo','Inter',system-ui,sans-serif;color:#2a2622;position:relative}\n    /* modo 2 conversas: a conversa fora de foco fica apagada (clareia no hover para indicar que dá para clicar) */\n    .panel.dual>.head,.panel.dual>.msgs,.panel.dual>.composer{transition:opacity .18s,filter .18s}\n    .panel.dual:not(.focused)>.head,.panel.dual:not(.focused)>.msgs,.panel.dual:not(.focused)>.composer{opacity:.5;filter:grayscale(.55)}\n    .panel.dual:not(.focused):hover>.head,.panel.dual:not(.focused):hover>.msgs,.panel.dual:not(.focused):hover>.composer{opacity:.75}\n    .head{display:flex;align-items:center;gap:12px;padding:12px 20px 12px 16px;background:#fff;border-bottom:1px solid #ece7df;position:relative;z-index:3}\n    .left{display:flex;align-items:center;gap:12px;flex:1;min-width:0;overflow:hidden}\n    .right{display:flex;align-items:center;gap:6px;flex:none;margin-left:16px}\n    .av{position:relative;flex:none;width:40px;height:40px;border-radius:50%;background:var(--accent);color:#fff;font-weight:600;font-size:14px;display:flex;align-items:center;justify-content:center}\n    .av i{position:absolute;right:-1px;bottom:-1px;width:11px;height:11px;border-radius:50%;background:#3aa65c;border:2px solid #fff}\n    .nm{flex:0 1 auto;font-size:16px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:40px}\n    .tag{font-size:11px;border-radius:8px;padding:3px 8px;white-space:nowrap}\n    .tag.ok{background:#e6f4ea;color:#2f8a4c}\n    .tag.slot{background:var(--accent-soft);color:var(--accent);font-weight:700}\n    .timer{display:flex;align-items:center;gap:4px;font-size:12px;border:1px solid #ece7df;border-radius:10px;padding:3px 8px;color:#5d554d;white-space:nowrap;flex:none;font-variant-numeric:tabular-nums}\n    .timer span{display:inline-block;min-width:5.2ch;text-align:left}\n    .tag{flex:none}\n    .sp{flex:1}\n    .btn{display:flex;align-items:center;gap:6px;border:0;border-radius:9px;padding:8px 14px;background:var(--accent);color:#fff;font:inherit;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap}\n    .more{position:relative}\n    .more>button{border:0;background:transparent;color:#8a8178;padding:6px;border-radius:8px;cursor:pointer;display:flex}\n    .more>button:hover{background:#f3eee8}\n    .menu{position:absolute;right:0;top:36px;z-index:20;min-width:220px;background:#fff;border:1px solid #ece7df;border-radius:12px;box-shadow:0 8px 24px rgba(42,38,34,.12);padding:6px}\n    .menu button{display:block;width:100%;text-align:left;border:0;background:transparent;border-radius:8px;padding:9px 12px;font:inherit;font-size:13px;color:#2a2622;cursor:pointer}\n    .menu button:hover{background:#fdf1df}\n    .msgs{flex:1;overflow:auto;padding:18px 20px;display:flex;flex-direction:column;gap:10px}\n    .day{font-size:11px;color:#8a8178;letter-spacing:.04em}\n    .sys{align-self:center;font-size:12px;color:#5d554d;background:#ebe5dd;border-radius:12px;padding:5px 12px}\n    .line{display:flex;gap:8px;align-items:flex-end;max-width:78%}\n    .line.me{align-self:flex-end;flex-direction:row-reverse}\n    .mini{flex:none;width:28px;height:28px;border-radius:50%;background:var(--accent);color:#fff;font-size:10px;font-weight:600;display:flex;align-items:center;justify-content:center}\n    .bubble{border-radius:14px;padding:10px 14px;font-size:13.5px;line-height:1.45;white-space:pre-wrap;word-break:break-word}\n    .them .bubble{background:#fff;border:1px solid #ece7df;border-bottom-left-radius:4px}\n    .me .bubble{background:var(--accent);color:#fff;border-bottom-right-radius:4px}\n    .bot .bubble{background:#fbf6ee;border:1px dashed #e6d3b3}\n    .bot .who{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#b8741b;margin-bottom:3px}\n    .att{display:inline-block;margin-top:4px;font-size:12px;text-decoration:underline;color:inherit}\n    .meta{font-size:10.5px;color:#9a9088;margin-top:3px;display:flex;align-items:center;gap:4px}\n    .me .meta{justify-content:flex-end}\n    .composer{position:relative;display:flex;align-items:center;gap:10px;margin:10px 16px 14px;background:#fff;border:1px solid #e6dfd5;border-radius:14px;padding:6px 6px 6px 12px}\n    .composer:focus-within{border-color:var(--accent)}\n    .composer input{flex:1;min-width:0;border:0;outline:0;font:inherit;font-size:13.5px;background:transparent;color:#2a2622}\n    .ic{border:0;background:transparent;color:#8a8178;display:flex;padding:6px;border-radius:8px;cursor:pointer}\n    .ic:hover{background:#f3eee8}\n    .send{border:0;width:36px;height:36px;flex:none;border-radius:10px;background:var(--accent-soft);color:var(--accent);display:flex;align-items:center;justify-content:center;cursor:pointer}\n    .send:hover{background:var(--accent);color:#fff}\n    .emojis{position:absolute;right:46px;bottom:52px;z-index:20;display:grid;grid-template-columns:repeat(8,30px);gap:2px;background:#fff;border:1px solid #ece7df;border-radius:12px;box-shadow:0 8px 24px rgba(42,38,34,.12);padding:6px}\n    .emojis button{border:0;background:transparent;font-size:18px;border-radius:6px;cursor:pointer;padding:2px}\n    .emojis button:hover{background:#fdf1df}\n    .empty{flex:1;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;color:#8a8178;font-size:13px;text-align:center;padding:24px}\n    .empty b{color:#2a2622;font-size:15px}\n    .ic.mic.rec{color:#d9534f;gap:6px;font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;align-items:center}\n    .rdot{width:8px;height:8px;border-radius:50%;background:#d9534f;animation:rp 1s infinite}\n    @keyframes rp{50%{opacity:.3}}\n    .busy{font-size:12px;color:#8a8178;padding:0 6px}\n    .queued{display:flex;align-items:center;gap:12px;justify-content:space-between;margin:10px 16px 14px;background:#fff;border:1px dashed var(--accent);border-radius:14px;padding:10px 12px 10px 16px;font-size:13px;color:#5d554d}\n    .queued b{color:#2a2622}\n    .btn[disabled]{opacity:.6;cursor:default}\n    .tag.q{background:#fdf1df;color:#b8741b}\n  `,\n  render(d2) {\n    const { conv: c, dual, slot, focused, colors } = ctx(d2)\n    // a distribuição do servidor já entrega o atendimento em andamento (in_progress); a tela não recebe in_queue.\n    // Se um dia aparecer, mostra só o aviso, sem envio.\n    const queued = !!(c && c.transfer && c.transfer.status === 'in_queue')\n    const style = `--accent:${colors.accent};--accent-soft:${colors.soft}`\n    const cls = `panel ${dual ? 'dual' : ''} ${focused ? 'focused' : ''}`\n    if (!c) {\n      return `<div class=\"${cls}\" style=\"${style}\"><div class=\"empty\"><b>${dual ? `Conversa ${esc(slot)} livre` : 'Nenhuma conversa aberta'}</b>Quando um novo atendimento chegar, ele aparece aqui.</div></div>`\n    }\n    let lastDay = ''\n    const body = (c.messages || []).map((m, i) => {\n      const day = dayLabel(m.time)\n      const dayHtml = day && day !== lastDay ? `<div class=\"day\" data-key=\"day-${esc(day)}\">${esc(day)}</div>` : ''\n      lastDay = day || lastDay\n      const key = esc(m.id || m._id || `m${i}`)\n      if (m.sender === 'SYSTEM') return `${dayHtml}<div class=\"sys\" data-key=\"${key}\">${esc(m.text)}</div>`\n      if (m.sender === 'AGENT') return `${dayHtml}<div class=\"line me\" data-key=\"${key}\"><div><div class=\"bubble\">${msgBody(m)}</div><div class=\"meta\">${hhmm(m.time)} ${I.tick}</div></div></div>`\n      const bot = m.sender === 'ASSISTANT'\n      return `${dayHtml}<div class=\"line them ${bot ? 'bot' : ''}\" data-key=\"${key}\"><div class=\"mini\">${bot ? I.star : esc(initials(c.name))}</div><div><div class=\"bubble\">${bot ? `<div class=\"who\">${I.star} Assistente virtual</div>` : ''}${msgBody(m)}</div><div class=\"meta\">${hhmm(m.time)}</div></div></div>`\n    }).join('')\n    return `<div class=\"${cls}\" style=\"${style}\" data-cid=\"${esc(c._id)}\" data-first=\"${esc(String(c.name || '').split(' ')[0])}\">\n      <div class=\"head\">\n        <div class=\"left\">\n          <div class=\"av\">${esc(initials(c.name))}<i></i></div>\n          <div class=\"nm\">${esc(c.name)}</div>\n          <span class=\"timer\">${I.clock}<span data-since=\"${esc(c.since || '')}\">${clock(c.since, Date.now())}</span></span>\n          ${queued ? '<span class=\"tag q\">Na fila</span>' : ''}\n        </div>\n        <div class=\"right\">\n          ${queued ? '' : `<button class=\"btn\" data-act=\"close\">${I.check} Encerrar</button>`}\n          <div class=\"more\"><button data-act=\"menu\" title=\"Mais ações\">${I.dots}</button>\n            ${d2.state.menu ? `<div class=\"menu\">${MENU.map((o) => `<button data-menu=\"${o.id}\">${esc(o.label)}</button>`).join('')}</div>` : ''}\n          </div>\n        </div>\n      </div>\n      <div class=\"msgs\">${body}</div>\n      ${queued ? '<div class=\"queued\"><span><b>Na fila</b> · o atendimento começa sozinho quando a distribuição te atribuir</span></div>' : `<div class=\"composer\">\n        <button class=\"ic\" data-act=\"attach\" title=\"Anexar\">${I.clip}</button>\n        <input class=\"file\" type=\"file\" data-d2-static hidden accept=\"${ACCEPT}\"/>\n        <input class=\"txt\" data-d2-static/>\n        <button class=\"ic\" data-act=\"emoji\" title=\"Emoji\">${I.smile}</button>\n        ${d2.state.emoji ? `<div class=\"emojis\">${EMOJIS.map((e) => `<button data-emoji=\"${e}\">${e}</button>`).join('')}</div>` : ''}\n        ${d2.state.recBusy ? '<span class=\"busy\">preparando áudio…</span>' : d2.state.rec ? `<button class=\"ic\" data-act=\"mic-cancel\" title=\"Descartar gravação\">${I.x}</button><button class=\"ic mic rec\" data-act=\"mic\" title=\"Parar e enviar\"><span class=\"rdot\"></span>${fmtSecs(d2.state.recSecs || 0)}</button>` : `<button class=\"ic mic\" data-act=\"mic\" title=\"Gravar áudio\">${I.mic}</button>`}\n        <button class=\"send\" data-act=\"send\" title=\"Enviar\">${I.send}</button>\n      </div>`}\n    </div>`\n  },\n  mounted(d2) {\n    const input = () => d2.$('.composer .txt')\n    const send = () => {\n      const el = input(); if (!el) return\n      const text = el.value.trim(); if (!text) return\n      el.value = ''\n      d2.emit('onSend', { slot: slotOf(d2), text })\n    }\n    d2.on('.panel', 'mousedown', () => { if (!ctx(d2).focused) d2.emit('onFocus', slotOf(d2)) })\n    d2.on('[data-act=\"send\"]', 'click', send)\n    d2.on('.composer .txt', 'keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send() } })\n    d2.on('[data-act=\"close\"]', 'click', () => d2.emit('onCloseConversation', { slot: slotOf(d2) }))\n    // áudio: 1º clique grava, 2º clique para e envia; o X descarta\n    d2.on('[data-act=\"mic\"]', 'click', () => { if (d2.state.rec) stopRec(d2, false); else startRec(d2) })\n    d2.on('[data-act=\"mic-cancel\"]', 'click', () => stopRec(d2, true))\n    d2.onUnmount(() => stopRec(d2, true))\n    d2.on('[data-act=\"menu\"]', 'click', (e) => { e.stopPropagation(); d2.setState({ menu: !d2.state.menu, emoji: false }) })\n    d2.on('[data-menu]', 'click', (e, el) => { e.stopPropagation(); d2.state.menu = false; d2.emit('onMenuAction', { slot: slotOf(d2), action: el.dataset.menu }) })\n    // anexo: escolhe o arquivo aqui na tela (o gesto do usuário fica no iframe) e manda o arquivo pela ponte\n    d2.on('[data-act=\"attach\"]', 'click', () => { const f = d2.$('.composer .file'); if (f) f.click() })\n    d2.on('.composer .file', 'change', (e, el) => {\n      const file = el.files && el.files[0]\n      el.value = ''\n      if (!file) return\n      if (file.size > MAX_FILE) { if (d2.dialog && d2.dialog.alert) d2.dialog.alert('O arquivo passa de 10 MB.'); return }\n      d2.emit('onAttach', { slot: slotOf(d2), file })\n    })\n    d2.on('[data-act=\"emoji\"]', 'click', (e) => { e.stopPropagation(); d2.setState({ emoji: !d2.state.emoji, menu: false }) })\n    d2.on('[data-emoji]', 'click', (e, el) => {\n      e.stopPropagation()\n      const i = input(); if (i) { i.value += el.dataset.emoji; i.focus() }\n      d2.state.emoji = false\n    })\n    d2.on(document, 'click', () => { if (d2.state.menu || d2.state.emoji) d2.setState({ menu: false, emoji: false }) })\n    if (d2.bus) d2.bus.on('agentDesk:insertText', (p) => {\n      if (!p || String(p.slot) !== slotOf(d2)) return\n      const i = input(); if (!i) return\n      i.value = i.value ? `${i.value} ${p.text}` : p.text\n      i.focus()\n    })\n    const t = setInterval(() => { d2.$$('[data-since]').forEach((el) => { el.textContent = clock(el.dataset.since, Date.now()) }) }, 1000)\n    d2.onUnmount(() => clearInterval(t))\n    syncView(d2)\n  },\n  updated(d2) {\n    syncView(d2)\n  },\n}\n"}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"slot":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"slot"}}}},"onFocus":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onFocus"}}}},"onSend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onSend"}}}},"onCloseConversation":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onCloseConversation"}}}},"onMenuAction":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onMenuAction"}}}},"onAttach":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onAttach"}}}}}}}}}},{"_id":"6af1d07008aee8a1aa8c2a47","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"agentDeskCustomerPanel","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Painel do cliente: mostra a conversa em foco (lida do store).\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst fmt = (t) => { const d = new Date(t); return t && !isNaN(d) ? d.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : '--' }\nconst dur = (s) => { if (s == null || s === '') return '--'; s = Number(s); const p = (n) => String(n).padStart(2, '0'); return `${p(Math.floor(s / 3600))}:${p(Math.floor((s % 3600) / 60))}:${p(s % 60)}` }\nconst SLOT = { 1: { accent: '#e0962b', soft: '#fdf1df' }, 2: { accent: '#c45f3c', soft: '#fbe8e1' } }\nconst I = {\n  user: `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21a8 8 0 0116 0\"/></svg>`,\n  note: `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M14 3H6a2 2 0 00-2 2v14a2 2 0 002 2h12a2 2 0 002-2V9z\"/><path d=\"M14 3v6h6M8 13h8M8 17h5\"/></svg>`,\n  hist: `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M3 12a9 9 0 109-9 9 9 0 00-6.4 2.6L3 8\"/><path d=\"M3 3v5h5M12 7v5l3 2\"/></svg>`,\n  prev: `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M21 12a9 9 0 11-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/></svg>`,\n  chev: `<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M6 9l6 6 6-6\"/></svg>`,\n}\nconst field = (d2, key) => d2.field(d2.params.store, 'data.' + key)\nconst ctx = (d2) => {\n  const convs = field(d2, 'conversations').value || []\n  const dual = field(d2, 'viewMode').value === 'dual'\n  const slot = String(field(d2, 'focusedSlot').value || '1')\n  let conv\n  if (dual) conv = convs[Number(slot) - 1] || null\n  else { const sel = field(d2, 'selectedConversationId').value; conv = convs.find((c) => c._id === sel) || convs[0] || null }\n  return { conv, dual, slot, colors: SLOT[dual ? slot : '1'] || SLOT[1] }\n}\n// textarea é data-d2-static: ao trocar de conversa, carrega a anotação dela\nconst syncNotes = (d2) => {\n  const wrap = d2.$('.wrap'); if (!wrap) return\n  const cid = wrap.dataset.cid || ''\n  if (cid !== d2.root.dataset.cid) {\n    d2.root.dataset.cid = cid\n    const ta = d2.$('textarea'); const { conv } = ctx(d2)\n    if (ta) ta.value = (conv && conv.notes) || ''\n  }\n}\n\nexport default {\n  css: `\n    :host{display:block;height:100%}\n    .wrap{height:100%;box-sizing:border-box;overflow:auto;padding:12px;display:flex;flex-direction:column;gap:10px;background:#faf8f5;font-family:'NaturaCo','Inter',system-ui,sans-serif;color:#2a2622}\n    .who{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;color:var(--accent);background:var(--accent-soft);border-radius:10px;padding:8px 12px}\n    .who i{width:8px;height:8px;border-radius:50%;background:var(--accent)}\n    .card{background:#fff;border:1px solid #ece7df;border-radius:14px;overflow:hidden;flex:none}\n    .ch{display:flex;align-items:center;gap:10px;padding:14px;cursor:pointer;font-size:13.5px;font-weight:600;user-select:none}\n    .ch .ico{color:#e0962b;display:flex}\n    .ch .n{margin-left:auto;font-size:11px;color:#8a8178;font-weight:400}\n    .ch .cv{color:#8a8178;display:flex;transition:transform .15s}\n    .card.open .cv{transform:rotate(180deg)}\n    .cb{padding:0 14px 12px}\n    .cb.hide{display:none}\n    .f{padding:8px 0;border-top:1px solid #f1ece5}\n    .f:first-child{border-top:0}\n    .lb{font-size:10px;font-weight:700;letter-spacing:.05em;color:#9a9088;text-transform:uppercase;margin-bottom:3px}\n    .vl{font-size:13px;word-break:break-word}\n    .badge{display:inline-block;margin-left:6px;font-size:10px;font-weight:600;color:#2f8a4c;background:#e6f4ea;border-radius:8px;padding:1px 6px;vertical-align:middle}\n    textarea{width:100%;box-sizing:border-box;min-height:90px;border:1px solid #e6dfd5;border-radius:10px;padding:8px 10px;font:inherit;font-size:13px;resize:vertical;outline:0}\n    textarea:focus{border-color:#e0962b}\n    .save{margin-top:8px;border:0;border-radius:9px;padding:7px 14px;background:#e0962b;color:#fff;font:inherit;font-size:12.5px;font-weight:600;cursor:pointer}\n    .ok{margin-left:8px;font-size:12px;color:#2f8a4c}\n    .h{padding:8px 0;border-top:1px solid #f1ece5;font-size:12.5px}\n    .h:first-child{border-top:0}\n    .h a{color:#b8741b;font-weight:600;cursor:pointer;text-decoration:underline}\n    .h small{display:block;color:#8a8178;margin-top:2px}\n    .empty{font-size:12.5px;color:#8a8178;padding:4px 0 6px}\n  `,\n  render(d2) {\n    const { conv: c, dual, slot, colors } = ctx(d2)\n    const style = `--accent:${colors.accent};--accent-soft:${colors.soft}`\n    if (!c) return `<div class=\"wrap\" style=\"${style}\"><div class=\"empty\">Selecione uma conversa para ver os dados do cliente.</div></div>`\n    const open = Object.assign({ data: true }, d2.state.open)\n    const cu = c.customer || {}\n    const tr = c.transfer || {}\n    const hist = c.history || []\n    const field_ = (lb, v, extra = '') => `<div class=\"f\"><div class=\"lb\">${lb}</div><div class=\"vl\">${esc(v || '--')}${extra}</div></div>`\n    // o corpo da sanfona de anotações fica sempre no DOM (só escondido) para não perder o que foi digitado\n    const section = (id, ico, title, inner, count, keep) => `<div class=\"card ${open[id] ? 'open' : ''}\">\n      <div class=\"ch\" data-sec=\"${id}\"><span class=\"ico\">${ico}</span>${title}${count != null ? `<span class=\"n\">${count}</span>` : ''}<span class=\"cv\" style=\"${count != null ? '' : 'margin-left:auto'}\">${I.chev}</span></div>\n      ${open[id] || keep ? `<div class=\"cb ${open[id] ? '' : 'hide'}\">${inner}</div>` : ''}\n    </div>`\n    return `<div class=\"wrap\" style=\"${style}\" data-cid=\"${esc(c._id)}\">\n      ${dual ? `<div class=\"who\"><i></i>Conversa ${esc(slot)} · ${esc(c.name)}</div>` : ''}\n      ${section('data', I.user, 'Dados do cliente', [\n        field_('Nome', cu.name || c.name),\n        field_('E-mail', cu.email),\n        field_('Documento', cu.document),\n        field_('Código do cliente', cu.code, cu.authenticated ? '<span class=\"badge\">autenticado</span>' : ''),\n        field_('Telefone', cu.phone),\n        field_('IP consumidor', cu.ip),\n        field_('Fila', cu.queue),\n      ].join(''))}\n      ${section('notes', I.note, 'Anotações de atendimento', `<textarea data-d2-static placeholder=\"Insira sua anotação\"></textarea><div><button class=\"save\" data-act=\"notes\">Salvar anotação</button>${d2.state.saved ? '<span class=\"ok\">Anotação salva</span>' : ''}</div>`, null, true)}\n      ${section('current', I.hist, 'Histórico deste atendimento', [\n        field_('Protocolo', tr.protocol), field_('Chat ID', tr.chat_id), field_('Canal', tr.channel), field_('Estado', tr.status),\n        field_('Entrada na fila', fmt(tr.queued_at)), field_('Início do atendimento', fmt(tr.agent_joined_at)), field_('Tempo na fila', dur(tr.queue_time)),\n      ].join(''))}\n      ${section('previous', I.prev, 'Atendimentos anteriores', hist.length ? hist.map((h, i) => `<div class=\"h\" data-key=\"${esc(h.chat_id || i)}\"><a data-hist=\"${esc(h.chat_id || '')}\">${esc(h.protocol || '--')}</a><small>${esc(h.classification || '--')} · ${fmt(h.ended_at)}</small></div>`).join('') : '<div class=\"empty\">Nenhum atendimento anterior hoje.</div>', hist.length)}\n    </div>`\n  },\n  mounted(d2) {\n    d2.on('[data-sec]', 'click', (e, el) => {\n      const open = Object.assign({ data: true }, d2.state.open)\n      open[el.dataset.sec] = !open[el.dataset.sec]\n      d2.state.open = open\n    })\n    d2.on('[data-act=\"notes\"]', 'click', () => {\n      const ta = d2.$('textarea')\n      d2.emit('onSaveNotes', { notes: ta ? ta.value : '' })\n      d2.state.saved = true\n      clearTimeout(d2.root.__savedTimer)\n      d2.root.__savedTimer = setTimeout(() => { d2.state.saved = false }, 2500)\n    })\n    d2.onUnmount(() => clearTimeout(d2.root.__savedTimer))\n    d2.on('[data-hist]', 'click', (e, el) => d2.emit('onOpenHistory', el.dataset.hist))\n    syncNotes(d2)\n  },\n  updated(d2) {\n    syncNotes(d2)\n  },\n}\n"}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"onSaveNotes":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onSaveNotes"}}}},"onOpenHistory":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onOpenHistory"}}}}}}}}}},{"_id":"6af1d099d169681f5e7c8c37","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"agentDeskWorkspace","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Monta a tela de atendimento: lista + mensagens rápidas | 1 ou 2 chats | painel do cliente.\n// Fonte única de verdade = store `data.*` (conversations, selectedConversationId, focusedSlot, viewMode, activeTab).\n// Cada filho lê o store sozinho; aqui só fica o layout e o encaminhamento das ações.\n// Regra de negócio sai pelos handlers (on*), que serão ligados a UCs.\n// Com useMockData = true, carrega conversas de exemplo no store e simula as ações localmente.\nconst MENU_LABEL = { transfer: 'Transferir atendimento', download: 'Baixar conversa (PDF)', history_cpf: 'Histórico deste CPF', history_ip: 'Histórico do IP' }\n\nconst field = (d2, key) => d2.field(d2.params.store, 'data.' + key)\nconst isMock = (d2) => d2.param('useMockData', true) !== false\nconst getConvs = (d2) => field(d2, 'conversations').value || []\nconst setConvs = (d2, list) => field(d2, 'conversations').set(list)\n\n// conversa exibida num slot, sempre calculada na hora\nconst convOfSlot = (d2, slot) => {\n  const convs = getConvs(d2)\n  if (field(d2, 'viewMode').value === 'dual') return convs[Number(slot) - 1] || null\n  const sel = field(d2, 'selectedConversationId').value\n  return convs.find((c) => c._id === sel) || convs[0] || null\n}\nconst focusedConv = (d2) => {\n  const dual = field(d2, 'viewMode').value === 'dual'\n  return convOfSlot(d2, dual ? String(field(d2, 'focusedSlot').value || '1') : '1')\n}\nconst patchConv = (d2, id, fn) => setConvs(d2, getConvs(d2).map((c) => (c._id === id ? fn(c) : c)))\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst PDF_LIB = { url: 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js', global: 'html2pdf' }\nconst SENDER_LABEL = { AGENT: 'Atendente', HUMAN: 'Cliente', ASSISTANT: 'Assistente virtual', SYSTEM: 'Sistema' }\nconst fmtTime = (t) => { const d = new Date(t); return isNaN(d) ? '' : d.toLocaleString('pt-BR') }\n// espera a biblioteca de PDF carregar (até ~10 s); null se falhar\nconst loadPdfLib = async (d2) => {\n  for (let i = 0; i < 100; i++) {\n    const lib = d2.lib(PDF_LIB)\n    if (lib) return lib\n    if (d2.lib.state && d2.lib.state(PDF_LIB) === 'error') return null\n    await new Promise((r) => setTimeout(r, 100))\n  }\n  return null\n}\nconst downloadPdf = async (d2, conv) => {\n  if (!conv) return\n  const proto = (conv.transfer && conv.transfer.protocol) || conv._id\n  const html = `<div style=\"font-family:Arial,sans-serif;font-size:12px;color:#222;padding:16px\">\n    <h2 style=\"margin:0 0 4px;font-size:16px\">Atendimento ${esc(proto)}</h2>\n    <div style=\"color:#666;margin-bottom:12px\">${esc(conv.name)} · gerado em ${esc(new Date().toLocaleString('pt-BR'))}</div>\n    ${(conv.messages || []).map((m) => `<div style=\"margin:8px 0\"><b>${esc(SENDER_LABEL[m.sender] || m.sender)}</b> <span style=\"color:#888\">${esc(fmtTime(m.time))}</span><br/>${esc(m.text)}${m.link ? ` <a href=\"${esc(m.link)}\">${esc(m.link)}</a>` : ''}</div>`).join('')}\n  </div>`\n  const lib = await loadPdfLib(d2)\n  if (lib) {\n    const el = document.createElement('div')\n    el.innerHTML = html\n    await lib().set({ margin: 10, filename: `atendimento-${proto}.pdf`, html2canvas: { scale: 2 }, jsPDF: { unit: 'mm', format: 'a4' } }).from(el).save()\n    return\n  }\n  // reserva: janela de impressão do navegador (salvar como PDF)\n  const w = window.open('', '_blank')\n  if (w) { w.document.write(html); w.document.close(); w.focus(); w.print() }\n}\nconst notice = (d2, msg) => { if (d2.dialog && d2.dialog.alert) d2.dialog.alert(msg); else d2.log(msg) }\n// Ponte (tela dentro de iframe): além do evento da instância, cada ação vira um evento de janela\n// 'agentDesk:action'. O agentDeskBridgeGuest escuta e repassa para o app de fora. Sem a ponte, ninguém escuta.\nconst relay = (type, payload) => { try { window.dispatchEvent(new CustomEvent('agentDesk:action', { detail: { type, payload } })) } catch (e) {} }\nconst out = (d2, name, payload) => { d2.emit(name, payload); relay(name, payload) }\n\nconst mock = () => {\n  const now = Date.now()\n  const at = (minAgo) => new Date(now - minAgo * 60000).toISOString()\n  return [\n    {\n      _id: 'mock-camila', name: 'Camila Ferreira', since: at(3.2), notes: '',\n      customer: { name: 'Camila Ferreira', email: 'camila.ferreira@email.com', document: '312.***.***-04', code: 'NAT-4471829', phone: '(11) 9 4xxx-xx21', ip: '189.45.118.7', queue: 'AVON CFD & AVON VD', authenticated: true },\n      transfer: { protocol: '2026110900123', chat_id: 'kxHxRfzm8WFx', channel: 'web', status: 'in_progress', queued_at: at(5), agent_joined_at: at(3.2), queue_time: 108 },\n      history: [{ protocol: '2026110800871', classification: 'ended_by_agent', ended_at: at(1300), chat_id: 'abc' }, { protocol: '2026110700512', classification: 'ended_by_user', ended_at: at(2900), chat_id: 'def' }],\n      messages: [\n        { id: 's1', sender: 'SYSTEM', text: 'Conversa transferida do assistente virtual para você', time: at(4) },\n        { id: 'h1', sender: 'HUMAN', text: 'Oi, boa tarde! Fiz um pedido semana passada (nº 4471829) e ainda não recebi.', time: at(4) },\n        { id: 'h2', sender: 'HUMAN', text: 'Já passou da data prevista de entrega e estou ficando preocupada 😟', time: at(3.8) },\n        { id: 'a1', sender: 'AGENT', text: 'Olá, Camila! Tudo bem? Vou cuidar disso com você agora mesmo.', time: at(3) },\n        { id: 'a2', sender: 'AGENT', text: 'Só um instante enquanto localizo o seu pedido pelo número, tá?', time: at(2.9) },\n        { id: 'h3', sender: 'HUMAN', text: 'Tá bom, fico no aguardo 🙂', time: at(2) },\n        { id: 'a3', sender: 'AGENT', text: 'Encontrei aqui ✅ Seu pedido saiu para entrega hoje de manhã e deve chegar até as 19h.', time: at(1) },\n      ],\n    },\n    {\n      _id: 'mock-juliana', name: 'Juliana Souza', since: at(11.8), notes: '',\n      customer: { name: 'Juliana Souza', email: 'juliana.souza@email.com', document: '458.***.***-90', code: 'NAT-5520193', phone: '(21) 9 8xxx-xx44', ip: '177.22.90.13', queue: 'NATURA CF' },\n      transfer: { protocol: '2026110900131', chat_id: 'pQ7sLm2Kx9aZ', channel: 'web', status: 'in_progress', queued_at: at(14), agent_joined_at: at(11.8), queue_time: 132 },\n      history: [],\n      messages: [\n        { id: 's1', sender: 'SYSTEM', text: 'Conversa transferida do assistente virtual para você', time: at(12) },\n        { id: 'b1', sender: 'ASSISTANT', text: 'Resumo: cliente quer trocar um produto que chegou com a embalagem danificada.', time: at(12) },\n        { id: 'h1', sender: 'HUMAN', text: 'Oi! Meu hidratante chegou com a tampa quebrada, consigo trocar?', time: at(11.5) },\n        { id: 'a1', sender: 'AGENT', text: 'Oi, Juliana! Consegue sim. Você pode me mandar uma foto da embalagem?', time: at(10) },\n      ],\n    },\n  ]\n}\n// scope 'mine' = do agente (ordem dele); 'general' = da equipe (ordem da gestão)\nconst MOCK_QUICK = [\n  { _id: 'q1', scope: 'mine', title: 'Saudação', text: 'Olá! Tudo bem? Sou o Ramon e vou te ajudar agora mesmo. 😊' },\n  { _id: 'q2', scope: 'mine', title: 'Localizar pedido', text: 'Você pode me informar o número do pedido para eu localizar, por favor?' },\n  { _id: 'q3', scope: 'mine', title: 'Aguardar', text: 'Só um instante enquanto verifico essa informação para você.' },\n  { _id: 'q4', scope: 'mine', title: 'Prazo de entrega', text: 'Seu pedido já saiu para entrega e deve chegar ainda hoje até as 19h.' },\n  { _id: 'q5', scope: 'general', title: 'Política de troca', text: 'A troca pode ser solicitada em até 7 dias após o recebimento. Posso te enviar o passo a passo.' },\n  { _id: 'q6', scope: 'general', title: 'Reembolso', text: 'O reembolso é processado em até 5 dias úteis, na mesma forma de pagamento.' },\n  { _id: 'q7', scope: 'general', title: 'Virar consultora', text: 'Que ótimo! Vou te enviar o link de cadastro para você começar como consultora. 💛' },\n]\n\n// ---- handlers: sempre leem o store na hora do clique ----\nconst handlers = (d2) => ({\n  onSelect: (id) => {\n    const convs = getConvs(d2)\n    const idx = convs.findIndex((c) => c._id === id)\n    if (field(d2, 'viewMode').value === 'dual' && idx > -1 && idx < 2) field(d2, 'focusedSlot').set(String(idx + 1))\n    else { field(d2, 'selectedConversationId').set(id); field(d2, 'focusedSlot').set('1') }\n  },\n  onFocus: (slot) => field(d2, 'focusedSlot').set(String(slot)),\n  onSend: (p) => {\n    const conv = convOfSlot(d2, p && p.slot)\n    if (!conv || !p || !p.text) return\n    if (isMock(d2)) {\n      const msg = { id: `local-${Date.now()}`, sender: 'AGENT', text: p.text, time: new Date().toISOString() }\n      patchConv(d2, conv._id, (c) => ({ ...c, messages: [...(c.messages || []), msg] }))\n    }\n    out(d2, 'onSend', { slot: p.slot, text: p.text, conversationId: conv._id })\n  },\n  onCloseConversation: async (p) => {\n    const conv = convOfSlot(d2, p && p.slot)\n    if (!conv) return\n    // confirma sempre (com dados de exemplo ou de verdade)\n    const ok = d2.dialog && d2.dialog.confirm ? await d2.dialog.confirm(`Encerrar o atendimento com ${conv.name}? Essa ação é irreversível.`) : true\n    if (!ok) return\n    if (isMock(d2)) {\n      setConvs(d2, getConvs(d2).filter((c) => c._id !== conv._id))\n      field(d2, 'selectedConversationId').set('')\n      field(d2, 'focusedSlot').set('1')\n    }\n    out(d2, 'onCloseConversation', { slot: p.slot, conversationId: conv._id })\n  },\n  onMenuAction: (p) => {\n    const conv = convOfSlot(d2, p && p.slot)\n    // baixar conversa: o PDF é gerado aqui na tela (o app de fora não enxerga o conteúdo do iframe)\n    if (p && p.action === 'download') { downloadPdf(d2, conv); return }\n    if (isMock(d2)) notice(d2, `\"${MENU_LABEL[p.action] || p.action}\" será ligado na etapa de lógica.`)\n    out(d2, 'onMenuAction', { slot: p.slot, action: p.action, conversationId: conv && conv._id })\n  },\n  onAttach: (p) => {\n    const conv = convOfSlot(d2, p && p.slot)\n    if (!conv || !p || !p.file) return\n    if (isMock(d2)) notice(d2, `Anexo \"${p.file.name}\" (dados de exemplo: não é enviado).`)\n    out(d2, 'onAttach', { slot: p.slot, conversationId: conv._id, file: p.file })\n  },\n  onSaveNotes: (p) => {\n    const conv = focusedConv(d2)\n    if (!conv) return\n    if (isMock(d2)) patchConv(d2, conv._id, (c) => ({ ...c, notes: p.notes }))\n    out(d2, 'onSaveNotes', { notes: p.notes, conversationId: conv._id })\n  },\n  onOpenHistory: (chatId) => {\n    if (isMock(d2)) notice(d2, 'Abrir atendimento anterior será ligado na etapa de lógica.')\n    out(d2, 'onOpenHistory', chatId)\n  },\n  // envio rápido de uma mensagem pronta: vai direto para o chat em foco (mesmo caminho do envio normal)\n  onQuickSend: (text) => {\n    const dual = field(d2, 'viewMode').value === 'dual'\n    const slot = dual ? String(field(d2, 'focusedSlot').value || '1') : '1'\n    handlers(d2).onSend({ slot, text })\n  },\n  // nova ordem das mensagens do agente (as da equipe não mudam)\n  onReorder: (p) => {\n    if (!p || !p.ids) return\n    if (isMock(d2)) {\n      const all = field(d2, 'quickMessages').value || []\n      const mine = p.ids.map((id) => all.find((m) => m._id === id)).filter(Boolean)\n      const rest = all.filter((m) => !p.ids.includes(m._id))\n      field(d2, 'quickMessages').set([...mine, ...rest])\n    }\n    out(d2, 'onReorderQuickMessages', { scope: 'mine', ids: p.ids })\n  },\n  // cadastro de mensagens rápidas (aba Gerenciar)\n  onSaveQuickMessage: (p) => {\n    if (!p || !p.text) return\n    if (isMock(d2)) {\n      const all = field(d2, 'quickMessages').value || []\n      if (p._id) field(d2, 'quickMessages').set(all.map((m) => (m._id === p._id ? { ...m, title: p.title || m.title, text: p.text, queues: p.queues || [] } : m)))\n      else field(d2, 'quickMessages').set([...all, { _id: `local-${Date.now()}`, scope: 'mine', title: p.title || p.text.slice(0, 40), text: p.text, queues: p.queues || [] }])\n    }\n    out(d2, 'onSaveQuickMessage', { _id: p._id || '', title: p.title || '', text: p.text, queues: p.queues || [] })\n  },\n  onDeleteQuickMessage: (p) => {\n    if (!p || !p._id) return\n    if (isMock(d2)) field(d2, 'quickMessages').set((field(d2, 'quickMessages').value || []).filter((m) => m._id !== p._id))\n    out(d2, 'onDeleteQuickMessage', { _id: p._id })\n  },\n  onPick: (text) => {\n    const dual = field(d2, 'viewMode').value === 'dual'\n    if (d2.bus) d2.bus.emit('agentDesk:insertText', { slot: dual ? String(field(d2, 'focusedSlot').value || '1') : '1', text })\n  },\n})\n\nexport default {\n  css: `\n    :host{display:block;height:100%}\n    .ws{height:100%;display:grid;grid-template-columns:300px minmax(0,1fr) 300px;background:#f7f5f2;font-family:'NaturaCo','Inter',system-ui,sans-serif}\n    .left{display:grid;grid-template-rows:200px minmax(0,1fr);border-right:1px solid #ece7df;background:#fff;min-height:0}\n    .center{display:grid;min-height:0;min-width:0}\n    .center.dual{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:1px;background:#e6dfd5}\n    .right{border-left:1px solid #ece7df;min-height:0}\n    .cell{min-height:0;min-width:0;height:100%}\n    .placeholder{display:flex;align-items:center;justify-content:center;color:#8a8178;font-size:14px;background:#f4f1ec}\n  `,\n  render(d2) {\n    // este componente só reage ao layout (modo e aba); o conteúdo é lido pelos filhos\n    const dual = field(d2, 'viewMode').value === 'dual'\n    const tab = field(d2, 'activeTab').value || 'conversations'\n    const h = handlers(d2)\n    const store = d2.params.store\n    const chat = (slot) => `<div class=\"cell\">${d2.render(d2.param('chatPanel'), { store, slot: String(slot) }, {\n      key: `chat-${dual ? slot : 'single'}`,\n      onFocus: h.onFocus, onSend: h.onSend, onCloseConversation: h.onCloseConversation, onMenuAction: h.onMenuAction, onAttach: h.onAttach,\n    })}</div>`\n    const center = tab === 'quick'\n      ? `<div class=\"center\"><div class=\"cell\">${d2.render(d2.param('quickMessages'), { store, mode: 'manage' }, { key: 'quick-manage', onReorder: h.onReorder, onSaveQuickMessage: h.onSaveQuickMessage, onDeleteQuickMessage: h.onDeleteQuickMessage })}</div></div>`\n      : `<div class=\"center ${dual ? 'dual' : ''}\">${dual ? chat(1) + chat(2) : chat(1)}</div>`\n    return `<div class=\"ws\">\n      <div class=\"left\">\n        <div class=\"cell\">${d2.render(d2.param('conversationList'), { store, maxConversations: 2 }, { key: 'list', onSelect: h.onSelect })}</div>\n        <div class=\"cell\">${d2.render(d2.param('quickMessages'), { store, mode: 'panel' }, { key: 'quick', onPick: h.onPick, onQuickSend: h.onQuickSend, onReorder: h.onReorder, onSaveQuickMessage: h.onSaveQuickMessage, onDeleteQuickMessage: h.onDeleteQuickMessage })}</div>\n      </div>\n      ${center}\n      <div class=\"right\"><div class=\"cell\">${d2.render(d2.param('customerPanel'), { store }, { key: 'customer', onSaveNotes: h.onSaveNotes, onOpenHistory: h.onOpenHistory })}</div></div>\n    </div>`\n  },\n  mounted(d2) {\n    // carrega as conversas de exemplo no store (só se ainda estiver vazio)\n    if (isMock(d2) && !getConvs(d2).length) setConvs(d2, mock())\n    if (isMock(d2) && !(field(d2, 'quickMessages').value || []).length) field(d2, 'quickMessages').set(MOCK_QUICK)\n  },\n}\n"}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"conversationList":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"conversationList"}}}},"quickMessages":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"quickMessages"}}}},"chatPanel":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"chatPanel"}}}},"customerPanel":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"customerPanel"}}}},"useMockData":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"useMockData"}}}},"onSend":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onSend"}}}},"onCloseConversation":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onCloseConversation"}}}},"onMenuAction":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onMenuAction"}}}},"onSaveNotes":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onSaveNotes"}}}},"onOpenHistory":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onOpenHistory"}}}},"onAttach":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onAttach"}}}},"onReorderQuickMessages":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onReorderQuickMessages"}}}},"onSaveQuickMessage":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onSaveQuickMessage"}}}},"onDeleteQuickMessage":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"onDeleteQuickMessage"}}}}}}}}}},{"_id":"6af1f384257f7c9b0d960973","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"agentDeskBridgeGuest","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Ponte — lado de DENTRO. Roda no app \"embed\" (.next), dentro do iframe da tela ponte.\n// - Recebe do app de fora (window.parent) o estado da tela (conversas, mensagens rápidas, status) e grava no store.\n// - Repassa ao app de fora as ações da tela (evento de janela 'agentDesk:action' que os outros componentes disparam).\n// Não lê dado nenhum e não precisa de login: só aceita mensagem do window.parent com origem do portal.\nconst PARENT_OK = /^https:\\/\\/([a-z0-9-]+\\.)*natura-br-cf\\.(data2\\.app|next\\.data2\\.app|data2\\.qa|data2\\.builders)$/\n// chaves que o app de fora pode preencher. O resto (aba, 1/2 conversas, foco) é estado de tela local.\nconst STATE_KEYS = ['conversations', 'quickMessages', 'agentStatus', 'agent', 'statusOptions', 'queueOptions']\n\nconst field = (d2, key) => d2.field(d2.params.store, 'data.' + key)\nconst embedded = () => { try { return window.parent && window.parent !== window } catch (e) { return true } }\nconst parentOrigin = () => {\n  let o = ''\n  try { if (location.ancestorOrigins && location.ancestorOrigins.length) o = location.ancestorOrigins[0] } catch (e) {}\n  if (!o && document.referrer) { try { o = new URL(document.referrer).origin } catch (e) {} }\n  return { origin: o, ok: PARENT_OK.test(o) }\n}\nconst plain = (v) => { try { return JSON.parse(JSON.stringify(v === undefined ? null : v)) } catch (e) { return null } }\n// onSend -> send, onCloseConversation -> closeConversation, ...\nconst eventName = (t) => { const n = String(t || '').replace(/^on/, ''); return n.charAt(0).toLowerCase() + n.slice(1) }\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\n\nexport default {\n  css: `\n    :host{display:block;height:100%;pointer-events:none}\n    .chip{pointer-events:auto;display:flex;align-items:center;gap:10px;height:100%;box-sizing:border-box;padding:0 12px;border-radius:10px;background:rgba(42,38,34,.88);color:#fff;font:12px/1.2 'Inter',system-ui,sans-serif;white-space:nowrap;overflow:hidden}\n    .dot{flex:none;width:8px;height:8px;border-radius:50%;background:#a39a90}\n    .dot.ok{background:#3aa65c}.dot.wait{background:#e0962b}.dot.err{background:#d9534f}\n    .mut{color:#cfc6bc}\n    button{pointer-events:auto;border:0;border-radius:7px;padding:4px 9px;background:#e0962b;color:#fff;font:inherit;cursor:pointer}\n  `,\n  render(d2) {\n    if (!d2.param('debug', false)) return '<div></div>'\n    const s = d2.state\n    const cls = s.level === 'ok' ? 'ok' : s.level === 'err' ? 'err' : 'wait'\n    return `<div class=\"chip\">\n      <span class=\"dot ${cls}\"></span>\n      <b>Ponte (dentro):</b> ${esc(s.status || '')}\n      <span class=\"mut\">recebidos ${s.got || 0} · enviados ${s.sent || 0}</span>\n      <button data-act=\"mic\">Testar microfone</button>\n      <span class=\"mut\">${esc(s.mic || '')}</span>\n    </div>`\n  },\n  mounted(d2) {\n    let alive = true\n    d2.onUnmount(() => { alive = false })\n\n    // teste de microfone (item 3 do protótipo) — ligado antes de qualquer retorno, funciona dentro ou fora do iframe\n    d2.on('[data-act=\"mic\"]', 'click', async () => {\n      d2.state.mic = 'testando…'\n      let policy = 'desconhecida'\n      try {\n        const fp = document.permissionsPolicy || document.featurePolicy\n        if (fp && fp.allowsFeature) policy = fp.allowsFeature('microphone') ? 'liberado nesta página' : 'bloqueado nesta página'\n      } catch (e) {}\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia({ audio: true })\n        stream.getTracks().forEach((t) => t.stop())\n        d2.state.mic = `microfone OK (${policy})`\n      } catch (err) {\n        d2.state.mic = `microfone falhou: ${(err && err.name) || err} (${policy})`\n      }\n    })\n\n    const p = parentOrigin()\n    if (!embedded()) { d2.setState({ status: 'aberto fora de iframe (sem app de fora)', level: 'err' }); return }\n    if (!p.ok) { d2.setState({ status: `origem do app de fora não permitida: ${p.origin || '(vazia)'}`, level: 'err' }); return }\n    d2.setState({ status: `aguardando o app de fora (${p.origin})`, level: 'wait', got: 0, sent: 0 })\n\n    const post = (type, payload) => {\n      try {\n        // arquivo (anexo) vai inteiro; o resto vira JSON puro\n        const file = payload && typeof Blob !== 'undefined' && payload.file instanceof Blob ? payload.file : null\n        const data = plain(file ? { ...payload, file: undefined } : payload)\n        if (file && data) data.file = file\n        window.parent.postMessage({ type: 'agentDesk:' + type, payload: data }, p.origin)\n        d2.state.sent = (d2.state.sent || 0) + 1\n      } catch (e) { d2.setState({ status: `erro ao enviar: ${e && e.message}`, level: 'err' }) }\n    }\n\n    // estado vindo do app de fora -> store\n    d2.on(window, 'message', (e) => {\n      if (e.source !== window.parent || e.origin !== p.origin) return\n      const m = e.data\n      if (!m || m.type !== 'agentDesk:state' || !m.data || typeof m.data !== 'object') return\n      STATE_KEYS.forEach((k) => { if (Object.prototype.hasOwnProperty.call(m.data, k)) field(d2, k).set(m.data[k]) })\n      d2.setState({ status: `conectada (${p.origin})`, level: 'ok', got: (d2.state.got || 0) + 1 })\n    })\n\n    // ações da tela -> app de fora\n    d2.on(window, 'agentDesk:action', (e) => {\n      const d = (e && e.detail) || {}\n      if (d.type) post(eventName(d.type), d.payload)\n    })\n\n    // \"estou pronto\": repete a cada 1 s até chegar o primeiro estado (o app de fora pode carregar depois do iframe)\n    let tries = 0\n    const hello = () => {\n      if (!alive || d2.state.got || tries++ >= 30) return\n      post('ready', { at: Date.now() })\n      setTimeout(hello, 1000)\n    }\n    hello()\n  },\n}\n"}},"store":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"store"}}}},"debug":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"debug"}}}}}}}}}},{"_id":"6af21068e88217babb72fc6a","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"kanban_mcp_boas_praticas","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const SECOES = [\n  {\n    id: 'kanban',\n    titulo: 'O combinado do kanban',\n    sub: 'Cada Claude grava os cards do mesmo jeito, para que o Claude de outra pessoa consiga ler sem perguntar nada.',\n    itens: [\n      [\"Ler antes de gravar\", \"Antes de criar um card, leia a tabela para não duplicar o card de outra pessoa.\"],\n      [\"ID do card\", \"O `card_id` é a frente mais o número (`TRC-07`), gerado pelo `kanban_tarefa_criar`. O ticket do JIRA fica na demanda (`JCFA-1234`), ligada pelo `demand_key`. O nome da sessão do Claude começa com o `card_id`.\"],\n      [\"Declarar o que vai mexer\", \"Preencha `touches` com a reference completa e o nome de cada serviço, tela ou UC que vai editar. O `kanban_tarefa_pegar` avisa quando outra tarefa em andamento mexe no mesmo lugar: combine com a dona por um evento `request` antes de seguir.\"],\n      [\"O card anda com o trabalho\", \"Começou: `kanban_tarefa_pegar`. Mexeu no system: `kanban_mudanca`. Entregou: `in_validation` com `result` e `approver`. Publicou: `done` com a `app_version`.\"],\n      [\"Dependência em depends_on\", \"Ligue tarefas pelo `card_id` em `depends_on`. Ninguém pega uma tarefa enquanto alguma dependência não estiver em `done`.\"],\n      [\"Pedido é evento\", \"Precisa de algo de outra pessoa? `kanban_evento` com `type: request` e `to` = ela. Entra na caixa de entrada dela, no `kanban_minha_fila`. Dúvida de regra vira `question`, nunca suposição.\"],\n      [\"Não apagar\", \"Tarefa descartada vai para `cancelled` com `reason`, para o histórico ficar. A escrita direta nas tabelas é recusada: tudo passa pelos rushes.\"],\n      [\"Quem só planeja não grava\", \"Sessão de planejamento escreve no plano uma 'Nota proposta para o card'. O dono do card decide o que entra.\"]\n    ]\n  },\n  {\n    id: 'analise',\n    titulo: 'Antes de afirmar como algo funciona',\n    sub: 'O br-cf tem muito código comentado e muita integração fora do fluxo principal. Entender direito vale mais do que terminar rápido.',\n    itens: [\n      [\"Comece pelos dois artboards\", \"Todo fluxo do bot parte de `onboarding-navigation-CHAT` (`h-6OYFkUln7jfbRri6t2R`), que escolhe a tela pelo `data.flow`, e do menu `new-service-menu` (`nrbALWBoiqECspZ_sIBW1`), cujos cards gravam `data.flow` e `data.navigation`. Os fluxos não usam rotas do router.\"],\n      [\"Comentado é disabled:true\", \"O `handlerCode`, o `definitionCode` e o d2lang do MCP escondem o `disabled`. Antes de afirmar comportamento, leia o JSON cru: `get_service` com `view:'full'`, `get_artboard_tree` full ou `get_component` com `maxChars:0`.\"],\n      [\"O nome engana\", \"Na Troca, os rushes da família '⚠️WIP⚠️' são a produção. Antes de chamar algo de legado ou de teste, olhe os logs de execução.\"],\n      [\"Siga a cadeia inteira\", \"Rodapés compartilhados (`goTo_onClick` e `goBack_onClick`), UC que chama UC, rush, store antigo e mensagens do PowerCord. Muita regra mora fora da tela que parece óbvia.\"],\n      [\"find_references é cego para UC\", \"Ele não enxerga referências feitas dentro da definition de um user component. Para achar quem usa algo, baixe o JSON e procure a reference.\"],\n      [\"Rush pode ignorar o body\", \"Vários rushes leem `$sessionData` (tabela '📅Server - API Responses'), carregada no início, e não o `$body`. Campo novo que só vem no body tem de ser lido como `$body.X`.\"],\n      [\"Confirme executando\", \"Quando der, rode o rush (`execute_rush_request`) ou leia os logs antes de afirmar.\"],\n      [\"Contradição: reverifique\", \"Se o que a pessoa descreve contradiz o que você leu, suspeite primeiro da sua leitura.\"]\n    ]\n  },\n  {\n    id: 'editar',\n    titulo: 'Editando sem quebrar',\n    sub: 'O system é grande e compartilhado. Toda alteração precisa poder ser revisada e desfeita.',\n    itens: [\n      [\"systemId em toda chamada\", \"O foco do MCP é compartilhado e muda sem aviso quando outra sessão aponta para outro system. Passe `systemId` (`6671e96b001009d97f222f73`) em toda leitura e escrita.\"],\n      [\"Backup antes, auditoria depois\", \"Antes de alterar, salve o JSON do componente. Registre o caminho de cada alteração, para quem revisar conseguir desfazer.\"],\n      [\"Edição cirúrgica\", \"Em handler com cards desligados, use `update_records` com dot-path no dataset do componente (`ComponentInstancesDataSet`, `DataComponentInstancesDataSet`, `ServiceComponentInstancesDataSet` ou `UserComponentsDataSet`), com `dryRun` primeiro. `patch_block_interaction`, `patch_rush_handler` e `set_block_interaction` regeneram tudo a partir do handlerCode e religam o que estava comentado.\"],\n      [\"Cuidado com set_params\", \"Ele corrompe o CD: reference terminada em '-' vira subtração, `@Mod.metodo()` vira `args:{}` (o runtime pula a interação) e `title` vira objeto e derruba o canvas do workspace.\"],\n      [\"Nunca invente reference\", \"Crie sem `reference` e deixe a plataforma gerar. Atualize no lugar, nunca recrie: reference repetida gera duplicata com resolução aleatória.\"],\n      [\"Antes de apagar, olhe o canvas\", \"Instância órfã num workspace derruba o canvas inteiro. Confira com `list_workspace_instances`.\"],\n      [\"HML e PRD dividem peças\", \"Rodapés, componentes e o router do portal de agentes são compartilhados. Antes de mexer 'só em HML', confira quem mais usa.\"],\n      [\"Revisão só leitura sem aggregate_dataset\", \"Ele grava o pipeline no dataset e gera commit em seu nome. Use `query_dataset` com `fields`.\"]\n    ]\n  },\n  {\n    id: 'd2lang',\n    titulo: 'Armadilhas de d2lang e rush',\n    sub: 'Erros que passam na validação e só aparecem em execução.',\n    itens: [\n      [\"match sem casar é verdadeiro\", \"`match()` sem resultado devolve `[]`, que é verdadeiro. Teste com `count(match(...)) > 0`.\"],\n      [\"if sem else\", \"Devolve o `prevResult` de fora, e `else { null }` some na gravação. Escreva sempre um else com valor explícito (`false`, `''` ou `[]`).\"],\n      [\"return null\", \"Vira `then: null` e não sai. Use `data2-core-flow@Return`.\"],\n      [\"Erro de Request aborta no get\", \"`x = (Request(...) || {})` segura o erro, mas `get($x, '0')` aborta o fluxo inteiro. Leia `$x.0.campo` direto.\"],\n      [\"deleteRecord precisa de objeto\", \"`deleteRecord($id)` não apaga e responde ok. Use `deleteRecord({_id: $id})`.\"],\n      [\"execute_d2lang é sandbox\", \"Ele não tem o `prevResult` de fora. Lógica nova vai num rush de teste em HML, com dados reais.\"],\n      [\"Conta dentro de template vira texto\", \"`${$total + 1}` dentro de um template literal concatena (`01`). Faça a conta num passo antes e use só a variável no template.\"],\n      [\"else { null } some\", \"Na gravação, `else { null }` desaparece e o `if` passa a devolver o `prevResult` de fora. Use `\\\"\\\"`, `false` ou `[]` no else.\"],\n      [\"Objeto em lambda precisa de parênteses\", \"`map($x, (t) => {a: 1})` quebra o rush inteiro (424, sem log). Escreva `(t) => ({a: 1})`.\"],\n      [\"patch_user_component come cifrão\", \"No texto novo, `$$` vira `$`. Para gravar `$$`, escreva `$$$$`.\"]\n    ]\n  },\n  {\n    id: 'code',\n    titulo: 'Code components',\n    sub: 'Tudo do kanban é code component. Eles falham calados, então o cuidado vem antes.',\n    itens: [\n      [\"Constantes antes do export default\", \"`export default` age como return: `const` e `let` declarados depois dele nunca inicializam.\"],\n      [\"Todo módulo por param declarado\", \"Store, dataset, rush e UC filho chegam por `parameters` e são ligados na instância. Reference montada no código resolve às vezes e falha calada.\"],\n      [\"Store pelo d2.field\", \"Leia e grave com `d2.field(d2.params.store, 'data.x')`.\"],\n      [\"Atualização imutável\", \"Filho de `d2.render` só re-renderiza quando recebe objeto novo (`{...obj, campo}`).\"],\n      [\"Onde renderiza\", \"No builder `.data2.qa` e no player `.next.data2.app`. No player de produção `.data2.app` aparece um quadro preto, o que não importa para uso interno.\"]\n    ]\n  },\n  {\n    id: 'publish',\n    titulo: 'Publish, versão e segurança',\n    sub: 'Segurança é rotina em qualquer system, não um passo extra.',\n    itens: [\n      [\"A versão do app controla o que sobe\", \"O publish leva o system inteiro; o que chega ao usuário é a versão do app. Registre no card a `app_version` da entrega.\"],\n      [\"Gravado não é publicado\", \"Escrita em bloco, rush ou store volta `_publish_required` e só vale depois do publish.\"],\n      [\"Publish só com OK\", \"Nenhum Claude publica sem a pessoa dona do card pedir.\"],\n      [\"Tabela nova nasce com policy\", \"Login do board OU Rush OU Builder. Sem login, a leitura dá 401. Policy de rota é só redirect de UX.\"],\n      [\"Rush fura a policy\", \"Dentro de rush, `find` e `saveRecord` ignoram a policy da tabela. Nenhum listener público devolve dado de tabela fechada.\"],\n      [\"security_scan depois de criar\", \"Rode depois de criar tabela, rush ou app, e confira se nada novo aparece aberto.\"],\n      [\"Tabela de senhas é trancada\", \"A tabela usada como verifier de um login fica visível só para a própria pessoa logada; builder e rush leem vazio e não conseguem alterar registro existente. Dados de perfil (papel, nome) vão numa tabela à parte.\"],\n      [\"Usuário logado edita o próprio registro\", \"Sem policy de negação, a pessoa logada consegue mudar o próprio papel. Na tabela de senhas, negue tudo que não for find e get.\"],\n      [\"Saber se houve publish\", \"Consulte o `BundlesDataSet`. Rush chamado por invoke herda o bundle de quem chamou.\"]\n    ]\n  },\n  {\n    id: 'escrita',\n    titulo: 'Escrita e dados',\n    sub: 'Para que o texto de um sirva para o outro.',\n    itens: [\n      [\"Reference completa e nome\", \"Nunca abrevie (`u1qws…` não serve para achar nada). Escreva a reference inteira e o nome do componente.\"],\n      [\"Fuso -03:00\", \"Filtro de data com `dateFormat(x, 'YYYY-MM-DDT00:00:00-03:00')`. Sem fuso, o servidor lê UTC e o dia escolhido começa às 21h do dia anterior.\"],\n      [\"Data que importa vai num campo seu\", \"O `updatedAt` é da plataforma e muda em qualquer atualização em lote.\"]\n    ]\n  }\n]\n\nconst COMBINADO = [\n  \"## Kanban do time (br-cf)\",\n  \"O kanban fica no system natura-br-cf (6671e96b001009d97f222f73), workspace \\\"kanban mcp\\\". Toda chamada leva systemId explícito.\",\n  \"\",\n  \"Ler (query_dataset, só leitura):\",\n  \"- Tarefas: xGWFJ6YYPw7_18GmH_ypIDataSet\",\n  \"- Demandas: hCvHde1zOzxLcT1GWZJrGDataSet\",\n  \"- Eventos: IY33jMLn6x8R1eF8xlzlHDataSet\",\n  \"- Catálogo (frentes, papéis, pessoas): r4oehJcseoCIPn_fYVWg3DataSet\",\n  \"\",\n  \"Gravar: só pelos rushes, com execute_rush_request (POST). Todo corpo leva actor: { person: \\\"<seu nome no catálogo>\\\", agent: \\\"<seu papel>\\\" }.\",\n  \"- kanban_minha_fila (le1ED3ML02nCGBCq1jhRP): { person, agent? }. Primeiro passo de toda sessão.\",\n  \"- kanban_demanda_salvar (l2x8cq1Ba9JV5fzjlgiUN): cria ou atualiza a demanda; key do JIRA ou vazio para CF-N.\",\n  \"- kanban_tarefa_criar (d8UkM-ZJOPWOWkZNx5Pry): uma tarefa por vez.\",\n  \"- kanban_tarefa_atualizar (7ukxnQTL4Yjr3LSixfEWk): campos que não são status.\",\n  \"- kanban_tarefa_pegar (VKOHGMiGy7rRhU6FDsFvZ): { card_id, session }. Avisa colisão.\",\n  \"- kanban_tarefa_mover (5Si_x8rE-PiUh-IdlwBMz): { card_id, to, result?, approver?, blocked_reason?, app_version?, reason? }.\",\n  \"- kanban_mudanca (f3RXndAJjSpfYOFifjTkK): { card_id, reference, name, kind, what, backup }.\",\n  \"- kanban_evento (cy2BrvIvCa89mk9J_nqkR): { type, card_id ou demand_key, to?, text }.\",\n  \"- kanban_evento_resolver (_B1ZiptjVMKVQK3dNcSK7): { event_id, answer? }.\",\n  \"\",\n  \"Regras:\",\n  \"- Leia o seu papel no catálogo (kind papel) e siga as instructions.\",\n  \"- O nome da sessão começa com o card_id.\",\n  \"- Decisão pendente vira question e nunca é tratada como regra.\",\n  \"- Pedido para outra pessoa: kanban_evento com type request e to = a pessoa.\",\n  \"- Nenhum agente publica: quem publica é a pessoa dona do card.\",\n  \"- Se o rush responder ok:false, leia errors e corrija. Não tente gravar direto na tabela.\",\n  \"- Siga este artboard de boas práticas e o modelo em \\\"Modelo do kanban - br-cf\\\" (Gd64zCEsfF59hnpZ0Aes4).\"\n].join('\\n')\n\nexport default {\n  css: `\n    :host{display:block;height:100%}\n    .pg{height:100%;overflow:auto;background:#f6f5f2;color:#1d1d1b;font:15px/1.55 system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;box-sizing:border-box}\n    .top{padding:40px 48px 24px;border-bottom:1px solid #e4e1da;background:#fff}\n    .eyebrow{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#a0522d;font-weight:600}\n    h1{margin:6px 0 8px;font-size:30px;line-height:1.2}\n    .lead{margin:0;max-width:760px;color:#55534e}\n    .meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}\n    .chip{font-size:12px;padding:4px 10px;border-radius:999px;background:#f1eee8;color:#55534e}\n    .chip.warn{background:#fdf0e3;color:#9a4a12}\n    .corpo{display:grid;grid-template-columns:240px minmax(0,1fr);gap:32px;padding:28px 48px 64px;max-width:1320px;box-sizing:border-box}\n    .lado{position:sticky;top:20px;align-self:start;display:flex;flex-direction:column;gap:4px}\n    .busca{width:100%;box-sizing:border-box;padding:9px 12px;border:1px solid #d9d5cc;border-radius:8px;font:inherit;font-size:14px;background:#fff;margin-bottom:10px;color:#1d1d1b}\n    .nav-it{display:flex;justify-content:space-between;align-items:center;gap:8px;text-align:left;padding:8px 10px;border:0;border-radius:6px;background:transparent;font:inherit;font-size:14px;color:#3b3a36;cursor:pointer}\n    .nav-it:hover{background:#ece8e0}\n    .nav-it span{font-size:12px;color:#8b8880}\n    .sec{background:#fff;border:1px solid #e4e1da;border-radius:12px;padding:24px 28px;margin-bottom:20px;scroll-margin-top:16px}\n    .sec-h{display:flex;gap:14px;align-items:flex-start;margin-bottom:16px}\n    .num{flex:none;width:28px;height:28px;border-radius:50%;background:#1d1d1b;color:#fff;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;margin-top:1px}\n    h2{margin:0;font-size:19px;line-height:1.3}\n    .sec-h p{margin:2px 0 0;color:#6b6862;font-size:14px}\n    .itens{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px}\n    .it{border:1px solid #ece9e2;border-radius:10px;padding:14px 16px;background:#fbfaf8}\n    .it h3{margin:0 0 4px;font-size:15px;line-height:1.35}\n    .it p{margin:0;font-size:14px;color:#45433e}\n    code{font:12.5px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;background:#efece5;padding:1px 5px;border-radius:4px;word-break:break-word}\n    .comb{margin-top:20px;border:1px solid #1d1d1b;border-radius:10px;overflow:hidden}\n    .comb-h{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 16px;background:#1d1d1b;color:#fff}\n    .comb-h span{display:block;font-size:13px;color:#c9c6bf}\n    .copiar{flex:none;padding:7px 14px;border:0;border-radius:6px;background:#fff;color:#1d1d1b;font:inherit;font-size:13px;font-weight:600;cursor:pointer}\n    pre{margin:0;padding:16px;background:#fbfaf8;font:12.5px/1.6 ui-monospace,Consolas,monospace;white-space:pre-wrap;word-break:break-word}\n    .vazio{color:#6b6862;margin:0}\n    @media (max-width:860px){.corpo{grid-template-columns:1fr;padding:20px 16px}.lado{position:static}.top{padding:28px 16px 20px}}\n  `,\n\n  render(d2) {\n    const q = String(d2.state.q || '').trim().toLowerCase()\n    const visiveis = SECOES.map((s, n) => ({\n      s, n,\n      itens: q ? s.itens.filter(i => (i[0] + ' ' + i[1]).toLowerCase().indexOf(q) >= 0) : s.itens\n    })).filter(x => x.itens.length)\n    const total = SECOES.reduce((acc, s) => acc + s.itens.length, 0)\n\n    const nav = SECOES.map(s =>\n      `<button class=\"nav-it\" data-key=\"nav-${s.id}\" data-id=\"${s.id}\">${esc(s.titulo)}<span>${s.itens.length}</span></button>`\n    ).join('')\n\n    const combinado = `<div class=\"comb\">\n        <div class=\"comb-h\">\n          <div><b>Combinado para colar no Claude</b><span>Vai no CLAUDE.md ou na memória do projeto de cada dev.</span></div>\n          <button class=\"copiar\">${d2.state.copiado ? 'Copiado' : 'Copiar'}</button>\n        </div>\n        <pre>${esc(COMBINADO)}</pre>\n      </div>`\n\n    const corpo = visiveis.length ? visiveis.map(x => `\n      <section class=\"sec\" id=\"sec-${x.s.id}\" data-key=\"sec-${x.s.id}\">\n        <header class=\"sec-h\">\n          <span class=\"num\">${x.n + 1}</span>\n          <div><h2>${esc(x.s.titulo)}</h2><p>${esc(x.s.sub)}</p></div>\n        </header>\n        <ol class=\"itens\">${x.itens.map((i, k) => `\n          <li class=\"it\" data-key=\"${x.s.id}-${k}\"><h3>${fmt(i[0])}</h3><p>${fmt(i[1])}</p></li>`).join('')}\n        </ol>\n        ${x.s.id === 'kanban' && !q ? combinado : ''}\n      </section>`).join('')\n      : `<p class=\"vazio\">Nada encontrado para \"${esc(q)}\".</p>`\n\n    return `<div class=\"pg\">\n      <header class=\"top\">\n        <div class=\"eyebrow\">kanban mcp · natura-br-cf</div>\n        <h1>Boas práticas do MCP no br-cf</h1>\n        <p class=\"lead\">O que todo Claude do time segue ao trabalhar no natura-br-cf: como gravar os cards do kanban, como ler o system antes de afirmar, e como editar e publicar sem quebrar o trabalho dos outros.</p>\n        <div class=\"meta\">\n          <span class=\"chip\">${total} práticas</span>\n          <span class=\"chip\">Atualizado em 09/11/2026</span>\n          <span class=\"chip warn\">Modelo e API: artboard Modelo do kanban - br-cf</span>\n        </div>\n      </header>\n      <div class=\"corpo\">\n        <nav class=\"lado\">\n          <input class=\"busca\" type=\"search\" placeholder=\"Buscar prática…\" value=\"${esc(d2.state.q || '')}\" />\n          ${nav}\n        </nav>\n        <main>${corpo}</main>\n      </div>\n    </div>`\n  },\n\n  mounted(d2) {\n    d2.on('.busca', 'input', (e, el) => { d2.state.q = el.value })\n    d2.on('.nav-it', 'click', (e, el) => {\n      const alvo = d2.$('#sec-' + el.dataset.id)\n      if (alvo) alvo.scrollIntoView({ behavior: 'smooth', block: 'start' })\n    })\n    d2.on('.copiar', 'click', async () => {\n      try {\n        await navigator.clipboard.writeText(COMBINADO)\n        d2.state.copiado = true\n        setTimeout(() => { d2.state.copiado = false }, 2000)\n      } catch (err) {\n        const pre = d2.$('.comb pre')\n        if (pre) {\n          const r = document.createRange()\n          r.selectNodeContents(pre)\n          const sel = window.getSelection()\n          sel.removeAllRanges()\n          sel.addRange(r)\n        }\n      }\n    })\n  }\n}\n\nfunction esc(v) {\n  return String(v == null ? '' : v)\n    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n\nfunction fmt(s) {\n  return esc(s).replace(/`([^`]+)`/g, '<code>$1</code>')\n}\n"}}}}}}}},{"_id":"6af22bb6a804ce16a6f59b9b","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"kanban_mcp_modelo","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const SYSTEM_ID = '6671e96b001009d97f222f73'\n\nconst TABELAS = [\n  { nome: '📋 Kanban - Catálogo', ds: 'r4oehJcseoCIPn_fYVWg3DataSet', papel: 'O que vale no kanban', desc: 'Frentes (prefixos dos cards), papéis de agente com instruções e pessoas. Os rushes validam tudo contra esta tabela. O builder pode editar direto.', campos: 'kind (frente | papel | pessoa), key, name, description, instructions, active, order' },\n  { nome: '📋 Kanban - Demandas', ds: 'hCvHde1zOzxLcT1GWZJrGDataSet', papel: 'O ticket do JIRA', desc: 'Uma por ticket (JCFA-1234) ou demanda interna (CF-N). É a fonte da verdade: se uma doc de agente divergir do ticket, vale o ticket.', campos: 'key, title, url, jira_text, summary, acceptance[], prefix, owner, requester, status, priority, due, datas e autoria' },\n  { nome: '📋 Kanban - Tarefas', ds: 'xGWFJ6YYPw7_18GmH_ypIDataSet', papel: 'Os nós da rede', desc: 'Cada tarefa tem dona, papel de agente, critério de pronto, dependências e o que vai mexer. Tudo que outro agente precisa ler fica aqui, não em arquivo local.', campos: 'card_id, prefix, number, demand_key, title, type, owner, agent, session, approver, description, acceptance[], plan, depends_on[], touches[], status, status_changed_at, blocked_reason, priority, due, result, changes[], links[], env, app_version, datas e autoria' },\n  { nome: '📋 Kanban - Usuários', ds: 'tRlbJferL7uuiK4XRB90rDataSet', papel: 'Login do time (senhas)', desc: 'Tabela que a estratégia \"📋 Kanban - Login do time\" (Eg1eRV26seZ-YdxhhvYln) usa para conferir a senha. A plataforma tranca: cada pessoa logada só vê o próprio registro e não consegue alterá-lo; nem builder nem rush leem. Só o kanban_usuario_salvar cria acessos.', campos: 'email, password (guardado pela plataforma), created_at' },\n  { nome: '📋 Kanban - Perfis', ds: 'hMctQ1zQRf75g-sjb9JMLDataSet', papel: 'Quem é quem no board', desc: 'Liga o e-mail do login à pessoa do catálogo e ao papel no board (admin, editor, leitor, bloqueado). Os rushes descobrem quem está chamando por aqui; só eles gravam.', campos: 'email, person, role, must_change_password, datas e autoria' },\n  { nome: '📋 Kanban - Eventos', ds: 'IY33jMLn6x8R1eF8xlzlHDataSet', papel: 'Linha do tempo e caixa de entrada', desc: 'Cada nota, pedido, pergunta, decisão, mudança de status e alteração é um registro novo. Ninguém apaga a nota de ninguém.', campos: 'card_id, demand_key, type, from_person, from_agent, to, text, data, resolved, resolved_by, resolved_at, created_at' }\n]\n\nconst STATUS = [\n  ['backlog', 'Registrada, ainda não refinada', '—', 'ready, cancelled'],\n  ['ready', 'Aprovada pelo orquestrador; pode ser pega', 'acceptance e agent', 'backlog, blocked, cancelled (começar é pelo kanban_tarefa_pegar)'],\n  ['in_progress', 'Uma sessão está trabalhando', 'dependências em done e session', 'blocked, in_validation, ready, cancelled'],\n  ['blocked', 'Depende de algo de fora', 'blocked_reason', 'in_progress, ready, cancelled'],\n  ['in_validation', 'Entregue, esperando aprovação', 'result e approver', 'in_progress, awaiting_deploy, done, cancelled'],\n  ['awaiting_deploy', 'Aprovada, falta publicar', 'changes registradas', 'done, in_progress, cancelled'],\n  ['done', 'Aprovada ou publicada', 'app_version, se veio do deploy', '— (precisa de ajuste? abra um bug)'],\n  ['cancelled', 'Descartada, nunca apagada', 'reason', 'backlog']\n]\n\nconst RUSHES = [\n  ['kanban_minha_fila', 'le1ED3ML02nCGBCq1jhRP', '{ person, agent? }', 'Primeiro passo de toda sessão. Devolve caixa de entrada, em andamento, prontas para pegar (dependências já feitas), esperando dependência, bloqueadas, em validação, para aprovar e aguardando deploy.'],\n  ['kanban_demanda_salvar', 'l2x8cq1Ba9JV5fzjlgiUN', '{ actor, key?, title, owner, prefix, jira_text, summary, acceptance, status? … }', 'Cria ou atualiza pela key. Sem key, gera CF-N. Valida pessoa, frente e status e grava o evento.'],\n  ['kanban_tarefa_criar', 'd8UkM-ZJOPWOWkZNx5Pry', '{ actor, prefix, demand_key, type, title, agent, acceptance, depends_on, touches, approver, status? … }', 'Gera o card_id (maior número da frente + 1). Crie uma por vez. Nasce em backlog, ou ready se já tiver acceptance e agent.'],\n  ['kanban_tarefa_atualizar', '7ukxnQTL4Yjr3LSixfEWk', '{ actor, card_id, campos…, note? }', 'Tudo menos status: description, acceptance, plan, depends_on, touches, agent, approver, result, links, session… Só a pessoa dona.'],\n  ['kanban_tarefa_pegar', 'VKOHGMiGy7rRhU6FDsFvZ', '{ actor, card_id, session }', 'ready → in_progress, só se as dependências estiverem em done. Devolve aviso de colisão com tarefas em andamento que mexem nas mesmas references.'],\n  ['kanban_tarefa_mover', '5Si_x8rE-PiUh-IdlwBMz', '{ actor, card_id, to, result?, approver?, blocked_reason?, app_version?, reason? }', 'Valida a transição e o que cada status exige. Move quem é dona ou quem aprova.'],\n  ['kanban_mudanca', 'f3RXndAJjSpfYOFifjTkK', '{ actor, card_id, reference, name, kind, what, backup }', 'Registra cada alteração feita no system (com a tarefa em in_progress) e acrescenta a reference em touches.'],\n  ['kanban_evento', 'cy2BrvIvCa89mk9J_nqkR', '{ actor, type, card_id ou demand_key, to?, text, data? }', 'note, request, answer, question ou decision. request e question exigem to (uma pessoa do catálogo) e entram na caixa de entrada dela.'],\n  ['kanban_evento_resolver', '_B1ZiptjVMKVQK3dNcSK7', '{ actor, event_id, answer? }', 'Fecha request ou question. question exige answer, que vira um evento decision: a regra decidida.'],\n  ['kanban_quadro', '4gnGRN5tFeuBicawv04U9', '{ }', 'Do board: tarefas, demandas, catálogo e pedidos abertos numa chamada.'],\n  ['kanban_card', 'MwJgzXAkBEBWRFfpwvcF7', '{ card_id }', 'Do board: o card, a linha do tempo, a demanda, as dependências e quem depende dele.'],\n  ['kanban_eu', 'gHlaiRCi-O7vR0uLOCT6w', '{ }', 'Do board: o perfil de quem está logado e, para admin, a equipe.'],\n  ['kanban_senha_ok', 'RKSAmkvOcbm9Kcx9Hriiw', '{ }', 'Do board: marca que a pessoa já trocou a senha temporária.'],\n  ['kanban_usuario_salvar', '06HyhvqG4jXD6YoDn68Fw', '{ actor, email, person, role, temp_password? }', 'Só admin (ou builder): cadastra acesso com senha temporária ou muda o papel. Não redefine senha de quem já tem acesso.']\n]\n\nconst ROTINA = [\n  ['Ler o papel', 'No catálogo, o registro kind papel com a sua key. Siga as instructions.'],\n  ['Abrir a fila', 'kanban_minha_fila com o seu nome. Responda primeiro a caixa de entrada.'],\n  ['Pegar uma tarefa', 'kanban_tarefa_pegar com o nome da sessão começando pelo card_id. Leia o card, os eventos dele, a demanda e as dependências.'],\n  ['Trabalhar', 'Backup antes, kanban_mudanca a cada alteração. Precisa de outra pessoa? kanban_evento request para ela. Dúvida de regra? question, nunca suposição.'],\n  ['Entregar', 'kanban_tarefa_mover para in_validation com result e approver.']\n]\n\nconst SEGURANCA = [\n  ['Tarefas, Demandas e Eventos', 'O builder só lê (find e get). Gravar é só pelos rushes. save_records e update_records direto dão 401.'],\n  ['Catálogo', 'Builder lê e edita; rushes leem.'],\n  ['DataSources das 4 tabelas', 'Builder ou rush deste system.'],\n  ['Usuários e Perfis', 'Builder ou rush; Usuários também aceita a pessoa logada, mas só para ler o próprio registro.'],\n  ['Os 14 rushes', 'Builder (o MCP de cada dev) ou pessoa logada no board. Quando a chamada vem do board, a pessoa é a da sessão, não a do corpo; leitor e bloqueado não gravam.']\n]\n\nconst COMBINADO = [\n  '## Kanban do time (br-cf)',\n  'O kanban fica no system natura-br-cf (6671e96b001009d97f222f73), workspace \"kanban mcp\". Toda chamada leva systemId explícito.',\n  '',\n  'Ler (query_dataset, só leitura):',\n  '- Tarefas: xGWFJ6YYPw7_18GmH_ypIDataSet',\n  '- Demandas: hCvHde1zOzxLcT1GWZJrGDataSet',\n  '- Eventos: IY33jMLn6x8R1eF8xlzlHDataSet',\n  '- Catálogo (frentes, papéis, pessoas): r4oehJcseoCIPn_fYVWg3DataSet',\n  '',\n  'Gravar: só pelos rushes, com execute_rush_request (POST). Todo corpo leva actor: { person: \"<seu nome no catálogo>\", agent: \"<seu papel>\" }.',\n  '- kanban_minha_fila (le1ED3ML02nCGBCq1jhRP): { person, agent? }. Primeiro passo de toda sessão.',\n  '- kanban_demanda_salvar (l2x8cq1Ba9JV5fzjlgiUN): cria ou atualiza a demanda; key do JIRA ou vazio para CF-N.',\n  '- kanban_tarefa_criar (d8UkM-ZJOPWOWkZNx5Pry): uma tarefa por vez.',\n  '- kanban_tarefa_atualizar (7ukxnQTL4Yjr3LSixfEWk): campos que não são status.',\n  '- kanban_tarefa_pegar (VKOHGMiGy7rRhU6FDsFvZ): { card_id, session }. Avisa colisão.',\n  '- kanban_tarefa_mover (5Si_x8rE-PiUh-IdlwBMz): { card_id, to, result?, approver?, blocked_reason?, app_version?, reason? }.',\n  '- kanban_mudanca (f3RXndAJjSpfYOFifjTkK): { card_id, reference, name, kind, what, backup }.',\n  '- kanban_evento (cy2BrvIvCa89mk9J_nqkR): { type, card_id ou demand_key, to?, text }.',\n  '- kanban_evento_resolver (_B1ZiptjVMKVQK3dNcSK7): { event_id, answer? }.',\n  '',\n  'Regras:',\n  '- Leia o seu papel no catálogo (kind papel) e siga as instructions.',\n  '- O nome da sessão começa com o card_id.',\n  '- Decisão pendente vira question e nunca é tratada como regra.',\n  '- Pedido para outra pessoa: kanban_evento com type request e to = a pessoa.',\n  '- Nenhum agente publica: quem publica é a pessoa dona do card.',\n  '- Se o rush responder ok:false, leia errors e corrija. Não tente gravar direto na tabela.',\n  '- Siga as boas práticas do artboard \"Boas práticas MCP - br-cf\" (yWnJG2UzMJ-OZYGGhTGrp) e o modelo em \"Modelo do kanban - br-cf\" (Gd64zCEsfF59hnpZ0Aes4).'\n].join('\\n')\n\nexport default {\n  css: `\n    :host{display:block;height:100%}\n    .pg{height:100%;overflow:auto;background:#f6f5f2;color:#1d1d1b;font:15px/1.55 system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;box-sizing:border-box}\n    .top{padding:40px 48px 24px;border-bottom:1px solid #e4e1da;background:#fff}\n    .eyebrow{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#a0522d;font-weight:600}\n    h1{margin:6px 0 8px;font-size:30px;line-height:1.2}\n    .lead{margin:0;max-width:820px;color:#55534e}\n    .meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}\n    .chip{font-size:12px;padding:4px 10px;border-radius:999px;background:#f1eee8;color:#55534e}\n    .corpo{padding:28px 48px 64px;max-width:1320px;box-sizing:border-box}\n    .sec{background:#fff;border:1px solid #e4e1da;border-radius:12px;padding:24px 28px;margin-bottom:20px}\n    .sec-h{display:flex;gap:14px;align-items:flex-start;margin-bottom:16px}\n    .num{flex:none;width:28px;height:28px;border-radius:50%;background:#1d1d1b;color:#fff;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;margin-top:1px}\n    h2{margin:0;font-size:19px;line-height:1.3}\n    .sec-h p{margin:2px 0 0;color:#6b6862;font-size:14px}\n    .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}\n    .card{border:1px solid #ece9e2;border-radius:10px;padding:14px 16px;background:#fbfaf8}\n    .card h3{margin:0 0 2px;font-size:15px}\n    .tag{font-size:12px;color:#a0522d;font-weight:600;margin-bottom:6px}\n    .card p{margin:0 0 8px;font-size:14px;color:#45433e}\n    .campos{font-size:12.5px;color:#6b6862}\n    table{width:100%;border-collapse:collapse;font-size:14px}\n    th{text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:#8b8880;font-weight:600;padding:8px 10px;border-bottom:1px solid #e4e1da}\n    td{padding:10px;border-bottom:1px solid #f0ede7;vertical-align:top;color:#3b3a36}\n    tr:last-child td{border-bottom:0}\n    .tw{overflow-x:auto}\n    code{font:12.5px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;background:#efece5;padding:1px 5px;border-radius:4px;word-break:break-word}\n    ol.passos{margin:0;padding:0;list-style:none;display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;counter-reset:p}\n    ol.passos li{counter-increment:p;border:1px solid #ece9e2;border-radius:10px;padding:14px 16px;background:#fbfaf8}\n    ol.passos li b{display:block;margin-bottom:4px}\n    ol.passos li b::before{content:counter(p) '. ';color:#a0522d}\n    ol.passos li span{font-size:14px;color:#45433e}\n    .comb{border:1px solid #1d1d1b;border-radius:10px;overflow:hidden}\n    .comb-h{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 16px;background:#1d1d1b;color:#fff}\n    .comb-h span{display:block;font-size:13px;color:#c9c6bf}\n    .copiar{flex:none;padding:7px 14px;border:0;border-radius:6px;background:#fff;color:#1d1d1b;font:inherit;font-size:13px;font-weight:600;cursor:pointer}\n    pre{margin:0;padding:16px;background:#fbfaf8;font:12.5px/1.6 ui-monospace,Consolas,monospace;white-space:pre-wrap;word-break:break-word}\n    @media (max-width:860px){.corpo{padding:20px 16px}.top{padding:28px 16px 20px}}\n  `,\n\n  render(d2) {\n    const secao = (n, titulo, sub, corpo, key) => `\n      <section class=\"sec\" data-key=\"${key}\">\n        <header class=\"sec-h\"><span class=\"num\">${n}</span><div><h2>${esc(titulo)}</h2><p>${esc(sub)}</p></div></header>\n        ${corpo}\n      </section>`\n\n    const tabelas = `<div class=\"grid\">${TABELAS.map(t => `\n      <div class=\"card\" data-key=\"t-${t.ds}\">\n        <div class=\"tag\">${esc(t.papel)}</div>\n        <h3>${esc(t.nome)}</h3>\n        <p>${esc(t.desc)}</p>\n        <p><code>${esc(t.ds)}</code></p>\n        <div class=\"campos\">${esc(t.campos)}</div>\n      </div>`).join('')}</div>`\n\n    const status = `<div class=\"tw\"><table>\n      <thead><tr><th>Status</th><th>Quando</th><th>Para entrar exige</th><th>Pode ir para</th></tr></thead>\n      <tbody>${STATUS.map(s => `<tr data-key=\"s-${s[0]}\"><td><code>${s[0]}</code></td><td>${esc(s[1])}</td><td>${esc(s[2])}</td><td>${esc(s[3])}</td></tr>`).join('')}</tbody>\n    </table></div>`\n\n    const rushes = `<div class=\"tw\"><table>\n      <thead><tr><th>Rush</th><th>Corpo</th><th>O que garante</th></tr></thead>\n      <tbody>${RUSHES.map(r => `<tr data-key=\"r-${r[1]}\"><td><b>${esc(r[0])}</b><br><code>${esc(r[1])}</code></td><td><code>${esc(r[2])}</code></td><td>${esc(r[3])}</td></tr>`).join('')}</tbody>\n    </table></div>`\n\n    const rotina = `<ol class=\"passos\">${ROTINA.map((p, i) => `<li data-key=\"p-${i}\"><b>${esc(p[0])}</b><span>${esc(p[1])}</span></li>`).join('')}</ol>`\n\n    const seg = `<div class=\"tw\"><table>\n      <thead><tr><th>Onde</th><th>Quem passa</th></tr></thead>\n      <tbody>${SEGURANCA.map((s, i) => `<tr data-key=\"g-${i}\"><td><b>${esc(s[0])}</b></td><td>${esc(s[1])}</td></tr>`).join('')}</tbody>\n    </table></div>`\n\n    const comb = `<div class=\"comb\">\n      <div class=\"comb-h\"><div><b>Combinado para colar no Claude</b><span>Vai no CLAUDE.md ou na memória do projeto de cada dev.</span></div>\n        <button class=\"copiar\">${d2.state.copiado ? 'Copiado' : 'Copiar'}</button></div>\n      <pre>${esc(COMBINADO)}</pre>\n    </div>`\n\n    return `<div class=\"pg\">\n      <header class=\"top\">\n        <div class=\"eyebrow\">kanban mcp · natura-br-cf</div>\n        <h1>Modelo do kanban</h1>\n        <p class=\"lead\">A rede de tarefas dos agentes do time. Qualquer agente, de qualquer dev, chega a uma tarefa e consegue trabalhar só com o que está nas tabelas. Os agentes leem direto e gravam só pelos rushes, que validam tudo.</p>\n        <div class=\"meta\">\n          <span class=\"chip\">system ${SYSTEM_ID}</span>\n          <span class=\"chip\">6 tabelas · 14 rushes · app kanban-time</span>\n          <span class=\"chip\">Criado em 09/11/2026</span>\n        </div>\n      </header>\n      <main class=\"corpo\">\n        ${secao(1, 'As tabelas', 'Catálogo define o que vale; demanda é o ticket; tarefa é o nó da rede; evento é tudo que acontece.', tabelas, 'sec-tabelas')}\n        ${secao(2, 'Status da tarefa', 'O kanban_tarefa_mover recusa qualquer transição fora desta tabela.', status, 'sec-status')}\n        ${secao(3, 'A API: 9 rushes', 'Chamados pelo MCP com execute_rush_request (POST). Todo corpo que grava leva actor: { person, agent }.', rushes, 'sec-rushes')}\n        ${secao(4, 'Como uma sessão trabalha', 'A mesma rotina para qualquer papel.', rotina, 'sec-rotina')}\n        ${secao(5, 'Segurança', 'Policies aplicadas em 09/11/2026.', seg, 'sec-seg')}\n        ${secao(6, 'Combinado', 'O texto que cada dev cola no próprio Claude.', comb, 'sec-comb')}\n      </main>\n    </div>`\n  },\n\n  mounted(d2) {\n    d2.on('.copiar', 'click', async () => {\n      try {\n        await navigator.clipboard.writeText(COMBINADO)\n        d2.state.copiado = true\n        setTimeout(() => { d2.state.copiado = false }, 2000)\n      } catch (err) {\n        const pre = d2.$('.comb pre')\n        if (pre) {\n          const r = document.createRange()\n          r.selectNodeContents(pre)\n          const sel = window.getSelection()\n          sel.removeAllRanges()\n          sel.addRange(r)\n        }\n      }\n    })\n  }\n}\n\nfunction esc(v) {\n  return String(v == null ? '' : v)\n    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n"}}}}}}}},{"_id":"6af23213afec9e7dd0cf1f27","component":"data2-core-types@Component","system":"6671e96b001009d97f222f73","params":{"extends":"data2-core-primitives@Block","component":"kanban_mcp_app","package":"natura-br-cf","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"const STATUS = [\n  { k: 'backlog', n: 'Backlog', c: '#8a8f98' },\n  { k: 'ready', n: 'Pronta', c: '#4f7cff' },\n  { k: 'in_progress', n: 'Em andamento', c: '#0e9f9a' },\n  { k: 'blocked', n: 'Bloqueada', c: '#e5484d' },\n  { k: 'in_validation', n: 'Em validação', c: '#d97706' },\n  { k: 'awaiting_deploy', n: 'Aguardando deploy', c: '#9b59b6' },\n  { k: 'done', n: 'Feita', c: '#2f9e44' },\n  { k: 'cancelled', n: 'Cancelada', c: '#adb5bd' }\n]\nconst STATUS_DEMANDA = ['open', 'in_progress', 'in_validation', 'done', 'cancelled']\nconst TRANS = {\n  backlog: ['ready', 'cancelled'],\n  ready: ['backlog', 'blocked', 'cancelled'],\n  in_progress: ['blocked', 'in_validation', 'ready', 'cancelled'],\n  blocked: ['in_progress', 'ready', 'cancelled'],\n  in_validation: ['in_progress', 'awaiting_deploy', 'done', 'cancelled'],\n  awaiting_deploy: ['done', 'in_progress', 'cancelled'],\n  done: [],\n  cancelled: ['backlog']\n}\nconst TIPOS = { feature: 'Feature', spike: 'Spike', bug: 'Bug' }\nconst COR_TIPO = { feature: '#4f7cff', spike: '#0e9f9a', bug: '#e5484d' }\nconst EVENTOS = { created: 'Criada', status: 'Status', note: 'Nota', request: 'Pedido', answer: 'Resposta', question: 'Pergunta', decision: 'Decisão', change: 'Alteração', publish: 'Publicação' }\nconst ROLES = ['admin', 'editor', 'leitor', 'bloqueado']\nconst ENVS = ['DEV', 'HML', 'PRD']\n\nconst AUTOTESTE = autoteste()\n\nexport default {\n  css: `\n    :host{display:block;height:100%;--bg:#f5f4f1;--card:#fff;--line:#e4e1da;--soft:#f1efea;--tx:#1d1d1b;--tx2:#5c5a55;--tx3:#8b8880;--ac:#a0522d;--ok:#2f9e44;--bad:#e5484d}\n    *{box-sizing:border-box}\n    .root{height:100%;background:var(--bg);color:var(--tx);font:14px/1.5 system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;display:flex;flex-direction:column;overflow:hidden}\n    button{font:inherit;cursor:pointer}\n    input,select,textarea{font:inherit;color:var(--tx);background:#fff;border:1px solid #d9d5cc;border-radius:8px;padding:8px 10px;width:100%}\n    textarea{min-height:70px;resize:vertical}\n    label{display:block;font-size:12px;color:var(--tx2);margin:10px 0 4px;font-weight:600}\n    .btn{border:1px solid #d9d5cc;background:#fff;border-radius:8px;padding:7px 12px;color:var(--tx);white-space:nowrap}\n    .btn:hover{background:var(--soft)}\n    .btn.pri{background:var(--tx);border-color:var(--tx);color:#fff}\n    .btn.pri:hover{opacity:.9}\n    .btn.min{padding:4px 9px;font-size:12px}\n    .btn[disabled]{opacity:.5;cursor:default}\n    .erro{background:#fdecec;color:#9b1c1f;border-radius:8px;padding:8px 10px;margin-top:10px;font-size:13px}\n    .aviso{background:#fdf0e3;color:#8a4a12;border-radius:8px;padding:8px 10px;margin-top:10px;font-size:13px}\n    .centro{flex:1;display:flex;align-items:center;justify-content:center;padding:24px}\n    .caixa{width:min(400px,100%);background:#fff;border:1px solid var(--line);border-radius:14px;padding:28px}\n    .caixa h1{margin:0 0 4px;font-size:22px}\n    .caixa p{margin:0 0 6px;color:var(--tx2)}\n    .marca{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ac);font-weight:700}\n    .top{display:flex;align-items:center;gap:12px;padding:10px 16px;background:#fff;border-bottom:1px solid var(--line);flex-wrap:wrap}\n    .top .nome{font-weight:800;font-size:16px;margin-right:6px}\n    .tabs{display:flex;gap:4px;background:var(--soft);border-radius:10px;padding:3px}\n    .tab{border:0;background:transparent;border-radius:8px;padding:6px 12px;color:var(--tx2)}\n    .tab.on{background:#fff;color:var(--tx);box-shadow:0 1px 2px rgba(0,0,0,.08)}\n    .badge{display:inline-block;min-width:18px;padding:0 5px;border-radius:9px;background:var(--bad);color:#fff;font-size:11px;font-weight:700;margin-left:5px;text-align:center}\n    .esp{flex:1}\n    .eu{font-size:13px;color:var(--tx2)}\n    .filtros{display:flex;gap:8px;padding:10px 16px;flex-wrap:wrap;align-items:center;border-bottom:1px solid var(--line);background:#fbfaf8}\n    .filtros input[type=search]{width:240px}\n    .filtros select{width:auto;min-width:130px}\n    .chk{display:flex;gap:6px;align-items:center;font-size:13px;color:var(--tx2);margin:0}\n    .chk input{width:auto}\n    .main{flex:1;overflow:auto;min-height:0}\n    .cols{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(250px,1fr);gap:12px;padding:14px 16px;height:100%;min-height:400px}\n    .col{background:var(--soft);border-radius:12px;display:flex;flex-direction:column;min-height:0}\n    .col-h{display:flex;align-items:center;gap:8px;padding:10px 12px;font-weight:700;font-size:13px}\n    .dot{width:9px;height:9px;border-radius:50%;flex:none}\n    .col-h .n{margin-left:auto;color:var(--tx3);font-weight:600}\n    .col-b{overflow:auto;padding:0 8px 8px;display:flex;flex-direction:column;gap:8px}\n    .tile{background:#fff;border:1px solid var(--line);border-left:3px solid var(--line);border-radius:10px;padding:10px 11px;cursor:pointer;text-align:left;width:100%}\n    .tile:hover{box-shadow:0 2px 8px rgba(0,0,0,.06)}\n    .tile .l1{display:flex;gap:6px;align-items:center;font-size:12px;color:var(--tx3);font-weight:700}\n    .tile .t{font-weight:600;margin:4px 0 8px;font-size:13.5px;line-height:1.35;color:var(--tx)}\n    .tile .l3{display:flex;gap:6px;align-items:center;flex-wrap:wrap;font-size:12px;color:var(--tx2)}\n    .chip{display:inline-flex;align-items:center;gap:4px;border-radius:999px;padding:1px 8px;font-size:11.5px;font-weight:600;background:var(--soft);color:var(--tx2)}\n    .av{width:22px;height:22px;border-radius:50%;background:#e9e3d8;color:#6b4a2b;font-size:10.5px;font-weight:800;display:inline-flex;align-items:center;justify-content:center}\n    .vazio{color:var(--tx3);font-size:13px;padding:10px;text-align:center;border:1px dashed var(--line);border-radius:10px}\n    .painel{padding:16px;display:grid;gap:14px;grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}\n    .kpis{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));grid-column:1/-1}\n    .kpi{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px}\n    .kpi .v{font-size:28px;font-weight:800}\n    .kpi .r{font-size:12px;color:var(--tx3);text-transform:uppercase;letter-spacing:.04em}\n    .bloco{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 16px}\n    .bloco h3{margin:0 0 10px;font-size:14px}\n    .barra{display:grid;grid-template-columns:130px 1fr 34px;gap:8px;align-items:center;font-size:13px;margin:6px 0}\n    .barra .trilho{height:8px;background:var(--soft);border-radius:4px;overflow:hidden}\n    .barra .enche{height:100%;border-radius:4px}\n    .barra .q{text-align:right;color:var(--tx2);font-weight:600}\n    .lin{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-top:1px solid var(--soft);font-size:13px}\n    .lin:first-of-type{border-top:0}\n    .link{border:0;background:none;padding:0;color:var(--ac);font-weight:700;text-decoration:underline;cursor:pointer}\n    .scrim{position:fixed;inset:0;background:rgba(20,18,15,.45);display:flex;align-items:flex-start;justify-content:center;padding:4vh 12px;z-index:50;overflow:auto}\n    .modal{background:#fff;border-radius:14px;width:min(1080px,100%);box-shadow:0 20px 60px rgba(0,0,0,.25)}\n    .modal.peq{width:min(560px,100%)}\n    .m-h{display:flex;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--line)}\n    .m-h h2{margin:0;font-size:17px;flex:1}\n    .m-b{padding:16px 18px}\n    .duas{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:18px}\n    .grade{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px 14px;margin:8px 0 12px}\n    .grade div span{display:block;font-size:11.5px;color:var(--tx3);text-transform:uppercase;letter-spacing:.04em}\n    .sec{margin-top:14px}\n    .sec h4{margin:0 0 6px;font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--tx3)}\n    .texto{white-space:pre-wrap;word-break:break-word;background:#fbfaf8;border:1px solid var(--soft);border-radius:8px;padding:8px 10px;font-size:13px}\n    ul.l{margin:0;padding-left:18px}\n    .acoes{background:#fbfaf8;border:1px solid var(--line);border-radius:12px;padding:12px}\n    .acoes h4{margin:0 0 4px}\n    .ev{border-left:2px solid var(--line);padding:2px 0 10px 12px;position:relative}\n    .ev::before{content:'';position:absolute;left:-5px;top:6px;width:8px;height:8px;border-radius:50%;background:var(--ac)}\n    .ev .q{font-size:12px;color:var(--tx3)}\n    .ev .x{white-space:pre-wrap;word-break:break-word;font-size:13px}\n    .ev.aberto::before{background:var(--bad)}\n    .linha{display:flex;gap:8px;align-items:center;flex-wrap:wrap}\n    .toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:var(--tx);color:#fff;border-radius:10px;padding:9px 16px;z-index:60;font-size:13px}\n    table.t{width:100%;border-collapse:collapse;font-size:13px}\n    table.t th{text-align:left;font-size:11.5px;color:var(--tx3);text-transform:uppercase;padding:6px 8px;border-bottom:1px solid var(--line)}\n    table.t td{padding:8px;border-bottom:1px solid var(--soft);vertical-align:middle}\n    table.t select{width:auto}\n    .muted{color:var(--tx3);font-size:12.5px}\n    @media (max-width:860px){.duas{grid-template-columns:1fr}.filtros input[type=search]{width:100%}}\n  `,\n\n  render(d2) {\n    const S = d2.state\n    const fase = S.fase || 'carregando'\n    let corpo = ''\n    if (fase === 'carregando') corpo = `<div class=\"centro\"><div class=\"muted\">Carregando o kanban…</div></div>`\n    else if (fase === 'login') corpo = telaLogin(S)\n    else if (fase === 'trocar') corpo = telaTrocar(S, true)\n    else corpo = telaApp(S)\n    const toast = S.toast ? `<div class=\"toast\">${esc(S.toast)}</div>` : ''\n    const falhas = AUTOTESTE.length ? `<div class=\"erro\" style=\"margin:0;border-radius:0\">Autoteste: ${esc(AUTOTESTE.join(' | '))}</div>` : ''\n    return `<div class=\"root\">${falhas}${corpo}${toast}</div>`\n  },\n\n  async mounted(d2) {\n    d2.on('[data-act]', 'click', (e, el) => { e.preventDefault(); acao(d2, el.dataset.act, el) })\n    const filtro = (e, el) => {\n      const f = Object.assign({}, d2.state.f || {})\n      f[el.dataset.filtro] = el.type === 'checkbox' ? el.checked : el.value\n      d2.state.f = f\n    }\n    d2.on('[data-filtro]', 'input', filtro)\n    d2.on('[data-filtro]', 'change', filtro)\n    d2.on('select[name=mv_to]', 'change', (e, el) => { d2.state.mvTo = el.value })\n    d2.on('select[name=ev_tipo]', 'change', (e, el) => { d2.state.evTipo = el.value })\n    d2.on('input[name=lg_senha]', 'keydown', (e) => { if (e.key === 'Enter') acao(d2, 'login') })\n    d2.on(window, 'keydown', (e) => { if (e.key === 'Escape' && d2.state.modal) d2.state.modal = null })\n    if (d2.context.isBuilder && !d2.context.isPlaying) { d2.state.fase = 'login'; return }\n    await iniciar(d2)\n  }\n}\n\nfunction autoteste() {\n  const falhas = []\n  const me = { person: 'Ana Teste', role: 'admin', email: 'ana@x.com' }\n  const t1 = { card_id: 'KB-1', prefix: 'KB', number: 1, demand_key: 'CF-1', title: 'Um', type: 'feature', owner: 'Ana Teste', agent: 'DEV', approver: 'Ana Teste', status: 'in_validation', depends_on: [], touches: [{ reference: 'abc', name: 'Tabela', kind: 'table' }], changes: [{ reference: 'abc', name: 'Tabela', what: 'criada', at: '2026-11-09T10:00:00Z', by: 'Ana' }], acceptance: ['a', 'b'], description: 'x', plan: 'p', result: 'r', priority: 1, status_changed_at: '2026-11-09T10:00:00Z' }\n  const t2 = Object.assign({}, t1, { card_id: 'KB-2', number: 2, status: 'blocked', depends_on: ['KB-1'], blocked_reason: 'esperando', type: 'bug', acceptance: [] })\n  const t3 = Object.assign({}, t1, { card_id: 'KB-3', number: 3, status: 'awaiting_deploy', type: 'spike' })\n  const dados = { tasks: [t1, t2, t3], demands: [{ key: 'CF-1', title: 'Demanda', status: 'open', prefix: 'KB', owner: 'Ana Teste', url: 'https://x' }], catalog: [{ kind: 'frente', key: 'KB', name: 'Kanban' }, { kind: 'papel', key: 'DEV', name: 'Dev' }, { kind: 'pessoa', key: 'Ana Teste' }], open: [{ _id: 'e1', type: 'request', to: 'Ana Teste', from_person: 'Bia', card_id: 'KB-1', text: 'oi', created_at: '2026-11-09T10:00:00Z' }] }\n  const cardData = { ok: true, card: t1, demand: dados.demands[0], events: [{ _id: 'e1', type: 'question', to: 'Ana Teste', from_person: 'Bia', text: 'q?', resolved: false, created_at: '2026-11-09T10:00:00Z' }, { _id: 'e2', type: 'status', from_person: 'Ana', text: 'a → b', resolved: true }], depends_on: [{ card_id: 'KB-0', title: 'z', status: 'done' }], dependents: [{ card_id: 'KB-2', title: 'dois', status: 'blocked' }] }\n  const base = { me, dados, team: [{ email: 'ana@x.com', person: 'Ana Teste', role: 'admin', must_change_password: false }], f: { q: 'k', minhas: true, canceladas: true } }\n  const casos = [\n    ['login', () => telaLogin({ erroLogin: 'x' })],\n    ['trocar', () => telaTrocar({ me, erroSenha: 'x' })],\n    ['quadro', () => telaApp(Object.assign({}, base, { view: 'quadro' }))],\n    ['painel', () => telaApp(Object.assign({}, base, { view: 'painel' }))],\n    ['demandas', () => telaApp(Object.assign({}, base, { view: 'demandas' }))],\n    ['equipe', () => telaApp(Object.assign({}, base, { view: 'equipe', erroEquipe: 'x' }))],\n    ['card', () => telaApp(Object.assign({}, base, { modal: { tipo: 'card', id: 'KB-1' }, cardData, erroModal: 'x' }))],\n    ['card-mover', () => ['in_validation', 'blocked', 'done', 'cancelled', 'in_progress'].map(mv => telaApp(Object.assign({}, base, { modal: { tipo: 'card', id: 'KB-1' }, cardData: Object.assign({}, cardData, { card: Object.assign({}, t3) }), mvTo: mv, evTipo: 'request' }))).join('')],\n    ['card-editar', () => telaApp(Object.assign({}, base, { modal: { tipo: 'card', id: 'KB-1' }, cardData, editando: true }))],\n    ['card-carregando', () => telaApp(Object.assign({}, base, { modal: { tipo: 'card', id: 'KB-1' }, cardData: null }))],\n    ['card-erro', () => telaApp(Object.assign({}, base, { modal: { tipo: 'card', id: 'KB-1' }, cardData: { erro: 'x' } }))],\n    ['novoCard', () => telaApp(Object.assign({}, base, { modal: { tipo: 'novoCard', demanda: 'CF-1' } }))],\n    ['novaDemanda', () => telaApp(Object.assign({}, base, { modal: { tipo: 'novaDemanda' } }))],\n    ['senha', () => telaApp(Object.assign({}, base, { modal: { tipo: 'senha' } }))],\n    ['vazio', () => telaApp({ me: { person: 'X', role: 'leitor' }, dados: { tasks: [], demands: [], catalog: [], open: [] } })]\n  ]\n  casos.forEach(c => { try { const h = c[1](); if (!h || typeof h !== 'string') falhas.push(c[0] + ': vazio') } catch (e) { falhas.push(c[0] + ': ' + e.message) } })\n  return falhas\n}\n\nfunction esc(v) {\n  return String(v == null ? '' : v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\nfunction quando(s) {\n  if (!s) return ''\n  const d = new Date(s)\n  if (isNaN(d.getTime())) return esc(s)\n  return d.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })\n}\nfunction iniciais(n) {\n  const p = String(n || '?').trim().split(/\\s+/)\n  return esc(((p[0] || '')[0] || '') + ((p.length > 1 ? p[p.length - 1][0] : '') || '')).toUpperCase()\n}\nfunction stInfo(k) { return STATUS.find(s => s.k === k) || { k, n: k || '—', c: '#8a8f98' } }\nfunction opcoes(lista, atual, vazio) {\n  const base = vazio != null ? `<option value=\"\">${esc(vazio)}</option>` : ''\n  return base + lista.map(o => {\n    const v = typeof o === 'string' ? o : o.v\n    const t = typeof o === 'string' ? o : o.t\n    return `<option value=\"${esc(v)}\" ${String(v) === String(atual || '') ? 'selected' : ''}>${esc(t)}</option>`\n  }).join('')\n}\nfunction val(d2, nome) {\n  const el = d2.$(`[name=\"${nome}\"]`)\n  return el ? String(el.value || '').trim() : ''\n}\nfunction linhas(t) { return String(t || '').split('\\n').map(x => x.trim()).filter(Boolean) }\nfunction lista(t) { return String(t || '').split(/[,\\s]+/).map(x => x.trim().toUpperCase()).filter(Boolean) }\nfunction msgErro(err) {\n  if (!err) return 'erro desconhecido'\n  if (typeof err === 'string') return err\n  return err.message || (err.data && err.data.message) || err.error || JSON.stringify(err).slice(0, 200)\n}\nfunction catalogo(S, kind) { return ((S.dados && S.dados.catalog) || []).filter(c => c.kind === kind) }\nfunction podeGravar(S) { return S.me && ['admin', 'editor', 'builder'].includes(S.me.role) }\nfunction aviso(d2, t) {\n  d2.state.toast = t\n  setTimeout(() => { if (d2.state.toast === t) d2.state.toast = null }, 2600)\n}\n\nasync function chamar(d2, nome, body) {\n  const r = await d2.rush(d2.param(nome)).invoke(body || {})\n  if (r && typeof r === 'object' && r.data && typeof r.data === 'object' && ('ok' in r.data)) return r.data\n  return r\n}\n\nasync function iniciar(d2) {\n  const S = d2.state\n  try {\n    const eu = await chamar(d2, 'rEu', {})\n    if (eu && eu.ok) {\n      S.me = eu.me\n      S.team = eu.team || []\n      if (eu.me && eu.me.must_change_password) { S.fase = 'trocar'; return }\n      await carregar(d2)\n      S.fase = 'app'\n      return\n    }\n    if (S.tentouLogin && eu && eu.errors) S.erroLogin = eu.errors.join(' ')\n    S.fase = 'login'\n  } catch (err) {\n    S.fase = 'login'\n  }\n}\n\nasync function carregar(d2) {\n  const q = await chamar(d2, 'rQuadro', {})\n  if (!q || !q.ok) throw new Error(((q && q.errors) || ['não consegui carregar o quadro']).join(' '))\n  d2.state.dados = { tasks: q.tasks || [], demands: q.demands || [], catalog: q.catalog || [], open: q.open_events || [], at: q.generated_at }\n}\n\nasync function carregarCard(d2, id) {\n  try {\n    const c = await chamar(d2, 'rCard', { card_id: id })\n    d2.state.cardData = c && c.ok ? c : { erro: ((c && c.errors) || ['não consegui abrir o card']).join(' ') }\n  } catch (err) {\n    d2.state.cardData = { erro: msgErro(err) }\n  }\n}\n\nasync function gravar(d2, nome, body, okMsg) {\n  const S = d2.state\n  S.busy = true\n  S.erroModal = null\n  try {\n    const r = await chamar(d2, nome, body)\n    if (!r || !r.ok) { S.erroModal = ((r && r.errors) || ['a gravação falhou']).join(' • '); return null }\n    aviso(d2, okMsg)\n    await carregar(d2)\n    if (S.modal && S.modal.tipo === 'card') await carregarCard(d2, S.modal.id)\n    return r\n  } catch (err) {\n    S.erroModal = msgErro(err)\n    return null\n  } finally {\n    S.busy = false\n  }\n}\n\nfunction limpo(o) {\n  const r = {}\n  Object.keys(o).forEach(k => { const v = o[k]; if (v !== '' && v != null && !(Array.isArray(v) && v.length === 0 && k !== 'acceptance' && k !== 'depends_on')) r[k] = v })\n  return r\n}\n\nasync function acao(d2, act, el) {\n  const S = d2.state\n  if (S.busy && act !== 'fechar') return\n  const card = S.modal && S.modal.tipo === 'card' && S.cardData && S.cardData.card\n  if (act === 'login') {\n    const email = val(d2, 'lg_email'), senha = val(d2, 'lg_senha')\n    if (!email || !senha) { S.erroLogin = 'Informe e-mail e senha.'; return }\n    S.busy = true; S.erroLogin = null\n    try {\n      const r = await d2.auth(d2.param('auth')).login({ email, password: senha })\n      if (r && (r.isData2Error || r.error)) throw r\n      S.tentouLogin = true\n      await iniciar(d2)\n      if (S.fase === 'login' && !S.erroLogin) S.erroLogin = 'Entrou, mas o seu e-mail não tem perfil no kanban. Peça ao admin.'\n    } catch (err) {\n      try { console.error('[kanban] login falhou', err) } catch (e) {}\n      S.erroLogin = 'Não consegui entrar. Detalhe técnico: ' + msgErro(err)\n    } finally { S.busy = false }\n    return\n  }\n  if (act === 'trocar') {\n    const atual = val(d2, 'pw_atual'), nova = val(d2, 'pw_nova'), conf = val(d2, 'pw_conf')\n    S.erroSenha = null\n    if (nova.length < 8) { S.erroSenha = 'A senha nova precisa ter pelo menos 8 caracteres.'; return }\n    if (nova !== conf) { S.erroSenha = 'A confirmação não confere com a senha nova.'; return }\n    if (nova === atual) { S.erroSenha = 'A senha nova precisa ser diferente da atual.'; return }\n    S.busy = true\n    try {\n      const r = await d2.auth(d2.param('auth')).changePassword({ email: S.me && S.me.email, password: atual, oldPassword: atual, currentPassword: atual, newPassword: nova, newPasswordConfirmation: nova, passwordConfirmation: nova })\n      if (r && (r.isData2Error || r.error)) throw r\n      await chamar(d2, 'rSenhaOk', {})\n      if (S.me) S.me = Object.assign({}, S.me, { must_change_password: false })\n      aviso(d2, 'Senha trocada.')\n      if (S.fase === 'trocar') { await carregar(d2); S.fase = 'app' } else S.modal = null\n    } catch (err) {\n      S.erroSenha = 'Não consegui trocar a senha: ' + msgErro(err)\n    } finally { S.busy = false }\n    return\n  }\n  if (act === 'sair') {\n    try { await d2.auth(d2.param('auth')).logout() } catch (e) {}\n    d2.setState({ fase: 'login', me: null, dados: null, modal: null, cardData: null, team: [], tentouLogin: false, erroLogin: null })\n    return\n  }\n  if (act === 'view') { S.view = el.dataset.v; return }\n  if (act === 'recarregar') { S.busy = true; try { await carregar(d2); aviso(d2, 'Atualizado.') } catch (err) { aviso(d2, msgErro(err)) } finally { S.busy = false } return }\n  if (act === 'abrir') {\n    d2.setState({ modal: { tipo: 'card', id: el.dataset.id }, cardData: null, mvTo: '', editando: false, erroModal: null, evTipo: 'note' })\n    await carregarCard(d2, el.dataset.id)\n    return\n  }\n  if (act === 'fechar') { d2.setState({ modal: null, cardData: null, erroModal: null, erroSenha: null }); return }\n  if (act === 'novoCard') { d2.setState({ modal: { tipo: 'novoCard', demanda: el.dataset.demanda || '' }, erroModal: null }); return }\n  if (act === 'novaDemanda') { d2.setState({ modal: { tipo: 'novaDemanda' }, erroModal: null }); return }\n  if (act === 'senhaModal') { d2.setState({ modal: { tipo: 'senha' }, erroSenha: null }); return }\n  if (act === 'criarCard') {\n    const r = await gravar(d2, 'rCriar', limpo({\n      demand_key: val(d2, 'nc_demanda'), prefix: val(d2, 'nc_prefixo'), type: val(d2, 'nc_tipo'), title: val(d2, 'nc_titulo'),\n      agent: val(d2, 'nc_papel'), approver: val(d2, 'nc_aprova'), description: val(d2, 'nc_desc'), acceptance: linhas(val(d2, 'nc_aceite')),\n      depends_on: lista(val(d2, 'nc_deps')), status: val(d2, 'nc_status'), env: val(d2, 'nc_env'), priority: val(d2, 'nc_prio') ? Number(val(d2, 'nc_prio')) : null\n    }), 'Tarefa criada.')\n    if (r && r.card) { d2.setState({ modal: { tipo: 'card', id: r.card.card_id }, cardData: null, mvTo: '' }); await carregarCard(d2, r.card.card_id) }\n    return\n  }\n  if (act === 'criarDemanda') {\n    const r = await gravar(d2, 'rDemanda', limpo({\n      key: val(d2, 'nd_chave'), title: val(d2, 'nd_titulo'), url: val(d2, 'nd_url'), prefix: val(d2, 'nd_prefixo'), owner: val(d2, 'nd_dono'),\n      requester: val(d2, 'nd_pediu'), summary: val(d2, 'nd_resumo'), jira_text: val(d2, 'nd_jira'), acceptance: linhas(val(d2, 'nd_aceite'))\n    }), 'Demanda salva.')\n    if (r) d2.setState({ modal: null })\n    return\n  }\n  if (act === 'statusDemanda') {\n    await gravar(d2, 'rDemanda', { key: el.dataset.key, status: val(d2, 'dm_status') }, 'Status da demanda atualizado.')\n    return\n  }\n  if (act === 'eqGerar') {\n    const cs = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'\n    const arr = new Uint32Array(12)\n    crypto.getRandomValues(arr)\n    const s = Array.from(arr).map(n => cs[n % cs.length]).join('')\n    const i = d2.$('[name=\"eq_senha\"]')\n    if (i) i.value = s\n    return\n  }\n  if (act === 'eqCriar' || act === 'eqPapel') {\n    const body = act === 'eqCriar'\n      ? { email: val(d2, 'eq_email'), person: val(d2, 'eq_pessoa'), role: val(d2, 'eq_papel'), temp_password: val(d2, 'eq_senha') }\n      : { email: el.dataset.email, role: val(d2, `role_${el.dataset.i}`) }\n    S.busy = true\n    S.erroEquipe = null\n    try {\n      const r = await chamar(d2, 'rUsuario', body)\n      if (!r || !r.ok) { S.erroEquipe = ((r && r.errors) || ['falhou']).join(' • '); return }\n      const eu = await chamar(d2, 'rEu', {})\n      if (eu && eu.ok) S.team = eu.team || []\n      await carregar(d2)\n      if (act === 'eqCriar') {\n        aviso(d2, 'Pessoa cadastrada. Passe a senha temporária para ela por um canal privado.')\n        ;['eq_email', 'eq_pessoa'].forEach(n => { const i = d2.$(`[name=\"${n}\"]`); if (i) i.value = '' })\n      } else aviso(d2, 'Papel atualizado.')\n    } catch (err) {\n      S.erroEquipe = msgErro(err)\n    } finally { S.busy = false }\n    return\n  }\n  if (!card) return\n  if (act === 'pegar') { await gravar(d2, 'rPegar', { card_id: card.card_id, session: `Board · ${S.me && S.me.person}` }, 'Tarefa em andamento.'); return }\n  if (act === 'mover') {\n    const to = val(d2, 'mv_to')\n    if (!to) { S.erroModal = 'Escolha o status de destino.'; return }\n    const r = await gravar(d2, 'rMover', limpo({ card_id: card.card_id, to, result: val(d2, 'mv_result'), approver: val(d2, 'mv_aprova'), blocked_reason: val(d2, 'mv_bloq'), app_version: val(d2, 'mv_versao'), reason: val(d2, 'mv_motivo') }), 'Status atualizado.')\n    if (r) S.mvTo = ''\n    return\n  }\n  if (act === 'editar') { S.editando = true; S.erroModal = null; return }\n  if (act === 'cancelarEdicao') { S.editando = false; return }\n  if (act === 'salvarEdicao') {\n    const prio = val(d2, 'ed_prio')\n    const r = await gravar(d2, 'rAtualizar', {\n      card_id: card.card_id, title: val(d2, 'ed_titulo'), description: val(d2, 'ed_desc'), acceptance: linhas(val(d2, 'ed_aceite')), plan: val(d2, 'ed_plano'),\n      agent: val(d2, 'ed_papel') || undefined, approver: val(d2, 'ed_aprova') || undefined, depends_on: lista(val(d2, 'ed_deps')), env: val(d2, 'ed_env') || undefined,\n      priority: prio ? Number(prio) : undefined, result: val(d2, 'ed_result') || undefined\n    }, 'Card atualizado.')\n    if (r) S.editando = false\n    return\n  }\n  if (act === 'comentar') {\n    const tipo = val(d2, 'ev_tipo') || 'note'\n    const r = await gravar(d2, 'rEvento', limpo({ type: tipo, card_id: card.card_id, to: (tipo === 'request' || tipo === 'question') ? val(d2, 'ev_para') : '', text: val(d2, 'ev_texto') }), 'Registrado.')\n    if (r) { const t = d2.$('[name=\"ev_texto\"]'); if (t) t.value = '' }\n    return\n  }\n  if (act === 'resolver') {\n    await gravar(d2, 'rResolver', limpo({ event_id: el.dataset.id, answer: val(d2, `ans_${el.dataset.id}`) }), 'Resolvido.')\n    return\n  }\n}\n\nfunction telaLogin(S) {\n  return `<div class=\"centro\"><div class=\"caixa\">\n    <div class=\"marca\">kanban mcp · br-cf</div>\n    <h1>Entrar</h1>\n    <p>Use o e-mail e a senha que o admin do kanban cadastrou para você.</p>\n    <label>E-mail</label><input name=\"lg_email\" type=\"email\" autocomplete=\"username\" placeholder=\"nome@data2.app\" />\n    <label>Senha</label><input name=\"lg_senha\" type=\"password\" autocomplete=\"current-password\" />\n    ${S.erroLogin ? `<div class=\"erro\">${esc(S.erroLogin)}</div>` : ''}\n    <div style=\"margin-top:16px\" class=\"linha\"><button class=\"btn pri\" data-act=\"login\" ${S.busy ? 'disabled' : ''}>${S.busy ? 'Entrando…' : 'Entrar'}</button></div>\n    <p class=\"muted\" style=\"margin-top:14px\">Esqueceu a senha? Peça ao admin do kanban um acesso novo.</p>\n  </div></div>`\n}\n\nfunction formSenha(S) {\n  return `\n    <label>Senha atual (a temporária, no primeiro acesso)</label><input name=\"pw_atual\" type=\"password\" autocomplete=\"current-password\" />\n    <label>Senha nova (mínimo 8 caracteres)</label><input name=\"pw_nova\" type=\"password\" autocomplete=\"new-password\" />\n    <label>Confirme a senha nova</label><input name=\"pw_conf\" type=\"password\" autocomplete=\"new-password\" />\n    ${S.erroSenha ? `<div class=\"erro\">${esc(S.erroSenha)}</div>` : ''}`\n}\n\nfunction telaTrocar(S) {\n  return `<div class=\"centro\"><div class=\"caixa\">\n    <div class=\"marca\">primeiro acesso</div>\n    <h1>Defina a sua senha</h1>\n    <p>${esc(S.me && S.me.person)}, troque a senha temporária antes de entrar no kanban.</p>\n    ${formSenha(S)}\n    <div style=\"margin-top:16px\" class=\"linha\">\n      <button class=\"btn pri\" data-act=\"trocar\" ${S.busy ? 'disabled' : ''}>${S.busy ? 'Salvando…' : 'Salvar senha'}</button>\n      <button class=\"btn\" data-act=\"sair\">Sair</button>\n    </div>\n  </div></div>`\n}\n\nfunction filtrar(S) {\n  const f = S.f || {}\n  const q = String(f.q || '').toLowerCase()\n  return ((S.dados && S.dados.tasks) || []).filter(t => {\n    if (!f.canceladas && t.status === 'cancelled') return false\n    if (f.frente && t.prefix !== f.frente) return false\n    if (f.pessoa && t.owner !== f.pessoa) return false\n    if (f.papel && t.agent !== f.papel) return false\n    if (f.demanda && t.demand_key !== f.demanda) return false\n    if (f.minhas && S.me && t.owner !== S.me.person && t.approver !== S.me.person) return false\n    if (q && !(`${t.card_id} ${t.title} ${t.description || ''} ${t.demand_key || ''}`.toLowerCase().includes(q))) return false\n    return true\n  })\n}\n\nfunction telaApp(S) {\n  const view = S.view || 'quadro'\n  const me = S.me || {}\n  const abertosMeus = ((S.dados && S.dados.open) || []).filter(e => e.to === me.person).length\n  const ehAdmin = me.role === 'admin'\n  const tabs = `<div class=\"tabs\">\n    <button class=\"tab ${view === 'quadro' ? 'on' : ''}\" data-act=\"view\" data-v=\"quadro\">Quadro</button>\n    <button class=\"tab ${view === 'painel' ? 'on' : ''}\" data-act=\"view\" data-v=\"painel\">Painel${abertosMeus ? `<span class=\"badge\">${abertosMeus}</span>` : ''}</button>\n    <button class=\"tab ${view === 'demandas' ? 'on' : ''}\" data-act=\"view\" data-v=\"demandas\">Demandas</button>\n    ${ehAdmin ? `<button class=\"tab ${view === 'equipe' ? 'on' : ''}\" data-act=\"view\" data-v=\"equipe\">Equipe</button>` : ''}\n  </div>`\n  const novo = podeGravar(S) ? `<button class=\"btn\" data-act=\"novaDemanda\">+ Demanda</button><button class=\"btn pri\" data-act=\"novoCard\">+ Tarefa</button>` : ''\n  const top = `<header class=\"top\">\n    <span class=\"nome\">Kanban br-cf</span>${tabs}<span class=\"esp\"></span>${novo}\n    <button class=\"btn\" data-act=\"recarregar\" title=\"Atualizar\" ${S.busy ? 'disabled' : ''}>↻</button>\n    <span class=\"eu\"><span class=\"av\">${iniciais(me.person)}</span> ${esc(me.person)} · ${esc(me.role)}</span>\n    <button class=\"btn min\" data-act=\"senhaModal\">Trocar senha</button>\n    <button class=\"btn min\" data-act=\"sair\">Sair</button>\n  </header>`\n  let corpo = ''\n  if (view === 'painel') corpo = telaPainel(S)\n  else if (view === 'demandas') corpo = telaDemandas(S)\n  else if (view === 'equipe' && ehAdmin) corpo = telaEquipe(S)\n  else corpo = telaQuadro(S)\n  return `${top}${corpo}${telaModal(S)}`\n}\n\nfunction barraFiltros(S) {\n  const f = S.f || {}\n  const frentes = catalogo(S, 'frente').map(c => ({ v: c.key, t: `${c.key} · ${c.name}` }))\n  const pessoas = catalogo(S, 'pessoa').map(c => c.key)\n  const papeis = catalogo(S, 'papel').map(c => c.key)\n  const demandas = ((S.dados && S.dados.demands) || []).map(d => ({ v: d.key, t: `${d.key} · ${d.title}` }))\n  return `<div class=\"filtros\">\n    <input type=\"search\" data-filtro=\"q\" value=\"${esc(f.q || '')}\" placeholder=\"Buscar por ID, título ou descrição\" />\n    <select data-filtro=\"frente\">${opcoes(frentes, f.frente, 'Todas as frentes')}</select>\n    <select data-filtro=\"pessoa\">${opcoes(pessoas, f.pessoa, 'Todas as pessoas')}</select>\n    <select data-filtro=\"papel\">${opcoes(papeis, f.papel, 'Todos os papéis')}</select>\n    <select data-filtro=\"demanda\">${opcoes(demandas, f.demanda, 'Todas as demandas')}</select>\n    <label class=\"chk\"><input type=\"checkbox\" data-filtro=\"minhas\" ${f.minhas ? 'checked' : ''} /> Só as minhas</label>\n    <label class=\"chk\"><input type=\"checkbox\" data-filtro=\"canceladas\" ${f.canceladas ? 'checked' : ''} /> Canceladas</label>\n  </div>`\n}\n\nfunction telaQuadro(S) {\n  const tarefas = filtrar(S)\n  const f = S.f || {}\n  const abertos = (S.dados && S.dados.open) || []\n  const todas = (S.dados && S.dados.tasks) || []\n  const statusDe = {}\n  todas.forEach(t => { statusDe[t.card_id] = t.status })\n  const cols = STATUS.filter(s => s.k !== 'cancelled' || f.canceladas).map(s => {\n    const lista = tarefas.filter(t => t.status === s.k)\n    const tiles = lista.map(t => {\n      const depsAbertas = (t.depends_on || []).filter(d => statusDe[d] !== 'done').length\n      const pend = abertos.filter(e => e.card_id === t.card_id).length\n      return `<button class=\"tile\" data-act=\"abrir\" data-id=\"${esc(t.card_id)}\" data-key=\"t-${esc(t.card_id)}\" style=\"border-left-color:${COR_TIPO[t.type] || '#ccc'}\">\n        <div class=\"l1\">[${esc(t.card_id)}] <span class=\"chip\" style=\"color:${COR_TIPO[t.type] || '#555'}\">${esc(TIPOS[t.type] || t.type || '')}</span>${t.priority ? `<span class=\"chip\">P${esc(t.priority)}</span>` : ''}</div>\n        <div class=\"t\">${esc(t.title)}</div>\n        <div class=\"l3\"><span class=\"av\" title=\"${esc(t.owner)}\">${iniciais(t.owner)}</span>${t.agent ? `<span class=\"chip\">⚙ ${esc(t.agent)}</span>` : ''}${depsAbertas ? `<span class=\"chip\" title=\"dependências abertas\">⛓ ${depsAbertas}</span>` : ''}${pend ? `<span class=\"chip\" style=\"color:#e5484d\" title=\"pedidos ou perguntas abertas\">● ${pend}</span>` : ''}<span class=\"muted\">${esc(t.demand_key || '')}</span></div>\n      </button>`\n    }).join('')\n    return `<section class=\"col\" data-key=\"c-${s.k}\">\n      <div class=\"col-h\"><span class=\"dot\" style=\"background:${s.c}\"></span>${esc(s.n)}<span class=\"n\">${lista.length}</span></div>\n      <div class=\"col-b\">${tiles || `<div class=\"vazio\">Nenhuma tarefa</div>`}</div>\n    </section>`\n  }).join('')\n  return `${barraFiltros(S)}<main class=\"main\"><div class=\"cols\">${cols}</div></main>`\n}\n\nfunction barras(titulo, itens, total) {\n  const max = Math.max(1, ...itens.map(i => i.q))\n  return `<div class=\"bloco\"><h3>${esc(titulo)}</h3>${itens.length ? itens.map(i => `<div class=\"barra\"><span>${esc(i.n)}</span><div class=\"trilho\"><div class=\"enche\" style=\"width:${Math.round(i.q / max * 100)}%;background:${i.c || '#a0522d'}\"></div></div><span class=\"q\">${i.q}</span></div>`).join('') : '<div class=\"muted\">Nada ainda.</div>'}</div>`\n}\n\nfunction telaPainel(S) {\n  const me = S.me || {}\n  const tarefas = ((S.dados && S.dados.tasks) || []).filter(t => t.status !== 'cancelled')\n  const abertas = tarefas.filter(t => t.status !== 'done')\n  const conta = (fn) => tarefas.filter(fn).length\n  const abertos = (S.dados && S.dados.open) || []\n  const kpis = [\n    ['Em aberto', abertas.length], ['Em andamento', conta(t => t.status === 'in_progress')], ['Bloqueadas', conta(t => t.status === 'blocked')],\n    ['Em validação', conta(t => t.status === 'in_validation')], ['Aguardando deploy', conta(t => t.status === 'awaiting_deploy')], ['Perguntas e pedidos abertos', abertos.length]\n  ].map(k => `<div class=\"kpi\"><div class=\"v\">${k[1]}</div><div class=\"r\">${esc(k[0])}</div></div>`).join('')\n  const porStatus = STATUS.filter(s => s.k !== 'cancelled').map(s => ({ n: s.n, q: conta(t => t.status === s.k), c: s.c }))\n  const porFrente = catalogo(S, 'frente').map(c => ({ n: `${c.key} · ${c.name}`, q: abertas.filter(t => t.prefix === c.key).length })).filter(i => i.q)\n  const porPessoa = catalogo(S, 'pessoa').map(c => ({ n: c.key, q: abertas.filter(t => t.owner === c.key).length })).filter(i => i.q)\n  const minhaCaixa = abertos.filter(e => e.to === me.person)\n  const aprovar = tarefas.filter(t => t.status === 'in_validation' && t.approver === me.person)\n  const caixa = `<div class=\"bloco\"><h3>Minha caixa de entrada</h3>${minhaCaixa.length ? minhaCaixa.map(e => `<div class=\"lin\"><span class=\"chip\">${esc(EVENTOS[e.type] || e.type)}</span><div style=\"flex:1\"><div>${esc(e.text)}</div><div class=\"muted\">${esc(e.from_person)}${e.from_agent ? ' · ' + esc(e.from_agent) : ''} · ${quando(e.created_at)}</div></div>${e.card_id ? `<button class=\"link\" data-act=\"abrir\" data-id=\"${esc(e.card_id)}\">${esc(e.card_id)}</button>` : esc(e.demand_key || '')}</div>`).join('') : '<div class=\"muted\">Nada esperando por você.</div>'}</div>`\n  const apr = `<div class=\"bloco\"><h3>Esperando a sua aprovação</h3>${aprovar.length ? aprovar.map(t => `<div class=\"lin\"><button class=\"link\" data-act=\"abrir\" data-id=\"${esc(t.card_id)}\">${esc(t.card_id)}</button><div style=\"flex:1\">${esc(t.title)}<div class=\"muted\">${esc(t.owner)} · desde ${quando(t.status_changed_at)}</div></div></div>`).join('') : '<div class=\"muted\">Nenhuma entrega esperando você.</div>'}</div>`\n  const bloq = tarefas.filter(t => t.status === 'blocked')\n  const blq = `<div class=\"bloco\"><h3>Bloqueadas</h3>${bloq.length ? bloq.map(t => `<div class=\"lin\"><button class=\"link\" data-act=\"abrir\" data-id=\"${esc(t.card_id)}\">${esc(t.card_id)}</button><div style=\"flex:1\">${esc(t.title)}<div class=\"muted\">${esc(t.blocked_reason || '')}</div></div></div>`).join('') : '<div class=\"muted\">Nenhuma tarefa bloqueada.</div>'}</div>`\n  return `<main class=\"main\"><div class=\"painel\"><div class=\"kpis\">${kpis}</div>${caixa}${apr}${blq}${barras('Por status', porStatus)}${barras('Em aberto por frente', porFrente)}${barras('Em aberto por pessoa', porPessoa)}</div></main>`\n}\n\nfunction telaDemandas(S) {\n  const demandas = (S.dados && S.dados.demands) || []\n  const tarefas = (S.dados && S.dados.tasks) || []\n  const linhasT = demandas.map(d => {\n    const ts = tarefas.filter(t => t.demand_key === d.key && t.status !== 'cancelled')\n    const feitas = ts.filter(t => t.status === 'done').length\n    return `<tr data-key=\"d-${esc(d.key)}\"><td><b>${esc(d.key)}</b></td><td>${esc(d.title)}${d.url ? ` <a href=\"${esc(d.url)}\" target=\"_blank\" rel=\"noopener\">JIRA</a>` : ''}<div class=\"muted\">${esc(d.summary || '')}</div></td><td>${esc(d.prefix || '')}</td><td>${esc(d.owner || '')}</td><td>${esc(d.status || '')}</td><td>${feitas}/${ts.length}</td>\n      <td><button class=\"btn min\" data-act=\"view\" data-v=\"quadro\">Ver</button> ${podeGravar(S) ? `<button class=\"btn min\" data-act=\"novoCard\" data-demanda=\"${esc(d.key)}\">+ Tarefa</button>` : ''}</td></tr>`\n  }).join('')\n  return `<main class=\"main\"><div style=\"padding:16px\"><div class=\"bloco\"><h3>Demandas</h3>\n    <table class=\"t\"><thead><tr><th>Chave</th><th>Título</th><th>Frente</th><th>Dono</th><th>Status</th><th>Feitas</th><th></th></tr></thead><tbody>${linhasT || '<tr><td colspan=\"7\" class=\"muted\">Nenhuma demanda.</td></tr>'}</tbody></table>\n    <p class=\"muted\">Para filtrar o quadro por uma demanda, use o filtro \"Todas as demandas\" na aba Quadro.</p>\n  </div></div></main>`\n}\n\nfunction telaEquipe(S) {\n  const team = S.team || []\n  const rows = team.map((u, i) => `<tr data-key=\"u-${esc(u.email)}\"><td>${esc(u.person)}</td><td>${esc(u.email)}</td>\n    <td><select name=\"role_${i}\">${opcoes(ROLES, u.role)}</select></td><td>${u.must_change_password ? 'precisa trocar a senha' : 'ok'}</td>\n    <td><button class=\"btn min\" data-act=\"eqPapel\" data-email=\"${esc(u.email)}\" data-i=\"${i}\">Salvar papel</button></td></tr>`).join('')\n  return `<main class=\"main\"><div style=\"padding:16px;display:grid;gap:14px;grid-template-columns:repeat(auto-fill,minmax(420px,1fr))\">\n    <div class=\"bloco\"><h3>Equipe</h3>\n      <table class=\"t\"><thead><tr><th>Pessoa</th><th>E-mail</th><th>Papel</th><th>Senha</th><th></th></tr></thead><tbody>${rows || '<tr><td colspan=\"5\" class=\"muted\">Ninguém cadastrado.</td></tr>'}</tbody></table>\n      <p class=\"muted\">leitor só vê; editor grava; admin também gerencia a equipe; bloqueado não entra no quadro.</p>\n    </div>\n    <div class=\"bloco\"><h3>Cadastrar pessoa</h3>\n      <label>E-mail</label><input name=\"eq_email\" type=\"email\" placeholder=\"nome@data2.app\" />\n      <label>Nome no kanban (como vai aparecer nos cards)</label><input name=\"eq_pessoa\" placeholder=\"Nome Sobrenome\" />\n      <label>Papel</label><select name=\"eq_papel\">${opcoes(ROLES, 'editor')}</select>\n      <label>Senha temporária (a pessoa troca no primeiro acesso)</label>\n      <div class=\"linha\"><input name=\"eq_senha\" style=\"flex:1\" /><button class=\"btn\" data-act=\"eqGerar\">Gerar</button></div>\n      ${S.erroEquipe ? `<div class=\"erro\">${esc(S.erroEquipe)}</div>` : ''}\n      <div style=\"margin-top:12px\"><button class=\"btn pri\" data-act=\"eqCriar\" ${S.busy ? 'disabled' : ''}>Cadastrar</button></div>\n      <p class=\"aviso\">Ainda não dá para redefinir a senha de quem já tem acesso: a própria pessoa troca no board. Se alguém esquecer, fale com o Claude do time.</p>\n    </div>\n  </div></main>`\n}\n\nfunction telaModal(S) {\n  const m = S.modal\n  if (!m) return ''\n  if (m.tipo === 'senha') return `<div class=\"scrim\"><div class=\"modal peq\"><div class=\"m-h\"><h2>Trocar senha</h2><button class=\"btn min\" data-act=\"fechar\">Fechar</button></div><div class=\"m-b\">${formSenha(S)}<div style=\"margin-top:14px\"><button class=\"btn pri\" data-act=\"trocar\" ${S.busy ? 'disabled' : ''}>Salvar senha</button></div></div></div></div>`\n  if (m.tipo === 'novaDemanda') return modalDemanda(S)\n  if (m.tipo === 'novoCard') return modalNovoCard(S)\n  return modalCard(S)\n}\n\nfunction modalDemanda(S) {\n  const frentes = catalogo(S, 'frente').map(c => ({ v: c.key, t: `${c.key} · ${c.name}` }))\n  const pessoas = catalogo(S, 'pessoa').map(c => c.key)\n  return `<div class=\"scrim\"><div class=\"modal peq\"><div class=\"m-h\"><h2>Nova demanda</h2><button class=\"btn min\" data-act=\"fechar\">Fechar</button></div><div class=\"m-b\">\n    <label>Chave do JIRA (vazio = demanda interna CF-N)</label><input name=\"nd_chave\" placeholder=\"JCFA-1234\" />\n    <label>Título</label><input name=\"nd_titulo\" />\n    <label>Link do JIRA</label><input name=\"nd_url\" placeholder=\"https://…\" />\n    <label>Frente principal</label><select name=\"nd_prefixo\">${opcoes(frentes, '', '—')}</select>\n    <label>Dev responsável</label><select name=\"nd_dono\">${opcoes(pessoas, S.me && S.me.person)}</select>\n    <label>Quem pediu</label><input name=\"nd_pediu\" />\n    <label>Resumo (nas nossas palavras)</label><textarea name=\"nd_resumo\"></textarea>\n    <label>Texto original do JIRA</label><textarea name=\"nd_jira\" style=\"min-height:110px\"></textarea>\n    <label>Critérios de aceite (um por linha)</label><textarea name=\"nd_aceite\"></textarea>\n    ${S.erroModal ? `<div class=\"erro\">${esc(S.erroModal)}</div>` : ''}\n    <div style=\"margin-top:14px\"><button class=\"btn pri\" data-act=\"criarDemanda\" ${S.busy ? 'disabled' : ''}>Salvar demanda</button></div>\n  </div></div></div>`\n}\n\nfunction modalNovoCard(S) {\n  const frentes = catalogo(S, 'frente').map(c => ({ v: c.key, t: `${c.key} · ${c.name}` }))\n  const pessoas = catalogo(S, 'pessoa').map(c => c.key)\n  const papeis = catalogo(S, 'papel').map(c => ({ v: c.key, t: `${c.key} · ${c.name}` }))\n  const demandas = ((S.dados && S.dados.demands) || []).filter(d => d.status !== 'done' && d.status !== 'cancelled').map(d => ({ v: d.key, t: `${d.key} · ${d.title}` }))\n  const dem = ((S.dados && S.dados.demands) || []).find(d => d.key === S.modal.demanda)\n  return `<div class=\"scrim\"><div class=\"modal peq\"><div class=\"m-h\"><h2>Nova tarefa</h2><button class=\"btn min\" data-act=\"fechar\">Fechar</button></div><div class=\"m-b\">\n    <label>Demanda</label><select name=\"nc_demanda\">${opcoes(demandas, S.modal.demanda, 'Escolha a demanda')}</select>\n    <div class=\"linha\"><div style=\"flex:1\"><label>Frente</label><select name=\"nc_prefixo\">${opcoes(frentes, dem && dem.prefix, 'Escolha')}</select></div>\n      <div style=\"flex:1\"><label>Tipo</label><select name=\"nc_tipo\">${opcoes([{ v: 'feature', t: 'Feature' }, { v: 'spike', t: 'Spike (investigar)' }, { v: 'bug', t: 'Bug' }], 'feature')}</select></div></div>\n    <label>Título</label><input name=\"nc_titulo\" />\n    <div class=\"linha\"><div style=\"flex:1\"><label>Papel do agente</label><select name=\"nc_papel\">${opcoes(papeis, '', '—')}</select></div>\n      <div style=\"flex:1\"><label>Quem aprova</label><select name=\"nc_aprova\">${opcoes(pessoas, S.me && S.me.person, '—')}</select></div></div>\n    <label>Descrição</label><textarea name=\"nc_desc\"></textarea>\n    <label>Critérios de pronto (um por linha)</label><textarea name=\"nc_aceite\"></textarea>\n    <label>Depende de (IDs separados por vírgula)</label><input name=\"nc_deps\" placeholder=\"TRC-3, CAN-1\" />\n    <div class=\"linha\"><div style=\"flex:1\"><label>Status inicial</label><select name=\"nc_status\">${opcoes([{ v: 'backlog', t: 'Backlog' }, { v: 'ready', t: 'Pronta (precisa de critérios e papel)' }], 'backlog')}</select></div>\n      <div style=\"flex:1\"><label>Ambiente</label><select name=\"nc_env\">${opcoes(ENVS, '', '—')}</select></div>\n      <div style=\"flex:1\"><label>Prioridade</label><select name=\"nc_prio\">${opcoes(['1', '2', '3'], '', '—')}</select></div></div>\n    ${S.erroModal ? `<div class=\"erro\">${esc(S.erroModal)}</div>` : ''}\n    <div style=\"margin-top:14px\"><button class=\"btn pri\" data-act=\"criarCard\" ${S.busy ? 'disabled' : ''}>Criar tarefa</button></div>\n  </div></div></div>`\n}\n\nfunction modalCard(S) {\n  const cd = S.cardData\n  const fechar = `<button class=\"btn min\" data-act=\"fechar\">Fechar</button>`\n  if (!cd) return `<div class=\"scrim\"><div class=\"modal\"><div class=\"m-h\"><h2>${esc(S.modal.id)}</h2>${fechar}</div><div class=\"m-b muted\">Carregando…</div></div></div>`\n  if (cd.erro) return `<div class=\"scrim\"><div class=\"modal\"><div class=\"m-h\"><h2>${esc(S.modal.id)}</h2>${fechar}</div><div class=\"m-b\"><div class=\"erro\">${esc(cd.erro)}</div></div></div></div>`\n  const c = cd.card || {}\n  const me = S.me || {}\n  const st = stInfo(c.status)\n  const dem = cd.demand || {}\n  const ehDono = c.owner === me.person || me.role === 'builder'\n  const ehAprov = c.approver === me.person\n  const grava = podeGravar(S)\n  const campo = (n, v) => `<div><span>${esc(n)}</span>${v ? esc(v) : '<span class=\"muted\">—</span>'}</div>`\n  const grade = `<div class=\"grade\">${campo('Dona', c.owner)}${campo('Papel do agente', c.agent)}${campo('Quem aprova', c.approver)}${campo('Sessão', c.session)}${campo('Ambiente', c.env)}${campo('Prioridade', c.priority)}${campo('Versão do app', c.app_version)}${campo('No status desde', quando(c.status_changed_at))}</div>`\n  const lst = (arr) => (arr && arr.length) ? `<ul class=\"l\">${arr.map(x => `<li>${esc(x)}</li>`).join('')}</ul>` : '<span class=\"muted\">—</span>'\n  const deps = (cd.depends_on || []).map(t => `<li><button class=\"link\" data-act=\"abrir\" data-id=\"${esc(t.card_id)}\">${esc(t.card_id)}</button> ${esc(t.title)} <span class=\"chip\">${esc(stInfo(t.status).n)}</span></li>`).join('')\n  const depd = (cd.dependents || []).map(t => `<li><button class=\"link\" data-act=\"abrir\" data-id=\"${esc(t.card_id)}\">${esc(t.card_id)}</button> ${esc(t.title)} <span class=\"chip\">${esc(stInfo(t.status).n)}</span></li>`).join('')\n  const toques = (c.touches || []).map(t => `<li><b>${esc(t.name || '')}</b> <code>${esc(t.reference)}</code> ${t.kind ? `<span class=\"muted\">${esc(t.kind)}</span>` : ''}</li>`).join('')\n  const mudancas = (c.changes || []).map(m => `<li><b>${esc(m.name || '')}</b> <code>${esc(m.reference)}</code>: ${esc(m.what)}${m.backup ? ` <span class=\"muted\">(backup: ${esc(m.backup)})</span>` : ''}<div class=\"muted\">${esc(m.by || '')} · ${quando(m.at)}</div></li>`).join('')\n  let esquerda = ''\n  if (S.editando && ehDono) {\n    const papeis = catalogo(S, 'papel').map(p => ({ v: p.key, t: p.key }))\n    const pessoas = catalogo(S, 'pessoa').map(p => p.key)\n    esquerda = `<label>Título</label><input name=\"ed_titulo\" value=\"${esc(c.title)}\" />\n      <div class=\"linha\"><div style=\"flex:1\"><label>Papel do agente</label><select name=\"ed_papel\">${opcoes(papeis, c.agent, '—')}</select></div>\n      <div style=\"flex:1\"><label>Quem aprova</label><select name=\"ed_aprova\">${opcoes(pessoas, c.approver, '—')}</select></div>\n      <div style=\"flex:1\"><label>Ambiente</label><select name=\"ed_env\">${opcoes(ENVS, c.env, '—')}</select></div>\n      <div style=\"flex:1\"><label>Prioridade</label><select name=\"ed_prio\">${opcoes(['1', '2', '3'], c.priority, '—')}</select></div></div>\n      <label>Descrição</label><textarea name=\"ed_desc\">${esc(c.description || '')}</textarea>\n      <label>Critérios de pronto (um por linha)</label><textarea name=\"ed_aceite\">${esc((c.acceptance || []).join('\\n'))}</textarea>\n      <label>Plano</label><textarea name=\"ed_plano\" style=\"min-height:110px\">${esc(c.plan || '')}</textarea>\n      <label>Depende de (IDs separados por vírgula)</label><input name=\"ed_deps\" value=\"${esc((c.depends_on || []).join(', '))}\" />\n      <label>Resultado</label><textarea name=\"ed_result\">${esc(c.result || '')}</textarea>\n      ${S.erroModal ? `<div class=\"erro\">${esc(S.erroModal)}</div>` : ''}\n      <div class=\"linha\" style=\"margin-top:12px\"><button class=\"btn pri\" data-act=\"salvarEdicao\" ${S.busy ? 'disabled' : ''}>Salvar</button><button class=\"btn\" data-act=\"cancelarEdicao\">Cancelar</button></div>`\n  } else {\n    esquerda = `<div class=\"linha\"><span class=\"chip\" style=\"color:${COR_TIPO[c.type] || '#555'}\">${esc(TIPOS[c.type] || c.type || '')}</span><span class=\"chip\" style=\"color:${st.c}\">● ${esc(st.n)}</span>\n      ${dem.key ? `<span class=\"muted\">Demanda <b>${esc(dem.key)}</b> · ${esc(dem.title || '')}${dem.url ? ` · <a href=\"${esc(dem.url)}\" target=\"_blank\" rel=\"noopener\">JIRA</a>` : ''}</span>` : ''}</div>\n      ${grade}\n      ${c.blocked_reason ? `<div class=\"aviso\"><b>Bloqueada:</b> ${esc(c.blocked_reason)}</div>` : ''}\n      <div class=\"sec\"><h4>Descrição</h4><div class=\"texto\">${esc(c.description || '—')}</div></div>\n      <div class=\"sec\"><h4>Critérios de pronto</h4>${lst(c.acceptance)}</div>\n      ${c.plan ? `<div class=\"sec\"><h4>Plano</h4><div class=\"texto\">${esc(c.plan)}</div></div>` : ''}\n      ${c.result ? `<div class=\"sec\"><h4>Resultado</h4><div class=\"texto\">${esc(c.result)}</div></div>` : ''}\n      <div class=\"sec\"><h4>Depende de</h4>${deps ? `<ul class=\"l\">${deps}</ul>` : '<span class=\"muted\">—</span>'}</div>\n      <div class=\"sec\"><h4>Dependem desta</h4>${depd ? `<ul class=\"l\">${depd}</ul>` : '<span class=\"muted\">—</span>'}</div>\n      <div class=\"sec\"><h4>O que mexe</h4>${toques ? `<ul class=\"l\">${toques}</ul>` : '<span class=\"muted\">—</span>'}</div>\n      <div class=\"sec\"><h4>Alterações feitas</h4>${mudancas ? `<ul class=\"l\">${mudancas}</ul>` : '<span class=\"muted\">—</span>'}</div>\n      <div class=\"muted\" style=\"margin-top:12px\">Criada em ${quando(c.created_at)} por ${esc(c.created_by || '')} · atualizada em ${quando(c.updated_at)} por ${esc(c.updated_by || '')}</div>`\n  }\n  const destinos = (TRANS[c.status] || []).map(k => ({ v: k, t: stInfo(k).n }))\n  const mv = S.mvTo || ''\n  const pessoas = catalogo(S, 'pessoa').map(p => p.key)\n  let acoes = ''\n  if (grava && !S.editando) {\n    const extra = mv === 'in_validation' ? `<label>Resultado: o que foi entregue e o que faltou</label><textarea name=\"mv_result\">${esc(c.result || '')}</textarea><label>Quem aprova</label><select name=\"mv_aprova\">${opcoes(pessoas, c.approver, '—')}</select>`\n      : mv === 'blocked' ? `<label>Do que e de quem depende</label><textarea name=\"mv_bloq\"></textarea>`\n      : mv === 'done' && c.status === 'awaiting_deploy' ? `<label>Versão do app em que subiu</label><input name=\"mv_versao\" value=\"${esc(c.app_version || '')}\" />`\n      : mv === 'cancelled' ? `<label>Motivo</label><textarea name=\"mv_motivo\"></textarea>`\n      : mv ? `<label>Motivo (opcional)</label><input name=\"mv_motivo\" />` : ''\n    acoes = `<div class=\"acoes\">\n      ${c.status === 'ready' && c.owner === me.person ? `<button class=\"btn pri\" data-act=\"pegar\" ${S.busy ? 'disabled' : ''}>Começar (pegar a tarefa)</button>` : ''}\n      ${(ehDono || ehAprov) && destinos.length ? `<label>Mover para</label><select name=\"mv_to\">${opcoes(destinos, mv, 'Escolha o status')}</select>${extra}<div style=\"margin-top:8px\"><button class=\"btn\" data-act=\"mover\" ${S.busy ? 'disabled' : ''}>Mover</button></div>` : ''}\n      ${ehDono ? `<div style=\"margin-top:8px\"><button class=\"btn min\" data-act=\"editar\">Editar card</button></div>` : ''}\n      ${S.erroModal ? `<div class=\"erro\">${esc(S.erroModal)}</div>` : ''}\n    </div>`\n  }\n  const tipoEv = S.evTipo || 'note'\n  const comentar = grava ? `<div class=\"acoes\" style=\"margin-top:10px\"><h4>Registrar</h4>\n    <div class=\"linha\"><select name=\"ev_tipo\" style=\"flex:1\">${opcoes([{ v: 'note', t: 'Nota' }, { v: 'request', t: 'Pedido para alguém' }, { v: 'question', t: 'Pergunta (decisão pendente)' }], tipoEv)}</select>\n    ${tipoEv === 'request' || tipoEv === 'question' ? `<select name=\"ev_para\" style=\"flex:1\">${opcoes(pessoas, c.owner, 'Para quem')}</select>` : ''}</div>\n    <textarea name=\"ev_texto\" style=\"margin-top:6px\" placeholder=\"Escreva aqui\"></textarea>\n    <div style=\"margin-top:6px\"><button class=\"btn\" data-act=\"comentar\" ${S.busy ? 'disabled' : ''}>Registrar</button></div></div>` : ''\n  const eventos = (cd.events || []).map(e => {\n    const aberto = (e.type === 'request' || e.type === 'question') && !e.resolved\n    const podeResolver = aberto && grava && (e.to === me.person || e.from_person === me.person)\n    return `<div class=\"ev ${aberto ? 'aberto' : ''}\" data-key=\"e-${esc(e._id)}\">\n      <div class=\"q\"><b>${esc(EVENTOS[e.type] || e.type)}</b> · ${esc(e.from_person || '')}${e.from_agent ? ' · ' + esc(e.from_agent) : ''}${e.to ? ' → ' + esc(e.to) : ''} · ${quando(e.created_at)}${aberto ? ' · <b style=\"color:#e5484d\">aberto</b>' : ''}</div>\n      <div class=\"x\">${esc(e.text)}</div>\n      ${podeResolver ? `<div class=\"linha\" style=\"margin-top:6px\"><input name=\"ans_${esc(e._id)}\" placeholder=\"${e.type === 'question' ? 'Decisão tomada (obrigatório)' : 'Resposta (opcional)'}\" style=\"flex:1\" /><button class=\"btn min\" data-act=\"resolver\" data-id=\"${esc(e._id)}\">Resolver</button></div>` : ''}\n    </div>`\n  }).join('')\n  return `<div class=\"scrim\"><div class=\"modal\">\n    <div class=\"m-h\"><h2>[${esc(c.card_id)}] ${esc(c.title)}</h2>${fechar}</div>\n    <div class=\"m-b\"><div class=\"duas\">\n      <div>${esquerda}</div>\n      <div>${acoes}${comentar}<div class=\"sec\"><h4>Linha do tempo</h4>${eventos || '<span class=\"muted\">Sem eventos.</span>'}</div></div>\n    </div></div>\n  </div></div>`\n}\n"}},"auth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"auth"}}}},"rQuadro":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rQuadro"}}}},"rCard":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rCard"}}}},"rEu":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rEu"}}}},"rSenhaOk":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rSenhaOk"}}}},"rCriar":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rCriar"}}}},"rAtualizar":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rAtualizar"}}}},"rPegar":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rPegar"}}}},"rMover":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rMover"}}}},"rEvento":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rEvento"}}}},"rResolver":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rResolver"}}}},"rDemanda":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rDemanda"}}}},"rUsuario":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"rUsuario"}}}}}}}}}},{"_id":"5ed9bed7b9e0891ce913e884","component":"data2-core-types@Component","params":{"package":"data2-core","component":"DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}},{"_id":"5f472ae463c680de4ff296e9","component":"data2-core-types@Component","params":{"package":"data2-core","component":"Data2TableDataSource","extends":"data2-core@DataSource","renderable":{"dimensions":{"disabled":false,"component":"data2-core-types@Dimensions","params":{"width":1366,"exactWidth":false,"exactHeight":false,"height":768}}}}}]}},{"reference":"ExternalComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"_id":"5f060d9b1ca96def42f3c40a","displayName":"Builder AuthStrategy","component":"data2-core@AuthenticationStrategy","system":"5e3b796d6228227ca637d0f4","params":{"credentials":[{"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"status"}}],"verifier":{"component":"data2-core-types@DataSetReference","params":{"reference":"UsersDataSet"}},"sessionShared":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"password":{"disabled":false,"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},"email":{"disabled":false,"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},"resetPassword":{"resetEmail":{"artboard":{"params":{"preventEvaluation":true,"reference":"nR6Q9d7qdaPtZhJ28Q8Dy"},"component":"data2-core-types@ArtboardReference"}},"resetSuccessEmail":{"artboard":{"params":{"preventEvaluation":true,"reference":"yPCJBTKo8q1Mw0B_3Kpwl"},"component":"data2-core-types@ArtboardReference"}}},"session":[{"component":"data2-core-types@DataSetColumnReference","params":{"column":"name"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"email"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"password"}},{"component":"data2-core-types@DataSetColumnReference","params":{"column":"status"}}],"sessionMetadata":{"params":{"actions":[{"params":{"compare":{"params":{"subject":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},{"params":{"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"active"}},"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user.status"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is active user"}]},"component":"data2-core-logical-operators@And","displayName":"is active and has _id"}},"component":"data2-core-logical-operators@Not"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check $user"},{"params":{"action":{"component":"data2-core-types@Getter","displayName":"isData2User","params":{"module":{"component":"data2-core-functions@Match","params":{"pattern":{"component":"data2-core-types@Regex","params":{"pattern":"@data2\\.app$","flags":"i"}},"text":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user.email"}}}}}},"prop":{"component":"data2-core-primitives@String","params":{"content":"0"}}}},"resultName":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}},"component":"data2-core-flow@Step"},{"params":{"action":{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.d2_app"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"management"}}]},"component":"data2-core-functions@Includes","displayName":"isData2App Exception"},"resultName":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}},"component":"data2-core-flow@Step"},{"params":{"resultName":{"component":"data2-core-primitives@String","params":{"content":"has_access"}},"action":{"params":{"compare":{"params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}}},{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}}}]},"component":"data2-core-logical-operators@Or"},"else":{"params":{"actions":[{"params":{"actions":[{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}},"component":"data2-core-logical-operators@Not"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check $metadata.system"},{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":"filter","args":[{"_id":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}}]},"component":"data2-core-types@Interaction"},{"params":{"compare":{"params":{"subject":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records.length"}}}}},"component":"data2-core-logical-operators@Not","displayName":"systems not found"},"then":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"}},"component":"data2-core-flow@If","displayName":"check system found"},{"params":{"actions":[{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":"filter","args":[{"user":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},"organization":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"cursor.current.organization._id"}}}}}]},"component":"data2-core-types@Interaction"},{"params":{"module":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":"filter","args":[{"user":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$user._id"}}}},"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}}]},"component":"data2-core-types@Interaction"}]},"component":"data2-core-flow@Parallel"}]},"component":"data2-core-flow@Series","displayName":"common setup"},{"params":{"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"else":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"compare":{"params":{"components":{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"WuUa-VKHhW9uYU0E3-mJ8"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"mapper":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"system"}}],"body":{"params":{"expression":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.visibility"}}}},"defaultCase":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"public"}},"value":{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"owner"}},{"component":"data2-core-primitives@String","params":{"content":"billing-owner"}},{"component":"data2-core-primitives@String","params":{"content":"admin"}},{"component":"data2-core-primitives@String","params":{"content":"member"}}]},"component":"data2-core-functions@Includes","displayName":"has role with access"}]},"component":"data2-core-logical-operators@And","displayName":"has org member role with access"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is org member"},{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"comparisonSubject":{"component":"data2-core-primitives@String","params":{"content":"collaborator"}}},"component":"data2-core-comparison-operators@Equal","displayName":"is collaborator"}]},"component":"data2-core-logical-operators@And","displayName":"is org member and collaborator"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is collaborator"},{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"access"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"access.system._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"has system access"}]},"component":"data2-core-logical-operators@And","displayName":"is collaborator and has system access"}]},"component":"data2-core-logical-operators@Or"}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"private"}},"value":{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"components":[{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal","displayName":"is org member"},{"params":{"pattern":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.role"}}}},"subject":[{"component":"data2-core-primitives@String","params":{"content":"owner"}},{"component":"data2-core-primitives@String","params":{"content":"admin"}}]},"component":"data2-core-functions@Includes","displayName":"is owner/admin"}]},"component":"data2-core-logical-operators@And","displayName":"has org member role with access"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is owner/admin"},{"params":{"components":[{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Ok6Tsu5XkS7r8PyW_7TyW"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"member"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"member.organization._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system.organization._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"is org member"},{"params":{"array":{"params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"NqKAY34n2ffFL64zamp7t"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.records"}}},"component":"data2-core-types@Getter"},"filter":{"disabled":false,"component":"data2-core-functions@Function","params":{"parameters":[{"component":"data2-core-primitives@String","params":{"content":"access"}}],"body":{"params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"access.system._id"}}}},"comparisonSubject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"system._id"}}}}},"component":"data2-core-comparison-operators@Equal"}}}},"component":"data2-core-functions@ArrayFind","displayName":"has system access"}]},"component":"data2-core-logical-operators@And","displayName":"is member and has system access"}]},"component":"data2-core-logical-operators@Or","displayName":"is owner/admin or has system access"}}}]},"component":"data2-core-flow@Switch"}}}},"component":"data2-core-functions@ArrayMap"}},"component":"data2-core-logical-operators@And","displayName":"has org member for all systems in request"}},"component":"data2-core-flow@If"}]},"component":"data2-core-flow@Series"},"then":{"params":{"content":true},"component":"data2-core-primitives@Boolean"}},"component":"data2-core-flow@If"}},"component":"data2-core-flow@Step"},{"params":{"then":{"component":"data2-core-flow@If","displayName":"management exception never picks the system","params":{"compare":{"component":"data2-core-logical-operators@Or","params":{"components":[{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_user"}}}},{"component":"data2-core-logical-operators@Not","params":{"subject":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"is_data2_app_exception"}}}}}}]}},"then":{"system":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"$metadata.system"}}}}},"else":{"d2_app":"management"}}},"else":{"params":{"message":{"component":"data2-core-primitives@String","params":{"content":"Invalid Credentials"}},"code":{"component":"data2-core-primitives@String","params":{"content":"401"}}},"component":"data2-core-types@Error"},"compare":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"has_access"}}}}},"component":"data2-core-flow@If"}]},"component":"data2-core-flow@Series"},"sessionExpiresIn":{"component":"data2-core-primitives@Number","params":{"content":604800}},"sessionRenewal":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"identityProviders":[{"provider":{"component":"data2-core-primitives@String","params":{"content":"GOOGLE"}},"clientId":{"component":"data2-core-primitives@String","params":{"content":"55470861603-22pvfc1t49lp8cjnbfk71s5ct9hp1q1v.apps.googleusercontent.com"}}}]},"reference":"Data2BuilderAuthentication","updatedAt":"2026-10-29T19:05:43.007Z","updateMetadata":{"updatedBy":{"user":"5f1609be1ca96def427e1559"}}},{"_id":"5f060ee51ca96def42f3fbd1","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"UsersTableDataSource"}},"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":1}}}},"reference":"UsersDataSet","updatedAt":"2026-03-04T21:16:40.291Z"},{"component":"data2-core@DataSet","reference":"SessionsDataSet","params":{"requiresConfirmation":false,"pk":"key","datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"data2sessionsdatasource"}}}},{"component":"data2-core@ServerSessionsDataSource","reference":"data2sessionsdatasource","params":{}},{"reference":"UsersTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}},{"_id":"5f16172a0528b92601890f85","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":{"component":"data2-core-primitives@Number","params":{"content":10000}}},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"DataComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"DataComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:12.831Z","createdAt":"2020-07-20T22:14:02.097Z"},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"66f2cf9483706454f771304c","component":"data2-core@Store","system":"6671e96b001009d97f222f73","params":{"initialData":{"params":{"fields":{"responding":{"component":"data2-core-primitives@Boolean","params":{}},"messages":{"params":{"then":[{"index":0,"sender":"HUMAN","time":"2024-08-23T22:24:36+00:00","id":"rYQ8O7e_q8W0eL4rO10a5","assistant":{},"type":"text","text":{"body":"hola! "}},{"index":1,"sender":"ASSISTANT","time":"2024-08-23T22:24:37+00:00","id":"gVt2oHC_-sFE8pZVtTf1u","assistant":{},"type":"text","text":{"body":"Hola Soy Avi/Nat la asistente virtual de Natura y estoy aquí para ayudarte 😃"}},{"index":2,"sender":"ASSISTANT","time":"2024-08-23T22:24:38+00:00","id":"BiJT1RqXJqJOoe28rq1F5","assistant":{},"type":"interactive","interactive":{"type":"list","body":{"text":"Ahora elige como quieres ingresar: ⬇️","type":"text"},"action":{"button":"Perfiles","sections":[{"title":"Perfiles","rows":[{"id":"consultant","title":"Consultor(a) de Belleza Natura y digital. ❤️","description":"Consultor(a)"},{"id":"leader","title":"Nuestros líderes. 🫱🏽‍🫲🏿","description":"Líder"},{"id":"manager","title":"Nuestros gerentes. ⭐","description":"Gerente"}]}]}}},{"index":2,"sender":{"component":"data2-core-primitives@String","params":{"content":"HUMAN"}},"time":"2024-08-23T22:24:38+00:00","id":"BiJT1RqXJqJOoe28rq1F5","assistant":{},"type":{"component":"data2-core-primitives@String","params":{"content":"image"}},"interactive":{"type":"list","body":{"text":{"component":"data2-core-primitives@String","params":{"content":"https://natura-br-cf.data2.media/file/MY-RnS-p1fl5AxSmHPM9D"}},"type":{"component":"data2-core-primitives@String","params":{"content":"image"}}},"action":{"button":"Perfiles","sections":[{"title":"Perfiles","rows":[{"id":"consultant","title":"Consultor(a) de Belleza Natura y digital. ❤️","description":"Consultor(a)"},{"id":"leader","title":"Nuestros líderes. 🫱🏽‍🫲🏿","description":"Líder"},{"id":"manager","title":"Nuestros gerentes. ⭐","description":"Gerente"}]}]}}}],"compare":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"builder"}}}},"else":[]},"component":"data2-core-flow@If"},"startTime":{"component":"data2-core-primitives@String","params":{}},"chatId":{"params":{},"component":"data2-core-primitives@String"},"observe_fix_scroll":{"params":{},"component":"data2-core-functions@DateNow"},"typing":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"audio_url":{"component":"data2-core-primitives@String","params":{}},"audio_state":{"component":"data2-core-primitives@String","params":{"content":"standby"}},"survey_opened":{"component":"data2-core-primitives@Boolean","params":{}},"emoji":{"params":{},"component":"data2-core-primitives@Boolean"},"attachment_state":{"component":"data2-core-primitives@String","params":{"content":"standby"}},"attachment":{"component":"data2-core-primitives@String","params":{}},"mute":{"params":{},"component":"data2-core-primitives@Boolean"},"isTyping":{"params":{},"component":"data2-core-primitives@Boolean"},"flow":{"component":"data2-core-primitives@String","params":{"content":"START_FLOW"}},"webchat_navigation":{"component":"data2-core-primitives@String","params":{}},"navigation":{"component":"data2-core-primitives@String","params":{"content":"initial"}},"clickButton":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"durationStart":{"params":{"content":"2025-01-27T16:30:00.000Z"},"component":"data2-core-primitives@Date"},"navigation_csat":{"component":"data2-core-primitives@String","params":{"content":"initial"}},"notaCSAT":{"params":{"content":""},"component":"data2-core-primitives@Number"},"motivoCSAT":{"component":"data2-core-primitives@String","params":{}},"use_case":{"component":"data2-core-primitives@String","params":{}},"brand":{"component":"data2-core-primitives@String","params":{}},"omni":[],"titleCSAT":{"component":"data2-core-primitives@String","params":{}},"tag_session_control":{"params":{},"component":"data2-core-primitives@Boolean"},"conversation_flow":{"component":"data2-core-primitives@String","params":{"content":"ASSISTANT"}},"score_atendimento":{"component":"data2-core-primitives@String","params":{}},"score_solucao":{"component":"data2-core-primitives@String","params":{}}}},"component":"data2-core-types@FieldSet"}},"reference":"ufKuWDW7edV_702iYowsi"},{"_id":"66f2cf9483706454f771303e","component":"data2-core@Store","system":"6671e96b001009d97f222f73","params":{"initialData":{"audioToText":{"params":{},"component":"data2-core-primitives@Undefined"},"selectedUseCase":{"params":{},"component":"data2-core-primitives@Undefined"},"faqQuestion":{"params":{},"component":"data2-core-primitives@Undefined"},"answer":{"params":{},"component":"data2-core-primitives@Undefined"},"textToAudioUrl":{"params":{},"component":"data2-core-primitives@Undefined"},"selectedUseCaseDetail":{"params":{},"component":"data2-core-primitives@Undefined"},"selectedFaqDetail":{"params":{},"component":"data2-core-primitives@Undefined"},"tokens":{"params":{},"component":"data2-core-primitives@Undefined"},"usecaseTokens":{"params":{},"component":"data2-core-primitives@Undefined"},"voiceCharacters":{"params":{},"component":"data2-core-primitives@Undefined"},"question":{"params":{},"component":"data2-core-primitives@Undefined"},"replyOption":{"params":{},"component":"data2-core-primitives@Undefined"},"usecaseSideEffect":{"params":{},"component":"data2-core-primitives@Undefined"},"orderNumber":{"component":"data2-core-primitives@String","params":{}},"impersonation_consultant":{"params":{},"component":"data2-core-primitives@Null"}}},"reference":"NIc2rphwDz6yq9gNdWJQo"},{"_id":"68ff6250c90b01edc9688f0c","component":"data2-core@Store","system":"6671e96b001009d97f222f73","params":{"initialData":{"params":{"fields":{"JWE":{"component":"data2-core-primitives@String","params":{}},"siteid":{"component":"data2-core-primitives@String","params":{"content":""}},"address":{"address1":{"component":"data2-core-primitives@String","params":{}},"number":{"component":"data2-core-primitives@String","params":{}},"neighborhood":{"component":"data2-core-primitives@String","params":{}},"city":{"component":"data2-core-primitives@String","params":{}},"stateCode":{"component":"data2-core-primitives@String","params":{}},"countryCode":{"component":"data2-core-primitives@String","params":{}},"postalCode":{"component":"data2-core-primitives@String","params":{}},"complement":{"component":"data2-core-primitives@String","params":{}},"reference":{"component":"data2-core-primitives@String","params":{}}},"order-source":{"component":"data2-core-primitives@String","params":{}},"products":{"component":"data2-core-primitives@String","params":{}},"orderNo":{"component":"data2-core-primitives@String","params":{}},"person_data":{"component":"data2-core-primitives@String","params":{}},"option":{"params":{"content":0},"component":"data2-core-primitives@Number"},"reason":{"component":"data2-core-primitives@String","params":{}},"description":{"component":"data2-core-primitives@String","params":{}},"policy_accepted":{"params":{},"component":"data2-core-primitives@Boolean"},"lastScreen":{"component":"data2-core-primitives@String","params":{}},"navigation":{"component":"data2-core-primitives@String","params":{"content":"initial   "}},"header_step":{"params":{"content":1},"component":"data2-core-primitives@Number"},"all_orders_loaded":{"params":{},"component":"data2-core-primitives@Boolean"},"resolution_rules":{"onlyPostage":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"onlyVoucher":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"onlyRefund":{"params":{"content":false},"component":"data2-core-primitives@Boolean"}},"history_navigation":[],"came_from_ecom":{"params":{},"component":"data2-core-primitives@Boolean"},"aux_footer":{"params":{},"component":"data2-core-primitives@Boolean"},"start_transition":{"params":{"content":false},"component":"data2-core-primitives@Boolean"},"result_analisys_steb":[{"key":{"component":"data2-core-primitives@String","params":{"content":"recebido por"}},"value":{"component":"data2-core-primitives@String","params":{"content":"FERNANDO MAGNO COSTA MOREIRA"}}},{"key":{"component":"data2-core-primitives@String","params":{"content":"data e hora"}},"value":{"component":"data2-core-primitives@String","params":{"content":"22/05/2026 - 14:17"}}},{"key":{"component":"data2-core-primitives@String","params":{"content":"parentesco"}},"value":{"component":"data2-core-primitives@String","params":{"content":"próprio"}}},{"key":{"component":"data2-core-primitives@String","params":{"content":"número do pedido"}},"value":{"component":"data2-core-primitives@String","params":{"content":"52310963"}}}],"loading_orders_visible":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"status_modal_aviso_cancelamento":{"component":"data2-core-primitives@String","params":{}},"cancelled_orders":[],"selected_order":[],"active_search":{"params":{},"component":"data2-core-primitives@Boolean"},"other_reason":{"component":"data2-core-primitives@String","params":{}},"pending_api_request":{"component":"data2-core-primitives@Boolean","params":{}},"occurrence_id":{"component":"data2-core-primitives@String","params":{"content":""}},"payment_method":{"description":{"component":"data2-core-primitives@String","params":{"content":"CREDIT_CARD"}},"id":{"component":"data2-core-primitives@String","params":{"content":"6"}}},"paymentType":{"component":"data2-core-primitives@String","params":{}},"bank_info":{"bank_name":{"component":"data2-core-primitives@String","params":{"content":""}},"branch_number":{"component":"data2-core-primitives@String","params":{}},"account_number":{"component":"data2-core-primitives@String","params":{}},"cpf_number":{"component":"data2-core-primitives@String","params":{}},"pix_key":{"component":"data2-core-primitives@String","params":{}}},"occurrence":{"userCode":{"component":"data2-core-primitives@String","params":{"content":"702272125 "}},"flow":{"component":"data2-core-primitives@String","params":{"content":"cancelamento"}},"person_data":{"customerPersonNumber":{"component":"data2-core-primitives@String","params":{"content":"702226955"}},"customerNo":{"component":"data2-core-primitives@String","params":{"content":"702272125"}},"contactrole":{"component":"data2-core-primitives@String","params":{"content":"3"}}},"selected_order":{"order_no":{"component":"data2-core-primitives@String","params":{"content":""}},"order_total":{"component":"data2-core-primitives@Number","params":{"content":""}},"creationDate":{"component":"data2-core-primitives@String","params":{"content":""}},"itemsQuantity":{"component":"data2-core-primitives@Number","params":{"content":""}}},"client_with_order":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"email":{"component":"data2-core-primitives@String","params":{"content":""}},"cellphone_number":{"component":"data2-core-primitives@String","params":{"content":""}},"reason":{"component":"data2-core-primitives@String","params":{"content":""}},"description":{"component":"data2-core-primitives@String","params":{"content":""}},"primeiracategoriacod":{"component":"data2-core-primitives@String","params":{"content":"PC005"}},"caseorigincode":{"params":{"content":100},"component":"data2-core-primitives@Number"},"callcenterteamcode":{"component":"data2-core-primitives@String","params":{"content":"1103"}},"reasoncod":{"component":"data2-core-primitives@String","params":{"content":"PC005-SC0399-M0352"}},"nat_ithasbankaccount":{"component":"data2-core-primitives@Boolean","params":{}},"segundacategoriacod":{"component":"data2-core-primitives@String","params":{"content":"PC005-SC0399"}},"paymentId":{"component":"data2-core-primitives@String","params":{}}},"client_with_order":{"component":"data2-core-primitives@String","params":{}},"searchWord":{"component":"data2-core-primitives@String","params":{"content":""}},"infoUser":{"component":"data2-core-primitives@String","params":{}},"selected_products_exchange":[]}},"component":"data2-core-types@FieldSet"}},"reference":"0m2nXSHsvhtXAnyJbd4cT"},{"_id":"66d9ef1e7fae1bf9a82fd6ff","component":"data2-core@Store","system":"6671e96b001009d97f222f73","params":{"initialData":{"id":{"component":"data2-core-primitives@String","params":{}},"session":{"component":"data2-core-primitives@String","params":{}}}},"reference":"XYJbrrt9WXGWPqC0jb84k"},{"_id":"66f2cf9483706454f7713074","component":"data2-core@Store","system":"6671e96b001009d97f222f73","params":{"initialData":{"params":{"fields":{"env":{"params":{"defaultCase":{"component":"data2-core-primitives@String","params":{"content":"PRD"}},"expression":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"appDomain"}}}},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"webchat-homolog"}},"value":{"component":"data2-core-primitives@String","params":{"content":"HML"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"webchat-avon-homolog"}},"value":{"component":"data2-core-primitives@String","params":{"content":"HML"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"webchat-avon-homolog"}},"value":{"component":"data2-core-primitives@String","params":{"content":"HML"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"troca-app-fluxo"}},"value":{"component":"data2-core-primitives@String","params":{"content":"HML"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"webchat-dev"}},"value":{"component":"data2-core-primitives@String","params":{"content":"DEV"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"webchat-avon-dev"}},"value":{"component":"data2-core-primitives@String","params":{"content":"DEV"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"webchat-minha-loja-hml"}},"value":{"component":"data2-core-primitives@String","params":{"content":"HML"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"portal-agentes-cf-hml"}},"value":{"component":"data2-core-primitives@String","params":{"content":"HML"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"ai-portal-hml"}},"value":{"component":"data2-core-primitives@String","params":{"content":"HML"}}}}]},"component":"data2-core-flow@Switch"},"country":{"params":{"expression":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"params":{"content":true},"component":"data2-core-primitives@Boolean"},"value":{"component":"data2-core-primitives@String","params":{"content":"BR"}}}}],"defaultCase":{"component":"data2-core-primitives@String","params":{"content":"BR"}}},"component":"data2-core-flow@Switch"}}},"component":"data2-core-types@FieldSet"}},"reference":"HvjTXvK5P2jzTnoh81tCB"},{"_id":"66f2cf9483706454f771304a","component":"data2-core@Store","system":"6671e96b001009d97f222f73","params":{"initialData":{"params":{"fields":{"input":{"component":"data2-core-primitives@String","params":{}}}},"component":"data2-core-types@FieldSet"}},"reference":"4VY4FzGmgLCdrDJfZd35R"},{"_id":"690a0d322c64cbb7f4925982","component":"data2-core@Store","system":"6671e96b001009d97f222f73","params":{"initialData":{"params":{"fields":{"email":{"component":"data2-core-primitives@String","params":{"content":""}},"senha":{"component":"data2-core-primitives@String","params":{"content":""}},"siteId":{"params":{"expression":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"yvrRDnZw6fGfy_7ailFxP"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.brand_selected"}}}},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"Natura"}},"value":{"component":"data2-core-primitives@String","params":{"content":"NatBrazil"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"Avon"}},"value":{"component":"data2-core-primitives@String","params":{"content":"AvonBrazil"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"Minha Loja"}},"value":{"component":"data2-core-primitives@String","params":{"content":"CBBrazil"}}}}]},"component":"data2-core-flow@Switch"},"brand_url":{"params":{"expression":{"component":"data2-core-types@Getter","params":{"module":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"yvrRDnZw6fGfy_7ailFxP"}},"prop":{"component":"data2-core-primitives@String","params":{"content":"data.brand_selected"}}}},"cases":[{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"Natura"}},"value":{"component":"data2-core-primitives@String","params":{"content":"https://webchat.natura-br-cf.data2.app/"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"Avon"}},"value":{"component":"data2-core-primitives@String","params":{"content":"https://webchat-avon.natura-br-cf.data2.app/"}}}},{"component":"data2-core-flow@Case","params":{"expression":{"component":"data2-core-primitives@String","params":{"content":"Minha Loja"}},"value":{"component":"data2-core-primitives@String","params":{"content":"https://webchat-minha-loja.natura-br-cf.data2.app/"}}}}]},"component":"data2-core-flow@Switch"},"brand_selected":{"component":"data2-core-primitives@String","params":{"content":"Natura"}},"brand_option":[{"component":"data2-core-primitives@String","params":{"content":"Natura"}},{"component":"data2-core-primitives@String","params":{"content":"Avon"}},{"component":"data2-core-primitives@String","params":{"content":"Minha Loja"}}],"mock_fields":{"store":{"component":"data2-core-primitives@String","params":{}},"prop":{"component":"data2-core-primitives@String","params":{}},"value":{"component":"data2-core-primitives@String","params":{}}}}},"component":"data2-core-types@FieldSet"}},"reference":"yvrRDnZw6fGfy_7ailFxP"},{"_id":"6af2292078236f6d277a13fd","component":"data2-core@DataSet","system":"6671e96b001009d97f222f73","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"r4oehJcseoCIPn_fYVWg3"}}},"reference":"r4oehJcseoCIPn_fYVWg3DataSet"},{"_id":"6af22921cdb0aa25ad0e09d5","component":"data2-core@DataSet","system":"6671e96b001009d97f222f73","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"hCvHde1zOzxLcT1GWZJrG"}}},"reference":"hCvHde1zOzxLcT1GWZJrGDataSet"},{"_id":"6af229225fcf2681d0e17267","component":"data2-core@DataSet","system":"6671e96b001009d97f222f73","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"xGWFJ6YYPw7_18GmH_ypI"}}},"reference":"xGWFJ6YYPw7_18GmH_ypIDataSet"},{"_id":"6af22e8d29761895aee26f1a","component":"data2-core@DataSet","system":"6671e96b001009d97f222f73","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"tRlbJferL7uuiK4XRB90r"}}},"reference":"tRlbJferL7uuiK4XRB90rDataSet"},{"_id":"6af22e9740f9cd4d1ecceecb","component":"data2-core@AuthenticationStrategy","system":"6671e96b001009d97f222f73","params":{"credentials":[{"key":"email"},{"key":"password"}],"verifier":{"component":"data2-core-types@DataSetReference","params":{"reference":"tRlbJferL7uuiK4XRB90rDataSet"}},"email":"email","password":"password","role":"role","roleDefault":"leitor"},"reference":"Eg1eRV26seZ-YdxhhvYln"},{"_id":"6af22fb178236f6d277a290a","component":"data2-core@DataSet","system":"6671e96b001009d97f222f73","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"hMctQ1zQRf75g-sjb9JML"}}},"reference":"hMctQ1zQRf75g-sjb9JMLDataSet"},{"_id":"6af22922550701d137bd964e","component":"data2-core@DataSet","system":"6671e96b001009d97f222f73","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"IY33jMLn6x8R1eF8xlzlH"}}},"reference":"IY33jMLn6x8R1eF8xlzlHDataSet"},{"_id":"6af22920afec9e7dd0ceff21","component":"data2-core@Data2TableDataSource","system":"6671e96b001009d97f222f73","params":{"table":"r4oehJcseoCIPn_fYVWg3","schema":[{"component":"data2-core-types@Field","params":{"key":"kind","title":"Tipo (frente | papel | pessoa)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"key","title":"Chave","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"name","title":"Nome","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"description","title":"Descrição","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"instructions","title":"Instruções (papel)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"active","title":"Ativo","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"order","title":"Ordem","component":"data2-core-primitives@Number"}}]},"reference":"r4oehJcseoCIPn_fYVWg3"},{"_id":"6af22921a804ce16a6f59361","component":"data2-core@Data2TableDataSource","system":"6671e96b001009d97f222f73","params":{"table":"hCvHde1zOzxLcT1GWZJrG","schema":[{"component":"data2-core-types@Field","params":{"key":"key","title":"Chave (JCFA-NNNN ou CF-NN)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"title","title":"Título","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"url","title":"Link do JIRA","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"jira_text","title":"Texto original do JIRA","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"summary","title":"Resumo","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"acceptance","title":"Critérios de aceite","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"prefix","title":"Frente principal","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"owner","title":"Dev responsável","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"requester","title":"Quem pediu","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"priority","title":"Prioridade","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"due","title":"Prazo","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"status_changed_at","title":"No status desde","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"created_at","title":"Criado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"created_by","title":"Criado por","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"updated_at","title":"Atualizado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_by","title":"Atualizado por","component":"data2-core-primitives@String"}}]},"reference":"hCvHde1zOzxLcT1GWZJrG"},{"_id":"6af22922550701d137bd9649","component":"data2-core@Data2TableDataSource","system":"6671e96b001009d97f222f73","params":{"table":"xGWFJ6YYPw7_18GmH_ypI","schema":[{"component":"data2-core-types@Field","params":{"key":"card_id","title":"ID do card (TRC-07)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"prefix","title":"Frente","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"number","title":"Número","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"demand_key","title":"Demanda","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"title","title":"Título","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"type","title":"Tipo (feature | spike | bug)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"owner","title":"Pessoa dona","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"agent","title":"Papel do agente","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"session","title":"Sessão do Claude","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"approver","title":"Quem aprova","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"description","title":"Descrição","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"acceptance","title":"Critérios de pronto","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"plan","title":"Plano","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"depends_on","title":"Depende de","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"touches","title":"O que vai mexer","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"status","title":"Status","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"status_changed_at","title":"No status desde","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"blocked_reason","title":"Motivo do bloqueio","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"priority","title":"Prioridade","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"due","title":"Prazo","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"result","title":"Resultado","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"changes","title":"Alterações feitas","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"links","title":"Links","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"env","title":"Ambiente","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"app_version","title":"Versão do app","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"created_at","title":"Criado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"created_by","title":"Criado por","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"updated_at","title":"Atualizado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_by","title":"Atualizado por","component":"data2-core-primitives@String"}}]},"reference":"xGWFJ6YYPw7_18GmH_ypI"},{"_id":"6af22e8d29761895aee26f15","component":"data2-core@Data2TableDataSource","system":"6671e96b001009d97f222f73","params":{"table":"tRlbJferL7uuiK4XRB90r","schema":[{"component":"data2-core-types@Field","params":{"key":"email","title":"E-mail","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"password","title":"Senha","component":"data2-core-types@Password"}},{"component":"data2-core-types@Field","params":{"key":"person","title":"Pessoa (nome no catálogo)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"role","title":"Papel (admin | editor | leitor | bloqueado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"must_change_password","title":"Precisa trocar a senha","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"created_at","title":"Criado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_at","title":"Atualizado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_by","title":"Atualizado por","component":"data2-core-primitives@String"}}]},"reference":"tRlbJferL7uuiK4XRB90r"},{"_id":"6af22fb0e88217babb7363ff","component":"data2-core@Data2TableDataSource","system":"6671e96b001009d97f222f73","params":{"table":"hMctQ1zQRf75g-sjb9JML","schema":[{"component":"data2-core-types@Field","params":{"key":"email","title":"E-mail (igual ao login)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"person","title":"Pessoa (nome no catálogo)","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"role","title":"Papel (admin | editor | leitor | bloqueado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"must_change_password","title":"Precisa trocar a senha","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"created_at","title":"Criado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_at","title":"Atualizado em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_by","title":"Atualizado por","component":"data2-core-primitives@String"}}]},"reference":"hMctQ1zQRf75g-sjb9JML"},{"_id":"6af22922be0499b9a9284d78","component":"data2-core@Data2TableDataSource","system":"6671e96b001009d97f222f73","params":{"table":"IY33jMLn6x8R1eF8xlzlH","schema":[{"component":"data2-core-types@Field","params":{"key":"card_id","title":"Card","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"demand_key","title":"Demanda","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"type","title":"Tipo do evento","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"from_person","title":"De (pessoa)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"from_agent","title":"De (papel)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"to","title":"Para","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"text","title":"Texto","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"data","title":"Dados","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"resolved","title":"Resolvido","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"resolved_by","title":"Resolvido por","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"resolved_at","title":"Resolvido em","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"created_at","title":"Criado em","component":"data2-core-primitives@Date"}}]},"reference":"IY33jMLn6x8R1eF8xlzlH"}]}},{"_id":"5f1600b70528b92601890f75","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ServiceComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ServiceComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:20.208Z","createdAt":"2020-07-20T20:38:15.436Z"},{"_id":"5f1602190528b92601890f7d","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"pagination":{"size":40000},"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ComponentInstancesTableDataSource"}},"sort":{"_id":1},"pk":"_id","autoEdit":true,"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":{"params":{"allowGet":{"component":"data2-core-primitives@Boolean","params":{"content":true}}},"component":"data2@hkXy-UhMjhyfptdE6_fpW"}},"reference":"ComponentInstancesDataSet","updatedAt":"2025-11-06T21:07:07.089Z","createdAt":"2020-07-20T20:44:09.350Z"},{"_id":"5f16017f0528b92601890f7a","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"clientApplicationDomain"}}},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":[{"component":"data2-core-auth@PublicPolicy","params":{"reason":"Explicit so data2 can go private, 2026-11-05. Access is decided by the drive/file gate (applyFileTableGates, docs/drive-file-access.md), which runs after this policy; same behavior as today."}}]},"reference":"FileComponentInstancesDataSet","updatedAt":"2026-11-05T17:23:34.217Z","createdAt":"2020-07-20T20:41:35.797Z"},{"_id":"5f1601990528b92601890f7b","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ApplicationsTableDataSource"}},"pk":"_id","autoEdit":{"component":"data2-core-primitives@Boolean","params":{"content":true}},"master":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"params":{"column":"_id"},"component":"data2-core-types@DataSetColumnReference"}},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}}},"reference":"ApplicationsDataSet","updatedAt":"2022-12-13T21:51:13.995Z","createdAt":"2020-07-20T20:42:01.657Z"},{"_id":"60ddc1bbda3861ca65407c5a","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"domain":{"component":"data2-core-types@Reference","params":{"reference":"clientApplicationDomain"}}},"pagination":{"size":1},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"policies":[{"component":"data2-core-auth@PublicPolicy","params":{"reason":"Explicit so data2 can go private, 2026-11-05. Access is decided by the drive/file gate (applyFileTableGates, docs/drive-file-access.md), which runs after this policy; same behavior as today."}}]},"reference":"OneFileComponentInstancesDataSet","updatedAt":"2026-11-05T17:23:34.254Z","createdAt":"2020-07-20T20:41:35.797Z"},{"_id":"63ee3d38b8c9838c62fb05fb","displayName":"DriveFileComponentInstancesDataSet","component":"data2-core@DataSet","system":"5e3b796d6228227ca637d0f4","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"FileComponentInstancesTableDataSource"}},"pk":"_id","autoEdit":true,"filter":{"component":{"component":"data2-core-primitives@String","params":{"content":"data2-core@Drive"}}},"master":{"params":{"dataset":{"component":"data2-core-types@DataSetReference","params":{"reference":"SystemsDataSet"}},"localField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"system"}},"masterField":{"component":"data2-core-types@DataSetColumnReference","params":{"column":"_id"}}},"component":"data2-core-types@DataSetMaster"},"autoSelect":{"component":"data2-core-primitives@Boolean","params":{"content":false}},"policies":[{"component":"data2-core-auth@PublicPolicy","params":{"reason":"Explicit so data2 can go private, 2026-11-05. Access is decided by the drive/file gate (applyFileTableGates, docs/drive-file-access.md), which runs after this policy; same behavior as today."}}]},"reference":"DriveFileComponentInstancesDataSet","updatedAt":"2026-11-05T17:23:34.347Z","createdAt":"2023-02-16T14:27:07.797Z"},{"reference":"ServiceComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"68c02569d7bab408f9e6760f","component":"data2-rush@HTTPListener","reference":"EJcm4dixYpPvhK93wgv1_","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6aee4d06fad744a64a97b212"},{"_id":"6732513a99b7fd075a213cdf","component":"data2-rush@HTTPListener","reference":"Uy7fAj-Z5K9KzoFtDlRv1","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"68af307b51b2ed7df026e4d7","component":"data2-rush@HTTPListener","reference":"saVh40dkmh1Dhs1SpUd5f","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"676f0f2ad2b3cca7790ebd47","component":"data2-rush@HTTPListener","reference":"sHZn4e-P3j9-Sc3lakR0V","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"671ecd0e61586bfd0327f69c","component":"data2-rush@HTTPListener","reference":"1w3zqjjfQ5QyW8RuXuCLM","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"6a5a76866b6ce34b757739b9","component":"data2-rush@HTTPListener","reference":"8EZAJ3XAA-cB12IYhuL7E","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"68af2f2d51b2ed7df026e4d3","component":"data2-rush@HTTPListener","reference":"8zgahkmrXYHpVz986MVOl","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"6823b288d251610a8d9b245b","component":"data2-rush@HTTPListener","reference":"oFn2XLZPrGJKG20e_yU00","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"6863f8230cd5bab616ffc435","component":"data2-rush@HTTPListener","reference":"9mvmsexdX8cNL8l5gc3Ab","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"689b523ae43ead0988720a17","component":"data2-rush@HTTPListener","reference":"lqEexrZg_qRns_YrpnNth","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"68b9f328b789971775669fad","component":"data2-rush@HTTPListener","reference":"RCozTrwyQLspSwcAYhiZ6","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"6859c614074517d6d190ffe1","component":"data2-rush@HTTPListener","reference":"iCUHDLHYCOE_K8EM0vhOl","system":"6671e96b001009d97f222f73","params":{},"versioning":"enabled","bundleId":"6a84d7fd1dc1455478cd41b7"},{"_id":"6af22aad5fcf2681d0e177dd","component":"data2-rush@HTTPListener","reference":"le1ED3ML02nCGBCq1jhRP","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af229b2e88217babb734fa1","component":"data2-rush@HTTPListener","reference":"l2x8cq1Ba9JV5fzjlgiUN","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22a09d5edd50249266ea8","component":"data2-rush@HTTPListener","reference":"d8UkM-ZJOPWOWkZNx5Pry","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22a9178236f6d277a18d1","component":"data2-rush@HTTPListener","reference":"7ukxnQTL4Yjr3LSixfEWk","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22a36e88217babb735184","component":"data2-rush@HTTPListener","reference":"VKOHGMiGy7rRhU6FDsFvZ","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22a5540f9cd4d1ecce0ed","component":"data2-rush@HTTPListener","reference":"5Si_x8rE-PiUh-IdlwBMz","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22a8e40f9cd4d1ecce1ae","component":"data2-rush@HTTPListener","reference":"f3RXndAJjSpfYOFifjTkK","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22983e88217babb734ee9","component":"data2-rush@HTTPListener","reference":"cy2BrvIvCa89mk9J_nqkR","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22a94be0499b9a9285260","component":"data2-rush@HTTPListener","reference":"_B1ZiptjVMKVQK3dNcSK7","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af23110afec9e7dd0cf1bb0","component":"data2-rush@HTTPListener","reference":"4gnGRN5tFeuBicawv04U9","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af23114d5edd5024926863f","component":"data2-rush@HTTPListener","reference":"MwJgzXAkBEBWRFfpwvcF7","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22ecd40f9cd4d1eccefe7","component":"data2-rush@HTTPListener","reference":"gHlaiRCi-O7vR0uLOCT6w","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22eca40f9cd4d1eccefc7","component":"data2-rush@HTTPListener","reference":"RKSAmkvOcbm9Kcx9Hriiw","system":"6671e96b001009d97f222f73","params":{}},{"_id":"6af22ec6550701d137bda9fb","component":"data2-rush@HTTPListener","reference":"06HyhvqG4jXD6YoDn68Fw","system":"6671e96b001009d97f222f73","params":{}}]}},{"reference":"ComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6af23256afec9e7dd0cf2023","component":"data2-core-primitives@Artboard","system":"6671e96b001009d97f222f73","displayName":"Kanban br-cf - Board","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Kanban br-cf - Board"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1440,"height":900}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"eQFAy9JsAyKtLok4EyX3A"},{"_id":"1bf52f565d1e22590bd0b038","parent":"6af23256afec9e7dd0cf2023","component":"natura-br-cf@kanban_mcp_app","system":"6671e96b001009d97f222f73","displayName":"kanban app","params":{"auth":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"Eg1eRV26seZ-YdxhhvYln"}},"rQuadro":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"4gnGRN5tFeuBicawv04U9"}},"rCard":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"MwJgzXAkBEBWRFfpwvcF7"}},"rEu":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"gHlaiRCi-O7vR0uLOCT6w"}},"rSenhaOk":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"RKSAmkvOcbm9Kcx9Hriiw"}},"rCriar":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"d8UkM-ZJOPWOWkZNx5Pry"}},"rAtualizar":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"7ukxnQTL4Yjr3LSixfEWk"}},"rPegar":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"VKOHGMiGy7rRhU6FDsFvZ"}},"rMover":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"5Si_x8rE-PiUh-IdlwBMz"}},"rEvento":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"cy2BrvIvCa89mk9J_nqkR"}},"rResolver":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"_B1ZiptjVMKVQK3dNcSK7"}},"rDemanda":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"l2x8cq1Ba9JV5fzjlgiUN"}},"rUsuario":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"06HyhvqG4jXD6YoDn68Fw"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"freeTransform":true,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1440,"height":900}}}},"reference":"WaHZKJ2YyJT3Su9IjShIK"}]}},{"_id":"5fc4faeec8da12be8bf3d08a","component":"data2-core@Data2TableDataSource","system":"5e3b796d6228227ca637d0f4","params":{"paginate":{"default":1000},"super":{},"table":"fileComponentInstances","schema":[{"component":"data2-core-types@Field","params":{"key":"component","title":"Component","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"system","title":"System","component":"data2-core-primitives@ObjectId"}},{"component":"data2-core-types@Field","params":{"key":"displayName","title":"Display Name","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"params","title":"Params","component":"data2-core-primitives@Object"}},{"component":"data2-core-types@Field","params":{"key":"reference","title":"Reference","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":{"component":"data2-core-primitives@String","params":{"content":"fileId"}},"title":"File ID","component":"data2-core-primitives@ObjectId"},"break":true}],"hooks":{"before":{"all":[{"hook":"softDelete"}],"find":[{"hook":"useCache","options":{"additionalGroupKeys":["$and.0.system","$and.1.system"],"skipCache":true}}],"create":[{"hook":"timestamp","options":{"fields":["createdAt","updatedAt"]}}],"update":[{"hook":"timestamp","options":{"fields":["updatedAt"]}}],"patch":[{"hook":"timestamp","options":{"fields":["updatedAt"]}}]},"after":{"all":[{"hook":"fileUrl","options":{"downloadField":"params.fileUrl","uploadField":"params.uploadFileUrl","enhanceSchema":false}}],"find":[{"hook":"useCache","options":{"additionalGroupKeys":["$and.0.system","$and.1.system"],"skipCache":true}}],"create":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}],"update":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}],"patch":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}],"remove":[{"hook":"invalidateCache","options":{"additionalGroupKeys":["system"],"secondaryUniqueKeys":["reference"]}},{"hook":"invalidateEnvCache"}]}},"multi":["create"],"useEstimatedDocumentCount":true},"reference":"FileComponentInstancesTableDataSource","updatedAt":"2022-12-19T20:42:51.837Z","createdAt":"2020-11-30T13:59:52.294Z"},{"reference":"ApplicationsTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6af23274d5edd50249268ac1","displayName":"📋 Kanban br-cf","reference":"IDxPtFWNuv0_akaf4fqXK","params":{"domain":{"component":"data2-core-primitives@String","params":{"content":"kanban-time"}},"entryPoint":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Artboard","reference":"eQFAy9JsAyKtLok4EyX3A"}}},"system":"6671e96b001009d97f222f73","component":"data2-core-primitives@Application"}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}