{"context":{"domain":"lucas-playground"},"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":"67850da249571825a6d31832","name":"Lucas Barduco","domain":"lucas-playground","visibility":"public","settings":{"defaultAccessPolicy":"private"},"updatedAt":"2026-08-14T19:26:29.927Z"}]}},{"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":"6a83bd00e42011de7361a9d1","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"portalConsumo","package":"lucas-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// portalConsumo — consumo diário de recursos × sessões (Natura AVI)\n// Fontes: rush listener em lucas-playground (server-side) -> APIs de sessões e de consumo.\n// Regras de agregação e preços verificados contra a fatura de jul/26 (17/08/2026) e a de ago/26 (02/10/2026).\n\nconst LABELS = {\"-eMWJhD77iWbu9BslwXWP\":\"Contactos\",\"mVPZutoImEnKU6FPY8AF9\":\"New Data2TableDataSource (403)\",\"X5sS5FKuL_lktm807EG4R\":\"Copy of Contactos\",\"AGil7FxlmYES7_daXq0Yl\":\"New Data2TableDataSource (403)\",\"PagghRvpTiJYHSADx-Bqy\":\"New Data2TableDataSource (403)\",\"8QQNpXlufnvwA6y81Ptnk\":\"Contactos\",\"Jla2eUruuoa1LHAA5hE15\":\"Copy of Contactos\",\"Z4u2iV9yCMcQaYPlcwmDu\":\"tabla_test_errores\",\"GgDVarJVjK7kIkuo2ejwY\":\"Fechas_V1\",\"Xodw-uMJHOoo5T696NTn1\":\"New Data2TableDataSource (391)\",\"EqdMP8kNMsihDBIyTdWjK\":\"New Data2TableDataSource (403)\",\"PjTXTASW_1PJ46XDpfl9I\":\"New Data2TableDataSource (404)\",\"2T8lU2qKznBDUgl2L75VV\":\"Copy of Grupos\",\"f7T1PBCCMfEw-2q_EveHS\":\"Grupos\",\"EjkzVPO8aGOdMDm_LMcuL\":\"mensagens-zap\",\"wfUBk4a9nJKWLXvvenufg\":\"mensagens-zap\",\"BeuKCiw5NRuCGXqZMPhDc\":\"mensagens-zap\",\"bv5B0TwbvCSaeFHRcZCk5\":\"mensagens-zap\",\"Djl5PO0AA-A1VTvV09va8\":\"messages\",\"QvzYxVsqo0Rn_4NbLzUBI\":\"messages\",\"e4VJLkg9oL8PoNuN6k0Xz\":\"messages\",\"gv6tKeiwDpkLbsu9YlP0g\":\"Domínios\",\"gtuHlDZx1TQxLYcIvmI-Y\":\"Domínios\",\"FCRaa2xYnG-kfDI0Wbd2B\":\"atendimento_exemplo\",\"7eNXvpzWWA477bKIwPlq5\":\"atendimento_exemplo\",\"7-Kc_WfrOD-mQYO56Eiig\":\"regions\",\"sf62YbmAqEN36vphSMYE-\":\"regions\",\"51a_7nsaHRAtC42epq4Z0\":\"regions\",\"Xo1l3vBp_pB74VXCrT8yR\":\"useCases\",\"GAbOgAomS_h6HYNzMLy7L\":\"useCases\",\"0tM4Fz_rXCpHqi35nnSMY\":\"useCases\",\"zbvIy-5JbobFduGnTxumf\":\"useCases\",\"5M23Z1KfrPg07zEMNxSeI\":\"useCases\",\"d1pELeE8QZBtT04dCQ9wn\":\"useCases\",\"xZlxA3hlzH2tZoZJj1Hhr\":\"useCases\",\"6Mj-04KjCJmpKjoScafy5\":\"useCases\",\"49gwabxHRaQVQLFHJEo45\":\"useCases\",\"-qaMIuMz25iKUtXjPpozC\":\"useCases\",\"1M4RTkZl-N_8yOmZuNYH4\":\"usuarios\",\"DmQuM6oU6VYfeAoYC7bd0\":\"usuarios\",\"v1X-i9Ln6_tuv0R2XlSJ4\":\"usuarios\",\"0-_TObRRNMYQ2nFk-3qba\":\"Channels\",\"qN09iQTkTp7gopSyo4sjh\":\"Channels\",\"VQDwhh0gyuxY7CWMNlSC1\":\"compliance-checks\",\"X7yDiCSQyDL7rd5MexBgD\":\"compliance-checks\",\"Xg9PzioBsLbGijNqJ7QOq\":\"useCasesRegionExceptions\",\"I8uHiRQEEROX6zcV2Nryn\":\"useCasesRegionExceptions\",\"vJF6hgnBS7wyhqGy1cUAF\":\"useCasesRegionExceptions\",\"6Ocrxl6WqjioPrhNjF7XN\":\"useCasesRegionExceptions\",\"LWm0jxJUHtdFUfJ8XbtoU\":\"useCasesRegionExceptions\",\"EkLtoMcIvF-qIJ6Y-2HW-\":\"Example_useCase_info\",\"RmnRc6OpI89iAw3s09lm9\":\"Example_useCase_info\",\"TSM9kXKVs7-uDdNhAvlpt\":\"Example_useCase_info\",\"l1CdDCNCulMarka8qdd4Z\":\"test-case\",\"xoh9KYHLWrKIiB1gvZFyA\":\"test-case\",\"6Xf7hH_NqzWbKKmqeA0Cw\":\"test-case\",\"MYSWyJhYaWHrautfemsFK\":\"faqs\",\"JJdfsBfraSbfsHjLHIYOS\":\"faqs\",\"SFMbd_CNcxXDu3ko0a9z6\":\"faqs\",\"AA3uk7AskKMLKrGSfaVPM\":\"faqs\",\"TEjzNRc8J1DkpUSW2OoE1\":\"faqs\",\"BNGc_wWdSweyUPwsrv6E9\":\"faqs\",\"nODO90jdhicNzqdDLyD1f\":\"faqs\",\"upzDI34_r_FF4AJsBRAVI\":\"faqs\",\"Ol2BtLlQwjj2jFAWKwoYS\":\"faqs\",\"to58eOvqv5VD7V0BRatb9\":\"faqs\",\"9s1bq3WIsLgVx69uqISTz\":\"faqs\",\"6uTW9_Q1F_vlN6JaNKI5d\":\"faqs\",\"4yocmcfIT8NhhvhlBpfTi\":\"faqs\",\"SNvadrvyhJbPtxrv1qf8a\":\"faqs\",\"S8-O8Rhic-NB5M0IwZV-g\":\"faqs\",\"sBGA6UFP-KM4k42j7I6dt\":\"faqs\",\"goZAYeQSxwm7boqHlWFMg\":\"faqs\",\"woL64f-2KLy4Jx_2DOA6F\":\"faqs\",\"iYg4Vp5jr1DbbGrq1I269\":\"faqs\",\"USuhwwd4rIwWeAPwTB5TN\":\"faqs\",\"cLBugrXi5ts4EIuhroo5l\":\"faqs\",\"BCcvqoQlbSWc3-p_v-bdR\":\"faqs\",\"iZ6B0kDmsHLq_p-n6-jHA\":\"Conversation_cases\",\"ssdxsYMLLsXNtm5a2AYpl\":\"Conversation_cases\",\"8rPLY7MQSF2OKcMPp4rzm\":\"FAQ_Categories\",\"832myopBDXKYvuwGk_xRR\":\"FAQ_Categories\",\"2LLbal26K-tA1BRiaXY1O\":\"Variables\",\"aRieAJX30OJ-Bo-KW9Z5_\":\"Variables\",\"wbcVL8jbDJpta49Q7gLVS\":\"Casas de Cobrança - Sector\",\"YOxvBArm0GEn7FXdWRu2C\":\"Casas de Cobrança - Sector\",\"xBPbE7PXNGdyLBhYGAtqF\":\"Casas de Cobrança - Sector\",\"t7OsXgOpv_yOMuF8kOzRx\":\"Problemas-Reportado\",\"DrYMevjeCBEItuYfPUP7N\":\"Problemas-Reportado\",\"zPJ4TRpmk5YCJa-uToldx\":\"restrictions\",\"9mBfaU-zv0KhxnEUsbAC9\":\"restrictions\",\"jyicaYbxJfeHM7FKhzbKq\":\"analytics data points\",\"O3MUAVBuxfNaqwbIdKpsg\":\"analytics data points\",\"nCvWfH35myVYGRtV7fXA6\":\"analytics data points\",\"1TWn2_dt_9dxJWcmjr_Iz\":\"analytics data points\",\"O355rxCl9pTKYe67IFHe6\":\"analytics data points\",\"3tPtmxHGpA3TS5wLzFU8S\":\"analytics data points\",\"MLb8xx0gWt4Jq4vUg0wjF\":\"analytics data points\",\"dOYnXsUilNM5JPhVe1n01\":\"analytics data points\",\"j0xMxmtTCStxfFNWTd_HS\":\"analytics data points\",\"c9nrcLZKhpVegAlknQHln\":\"analytics data points\",\"eCFintPLBUhhAac1ZNSJT\":\"analytics data points\",\"v9Qn7UOkSkqz3IGkT4BWL\":\"analytics data points\",\"t-GSSwMY0sAN_YvvGpyZa\":\"analytics data points\",\"CnSk9qDeesed9nfrJBIIn\":\"analytics data points\",\"tKcVN0D5C67Y_60dRIBPn\":\"analytics data points\",\"sMZsvP486mxtZjYlUfLzW\":\"analytics data points\",\"JmWLuuCOW9deran97587K\":\"analytics data points\",\"e4vhKLp9BNShkk3iVda5Y\":\"analytics data points\",\"FwnA-K06Ls1PaEiZJbVgl\":\"analytics data points\",\"5eF7eJ55DyPrlN4q_WqPo\":\"analytics data points\",\"yX6DZYhTh6tXKI8y-iBAK\":\"analytics data points\",\"ffhu-FEyw5S3CnVfbWA5j\":\"analytics data points\",\"UeWNehWJ-C6OonoPwt3Lr\":\"analytics data points\",\"GVgI9ncCN1fvW8AYWg_7x\":\"analytics data points\",\"8PtmIpG-azGIS88kW4tjJ\":\"analytics data points\",\"zls_MMzKo4EDZTFzTrqp0\":\"analytics data points\",\"R60GQ6W1dJvomQBhg7QsW\":\"analytics data points\",\"_jpPKNL3Y6pfWUCx87Aaa\":\"analytics data points\",\"1wZGSCnnDbh9qp0Cz8sp-\":\"analytics data points\",\"Y-p_A0bvsCu1Zwq1FJI1a\":\"analytics data points\",\"o_6UFG7MSFL-kf9_dkhp3\":\"analytics data points\",\"ScfUoppyudq6A8xfFDkh8\":\"Reports\",\"veaP1I7oI7pQ6GwG-tudL\":\"Reports\",\"d1AFjBrKiLX05H5SElIMo\":\"Reports\",\"--2YzHGz3rUiwJmYhFCFY\":\"Reports\",\"CBTi4vgKQ0gi3tggd42Jz\":\"report-updates\",\"-k4xDNW5DYydh0wZnX3bE\":\"report-updates\",\"uiWT6f7J9YYSO4DvXPeHD\":\"chat_tokens\",\"dW_GWmWKmM2pT4H7A75e6\":\"chat_tokens\",\"T4i9sVBx9n3djR40FJUqT\":\"chat_tokens\",\"DH7DAlMdvHhrg_VLDqeoM\":\"chat_tokens\",\"hJ_zsvaGqPXrh7v6Drgjo\":\"chat_tokens\",\"o16mJt4sZqhrOwfRJzlgT\":\"chat_tokens\",\"thswnHxWWC6R_8yMV3cxR\":\"chat_tokens\",\"q9tSMqOajrZgWBXqKkEhb\":\"chat_tokens\",\"MoLQ3bMEa0NFZG4AyWtlJ\":\"chat_tokens\",\"BVpRak20FfLgt3pWDDlor\":\"chat_tokens\",\"wLgYrXeVHjZmUvrnzu_XR\":\"chat_sessions\",\"ewDp8c4XuPQHsmcZkYlwj\":\"chat_sessions\",\"d392opE0cxvTfYDXN7BMN\":\"chat_sessions\",\"c30byeVZ1vDPsYGqICIus\":\"chat_sessions\",\"lBOgGN_vipoAM7xUtz2Hb\":\"chat_sessions\",\"Yoa1H9P5jpJX-kSCeYCrv\":\"chat_sessions\",\"hDDILdvhm3DC1npwk_1dz\":\"chat_sessions\",\"h_dGPwfINLsizVrLJr2YM\":\"chat_sessions\",\"o8TGF4MrioTKsWDB2wkxt\":\"chat_sessions\",\"Gfo953M5TBGwrHJA1i5Io\":\"chat_sessions\",\"_Aw1gcDGuBmThkphMazhd\":\"chat_sessions\",\"QtV1SXdDZhZVgVw_C8rzs\":\"chat_sessions\",\"UEJB0ijlYPkzTp-3gUC9s\":\"chat_sessions\",\"MFmhRq5WhtXF8IlHpEP9j\":\"chat_sessions\",\"BWGFfUd4YiwwgFaUQF-sE\":\"chat_sessions\",\"A3bf21f04TogU48mbxZ3H\":\"chat_sessions\",\"QBZYUIPLFYAMn7d-Ilb-o\":\"chat_sessions\",\"t1-MFjGZYqUgrT8tw5Vak\":\"chat_sessions\",\"mLv6-FKu5m-e1kJl10KUR\":\"chat_sessions\",\"E8vWweuH4Y_l688kji_w9\":\"chat_sessions\",\"ttrR4NXNYDYWkdujKhttX\":\"chat_sessions\",\"_fBQhK3dxp-RprtzNXNza\":\"chat_sessions\",\"P3bB5iiz6COl0xy-nV_7P\":\"chat_sessions\",\"1oD92fx8HQJCuxvhkHcPk\":\"chat_sessions\",\"o8D31gej5btSGhjk2xguL\":\"chat_sessions\",\"YsbAvzNxtC3FMbW1_9kom\":\"chat_sessions\",\"nqeOZQrr-vawqUYSOkomC\":\"chat_sessions\",\"AkhiUzBAqv2l2IVIgpfsZ\":\"chat_sessions\",\"cmtVtu2mgURKIAzM0mbH_\":\"chat_sessions\",\"h2XjzeAQVWfxx2d00Q2Dm\":\"chat_sessions\",\"3RZN7xYLEjKRMYpDUYSkQ\":\"chat_sessions\",\"rUSnTg5t4vsDsDk7SDNj-\":\"chat_sessions\",\"wABLUA27FVILzKjyovGwb\":\"chat_sessions\",\"C_EQniM8bNnKa3Ywmh94j\":\"chat_sessions\",\"bSA5UPu9y1vPckiweBHRJ\":\"chat_sessions\",\"WOmftcbNfj0VcBk8qm5OC\":\"chat_sessions\",\"X1oG8NaDspvbCfke5m4lo\":\"chat_sessions\",\"a4hTdsIb0WBNjfgPcd9A4\":\"chat_sessions\",\"y5Kdo0npWIa-urYp_FUu8\":\"chat_sessions\",\"rILn30hitJfwUVd1tuXf_\":\"chat_sessions\",\"znpdiDJePvBiaVeFBxg-v\":\"chat_sessions\",\"HlkECips8XwQsodyA4ZNL\":\"chat_sessions\",\"hqRCdZur4FnEezCWXRrVb\":\"chat_sessions\",\"YRyXiJ5QuLXn-4mouRZmj\":\"chat_sessions\",\"Bg4so5NW6ao0YSLxWbaxV\":\"chat_sessions\",\"DWNAeiz3Fwajb7Hoj0GKT\":\"chat_sessions\",\"Q6Wu3QxofVWPbOtf-gVrY\":\"chat_sessions\",\"qCE8si7rHB1kgkEJv-CKT\":\"chat_sessions\",\"jGc8l0e7dTpSJlWR7sSob\":\"chat_sessions\",\"_HzpLg2qLQcMdqHKcJKWn\":\"chat_sessions\",\"LEBTQfreSXMbb_EDGsddQ\":\"chat_sessions\",\"jOv_YJO_MTiJS2M7cTQUV\":\"chat_sessions\",\"DjACE8KEgFUZj9MOSSx55\":\"chat_sessions\",\"5mOb6RjDzzYqYiYyR9E0G\":\"chat_sessions\",\"maes_J4bC6LyPNl6jZV8M\":\"agent_preset_messages\",\"i2_Ikk7v2VErkueUnGmjT\":\"agent_preset_messages\",\"t5BrNx9rlhe_ssbimNZMu\":\"agent_preset_messages\",\"LjY69zNPBf-S4iKWcJzWT\":\"agents_metadata\",\"ePgZOOB5OJJb2EwMuy0zL\":\"agents_metadata\",\"XSNQ2VDjr9Y0SrbPxS64b\":\"agents_metadata\",\"038JgaJHsqchHb4UVo8Ts\":\"agents_metadata\",\"qJrIWRriMclOIcIjPR6Oh\":\"agents_metadata\",\"ZWLqvZfiJpx86uSgJOl2A\":\"agents_metadata\",\"Fg_JJHfaCyQsBlveYWYIt\":\"agents_metadata\",\"fvKZ9V-3Rrmp2GMO4qInI\":\"agents_metadata\",\"yk8Y5tZnAbYjprdWbnvgE\":\"agents_metadata\",\"DsIkzHHECa5Lkyu5dtrbt\":\"agents_metadata\",\"-7xGKooW80Qa4IQlxovHk\":\"agents_metadata\",\"bpsgvRLn_BHAAm3xUB6j5\":\"agents_metadata\",\"d8w2Srgz-eZglmcZrmXau\":\"agents_metadata\",\"4XK1ENuItP0w0n511obF_\":\"agents_metadata\",\"1RQmj18MSuMkFbexypg2l\":\"agents_users\",\"H-MUXWjIIpHirHesYtM5n\":\"agents_users\",\"X7RLKuj6dZS1NYHPa3CP0\":\"agents_users\",\"EtoDSlJryqFOMgZKFxPE4\":\"agents_users\",\"DczyUUhzVvS_OdRZ-Aj90\":\"agents_users\",\"6CsX5RHV5820XHvgKN2id\":\"agents_users\",\"85nx1Kmy05t0lOrKC7KII\":\"agents_users\",\"T1CqwtSu90l1_nWzf-9fy\":\"agents_users\",\"qhFVZRe7NqZMIq1mCVEbg\":\"agents_users\",\"8lIMvY7b1L7I9dt8F6iPQ\":\"agents_users\",\"xE6AjwFLOj-Cjx5B133yt\":\"Agents Tools Users\",\"O86W3TFnJ7ONCsoiQ-pGt\":\"Agents Tools Users\",\"yw4kd73PqfxxryhljpJbH\":\"Agents Tools Users\",\"I1ZpZh5J1pT0JIfboAEo2\":\"Agents Tools Users\",\"-98ria-oVT1GTzK1du2G6\":\"Agents Tools Users\",\"6RPTuvUJd_KaJ1Qu3RHl3\":\"Agents Tools Users\",\"QGbcp9WEGwVUSlDNHYO22\":\"Agents Tools Users\",\"BLj74rYaHbNHW1UCawBh9\":\"Agents Tools Users\",\"9g5J8YXHvTV9w7XMsdcVd\":\"Agents Tools Users\",\"toQmjKeeWX71OPjlhYWiu\":\"Agents Tools Users\",\"dgIzqheJ2J6dHjtaGHZ6V\":\"Agents Tools Users\",\"SLmNv-LV-cJKTjap-64uJ\":\"chat_messages\",\"fDyiaJk_cX8iPAxuDOOB-\":\"chat_messages\",\"3euPAXpP7r3NHYY19q_HG\":\"chat_messages\",\"-jv87k1mdVwI2U5_NSnta\":\"chat_messages\",\"fN8oaDT5PlmKABq5MCgg5\":\"chat_messages\",\"TKTOC6PZTIKV7Ij6R4vBK\":\"chat_messages\",\"LY5ON94w1rXUTZQUvy_Ep\":\"chat_messages\",\"0c3zy9_fyhi6EiFsLxs3b\":\"chat_messages\",\"15ClnZ-EGURTogjRavNcq\":\"chat_messages\",\"jRjm7B6keoEDLWE0jFCrP\":\"chat_messages\",\"4_JX_njAPAo2_0NzA3xNZ\":\"chat_messages\",\"KXccp_XBI8iahirFM9h9E\":\"chat_messages\",\"b-PhlRThK_QDfblaAjfou\":\"chat_messages\",\"hCC8X9S7MLTEa1fez3fyP\":\"chat_messages\",\"mz3UXiIKmZSpvLYDhjAM5\":\"transfer_queues\",\"5lSTi_nVMs8MCfMqZfpOZ\":\"transfer_queues\",\"9AV3DZpifsc5h0hQRWyQi\":\"transfer_queues\",\"nv36O_QFJzRmmxX8uGjhg\":\"transfer_queues\",\"KNesKV3JcCeFSacCuLjae\":\"transfer_queues\",\"pywp7gyljUyLbmbw6iDbn\":\"transfer_queues\",\"L6VNbXSUlmh90DMLCa8q2\":\"transfer_queues\",\"Gx6pd7ZM5N0BIahawtZGw\":\"transfer_queues\",\"Mj2cfCtUmbr_IL9mLk4PZ\":\"transfer_queues\",\"bKj_NjT55d4Fix2jUnQNS\":\"transfer_queues\",\"8xZiXjMNRHdeVSgJgXywV\":\"Modulos-Documentação-Atendimento\",\"Y4yjtKxc1v-M8aemq_3H5\":\"Modulos-Documentação-Atendimento\",\"9E3rBaPzoSSA-UdShhbdg\":\"chat_surveys\",\"z1iQ24G4CTnn5UC_0ZH9w\":\"chat_surveys\",\"qV3mxP0h5cI7q7ELs0HF3\":\"chat_surveys\",\"sCA8hegfELxymLyC9zxgx\":\"chat_surveys\",\"a5cQUKa_Ix0m_t0W-dShd\":\"chat_surveys\",\"Alg2dzf8hAYzwLv4OYtn4\":\"chat_surveys\",\"Nk9qjr4zYtgmKrQ2GBPPA\":\"chat_surveys\",\"Xh6YV_K7bqwTrFwk114Et\":\"chat_surveys\",\"icJL8fNFwj68TGpOtlXaE\":\"chat_surveys\",\"0ZhXO-OloAf0ye_nvIi0Y\":\"chat_surveys\",\"ujDWrMc1jANYrCpKqbRzF\":\"chat_surveys\",\"oCR9E1mUo-MXgzObs8r1Q\":\"chat_surveys\",\"A5MXoXog2BiElCob1D0pU\":\"chat_surveys\",\"KE4umlgc2aV5_vhCRE7QX\":\"chat_surveys\",\"m6oAKN8QOnPSo_Ffc_Z05\":\"chat_surveys\",\"JXQpLx40EsOC-l9B64bc_\":\"chat_surveys\",\"W9bl1veSqEENnH4ROM66Q\":\"chat_surveys\",\"bA5kg89X4mRQhSU0D6Hj0\":\"chat_surveys\",\"68hak57KYxBW1NEvoECcr\":\"chat_surveys\",\"Tk42dVWLxk2QsbhlCvsbk\":\"chat_surveys\",\"5Q_a2YT_TxdkD9_FOqmoj\":\"chat_surveys\",\"xLzG_Kg73bJ6URDA_rDYn\":\"chat_surveys\",\"GSWFMZl8I33Ju2iPmLRT7\":\"chat_surveys\",\"vwtLsLx8BclkyNhkyPJqL\":\"chat_surveys\",\"4_4I_6QKhjnKgf2TtXEp9\":\"chat_surveys\",\"VasSfx7gPoPp5WNakzWKD\":\"chat_surveys\",\"OyxFrynz5V4gqLn2QF7_u\":\"chat_surveys\",\"4fCm30wzuBx1EkS-kkVi2\":\"chat_surveys\",\"-qIEb7sWhxUInJs8mjk2Q\":\"chat_surveys\",\"9OR3fFnhlHdaPEk2jJFXw\":\"chat_surveys\",\"Pl9Xn2aiXv3E3emhIHpFq\":\"chat_surveys\",\"6B9UinarHQKGNh4n8W3eK\":\"chat_surveys\",\"p5LgerVyNyI_qsMtWrDVB\":\"chat_surveys\",\"iLEnF7jaLpxFtnEoGzAmf\":\"chat_surveys\",\"Ucy5rj_KJsjFvyqtg6NY0\":\"Modulos-Documentação-Supervisor-Atendimento\",\"1lwZRZgvL199Dp4KCu27j\":\"Modulos-Documentação-Supervisor-Atendimento\",\"CqSALqg7f_W_s_fzWoo_H\":\"agent_statuses\",\"NqsdtqK2nFDvcc2h1VC_R\":\"agent_statuses\",\"fVRBN-ev_k6BJ8j8amQbn\":\"agent_statuses\",\"H5xtMWnip8C7MqTEKaOZs\":\"chat_tapume_surveys\",\"nMP58rFQ6TKaMIg4NHvoY\":\"chat_tapume_surveys\",\"M6g_FMuTFuc6VSfMcEDGr\":\"chat_tapume_surveys\",\"x9Ij7g0QuquqTW8ZzY0sF\":\"chat_tapume_surveys\",\"3YzsrIyrCZxsU4lUSsV40\":\"chat_transfers\",\"IMuPFgdPnuGBMMJz7gvua\":\"chat_transfers\",\"SJ0XGfl6pGjNIHo8Xxl2v\":\"chat_transfers\",\"ML824TElJ3RyuJaD_dVd9\":\"chat_transfers\",\"28oTESKi05XRJEoWOf017\":\"chat_transfers\",\"3OsPiNI8N0PqYHwI1-p5y\":\"chat_transfers\",\"gC2Vv_5YI5QyZaNkWsMmj\":\"chat_transfers\",\"4iZ9fAXLHR7C4hGZkY7aA\":\"chat_transfers\",\"lmmc6ru9ofYGCVBrWwXF4\":\"chat_transfers\",\"wJERMSPwPN8omdEr3h2DM\":\"chat_transfers\",\"rkBUbOOW2D-RpHZn8Vteh\":\"chat_transfers\",\"GTroAOI8boz8l7D4i9fq1\":\"chat_transfers\",\"bQdn_BJPY5BdCCT1Bc4Az\":\"chat_transfers\",\"ge_5sPCgj7mMqJT_fT4Wr\":\"chat_transfers\",\"UUVlZGiZBDcJreRCAVxgX\":\"chat_transfers\",\"fE5LZh-L724h-LBxktr6y\":\"chat_transfers\",\"K8CvPqcPDJdFxBlPd_XQY\":\"chat_transfers\",\"5gyDA5VyRg3tyMPMG0U9w\":\"chat_transfers\",\"wLJ1a0bSZqIUsREZjTjBO\":\"chat_transfers\",\"qUIdz-sjA7hCv00Dq5dSu\":\"chat_transfers\",\"y9757YYggYbWh_dNrqvXi\":\"chat_transfers\",\"GqPSEAdTJDCSgPOBmxpZk\":\"chat_transfers\",\"QmVUhNC2Ou-CQyekj39y-\":\"chat_transfers\",\"TFcew_mlS8RtpGyW6euyW\":\"chat_transfers\",\"QrYS6PwuV3r-XQhLj3Lry\":\"transfer_settings\",\"yxlb4DMp8ENnyr-nV1--O\":\"transfer_settings\",\"Q9egkm3CNc4ZR_nQn3kkJ\":\"transfer_settings\",\"FMZd-g28py86fWglUczOW\":\"transfer_settings\",\"zIZ3z5GmgMA-RW4kpzOVk\":\"transfer_settings\",\"ihO3aL2sivoJNc2aw9T1n\":\"transfer_settings\",\"Ie_ISguTUHnYVbBWRWxQr\":\"sinch_steps_sessions\",\"KYGAYStTDgxH479kBy3QC\":\"sinch_steps_sessions\",\"MFWTVDitkGbr7ENR-kRwB\":\"sinch_steps_sessions\",\"K6pRAQgJN6tCGhnbdXviY\":\"sinch_steps_sessions\",\"g4BgnFCIf1XoBsIM0t94m\":\"sinch_steps_sessions\",\"04elkD5HncP367-BsmoX9\":\"sinch_steps_sessions\",\"q0iatRk44sLxhNRuAhtT3\":\"sinch_steps_sessions\",\"ojZpd1mnFRZ2g_1pOcfl8\":\"agent_logs\",\"eqkAvHVk6DX2xGRk_KVxS\":\"agent_logs\",\"Ge6i1ZIQhGLEi5Qh9n982\":\"agent_logs\",\"TSKbBE3Qlj4eMUqOJkFn6\":\"agent_logs\",\"shcf7wfyAH2Y4tx78ZqS6\":\"agent_logs\",\"Qq9megeRksFRO1CuMKr3P\":\"agent_timings\",\"eVBYSEqcGrcaiP9bHlpBu\":\"agent_timings\",\"R14fadMIZbhK70MjbuWPU\":\"agent_timings\",\"oJjw__DWcGSsfsLUHtSZO\":\"agent_timings\",\"mkbAgmdYVCL9_EMxYG4Lb\":\"chat_pings\",\"TyRsjgSwJYm6_93_XH5ep\":\"chat_pings\",\"ptNXe02qYQ_LeFI4TqNtL\":\"chat_pings\",\"qq1dgIZncfRiUevPZVv3_\":\"UseCaseSteps\",\"Uci7c8cm8hbW4KDozdKul\":\"UseCaseSteps\",\"3f_YFH-bS6u0eSZYlTEe0\":\"UseCaseSteps\",\"xcVTMBwR26bWQtPCjAVt5\":\"faq_requests\",\"irU0JBxZKauF0x7TpjNvz\":\"faq_requests\",\"WhNxQ0zFHIQApBVj4LyP6\":\"faq_requests\",\"i805VL27Sl_wQKu2yvi3d\":\"faq_requests\",\"BlOsu9b3LEnR5sxfheRJd\":\"faq_requests\",\"tCkTVP0zAVd9dUbeDHrXy\":\"faq_requests\",\"zshfnVjwCZunheJQ7kiIU\":\"FAQ Users\",\"cJ6b-suQNT8TY7MgxDwPk\":\"FAQ Users\",\"gTH5wa-_swRkxfhcoU5QO\":\"FAQ Users\",\"pq1AUuqEA82kInMuwxoQT\":\"faq_request_updates\",\"4h0bK-3oQ-cM4Wlbn23NN\":\"faq_request_updates\",\"5dEAo10N-Ids1GY-xPBxM\":\"faq_request_updates\",\"XY_aXfpV3VAyNPVwzOTRb\":\"faq_request_updates\",\"6EJjSGsWi4yG8g9OarbBl\":\"faq_request_updates\",\"jUn-ddUehtN8SfxgR93ts\":\"faq_request_updates\",\"yz6pQrrR06MhwCgJ6bjFH\":\"FAQ Users Areas\",\"jGmBCtT4_wYfi6a93VYpw\":\"FAQ Users Areas\",\"FvK6UoUuVhCpGMdiLCoZT\":\"Ecuador Admin Users\",\"awmvv5yP7paRzHVrk3BWl\":\"Ecuador Admin Users\",\"dhASLUQLNJNMwVe1-3vtw\":\"Ecuador Admin Users\",\"jEQ9c3OKw6ShpC4M5a_sa\":\"Argentina - Pendencias\",\"-AsBS4q0GVfjL3kewEz6l\":\"Argentina - Pendencias\",\"cnZcJ4tFFZ5kiMk7hbR9O\":\"Copy of perguntas-home\",\"j7NM4WrnDLSTUYDKrt9h5\":\"Copy of perguntas-home\",\"vwEWwHQ7m5weZ4UGicks4\":\"Mario-Message-History\",\"x4fWl_3_IkdysEl6g7A_X\":\"Mario-Message-History\",\"8jiJA5fAvTozWenJpeeHq\":\"Luis-sessions\",\"4gj4QF6JQwlxyKWUl8t03\":\"Luis-sessions\",\"UZwfBvHvjawXCXzmwVCkQ\":\"stefcurrentsessions\",\"LEv-iCZDgzbMKr4kfcgTo\":\"stefcurrentsessions\",\"tOC1l_ZUVLHY7v7jYyK-Y\":\"Luis-Data\",\"YfLuRq9qvhtCIUDoqlb6_\":\"Luis-Data\",\"qicSv-Up3RPA2V2CrCRhK\":\"Juan - useCases\",\"EF1X4AM5EA9uRKTArjTOC\":\"Juan - useCases\",\"ckbKE1fKjiKguj6ILHHl7\":\"codeplus_package_table\",\"xTQLN3uU5v-FRplor6V7v\":\"codeplus_package_table\",\"o4aM6vX9NeoMtCuhasmb0\":\"mx-beedoo-faqs (Old)\",\"whHxn63DSYKreTie-PvnB\":\"mx-beedoo-faqs (Old)\",\"wTaI3Klg72WdtaZ3qSi0o\":\"agent-tools-monitoring\",\"NWqwS1Sv6g2bGr1ArVVh_\":\"agent-tools-monitoring\",\"PNkqnA6gVKRTpeDS_07JO\":\"agent-tools-monitoring\",\"8TBNcHi2f1K2BP6Z2AmtB\":\"agent-tools-monitoring\",\"3bjoKIXjN5jgkVgktn_HP\":\"agent-tools-monitoring\",\"OEqsoobUuPMPveDTdAf5L\":\"AnalyticsDataPoints-D2\",\"xV6OLYmtNZu72zKMajYmM\":\"AnalyticsDataPoints-D2\",\"Hb8L9GMMjC5qX8z6iIIx9\":\"AnalyticsDataPoints-D2\",\"F8SEnSImA-c9fobmv0c4i\":\"AnalyticsDataPoints-D2\",\"BMLGj0UX3xAxq69DbhbZ8\":\"AnalyticsDataPoints-D2\",\"eCn1OvV41WiVS2qrkr6xs\":\"AnalyticsDataPoints-D2\",\"zdL-a5kftjHSQGjajNYb4\":\"AnalyticsDataPoints-D2\",\"u0iIup-sX78gePlof9ZY5\":\"AnalyticsDataPoints-D2\",\"kUJhVyM7Fbv1Mp_pKWhiN\":\"AnalyticsDataPoints-D2\",\"VE51cePupS4AoOZcfzie4\":\"AnalyticsDataPoints-D2\",\"K0V6prUyKjx4soV2O_jlL\":\"AnalyticsDataPoints-D2\",\"iGh4jdk4fCnnwyglY8-YJ\":\"AnalyticsDataPoints-D2\",\"ATfwUDt7d_iwvBxOPYkkO\":\"AnalyticsDataPoints-D2\",\"dAJQK4BHqVH560alKgyeP\":\"AnalyticsDataPoints-D2\",\"T6JJZnt6N2I9f64xh5JpA\":\"AnalyticsDataPoints-D2\",\"KBUoMpU4sEAxrLPyMPHQI\":\"AnalyticsDataPoints-D2\",\"SHLdMrFy0ebCxKsWIV0io\":\"AnalyticsDataPoints-D2\",\"rzgPCYyW6OVaRcj-2WKy5\":\"AnalyticsDataPoints-D2\",\"ASef1m5pxyqsd3ps3Cz5g\":\"AnalyticsDataPoints-D2\",\"XKBCH3Swfg1ZJEAtSLtGg\":\"AnalyticsDataPoints-D2\",\"bsQpkMSg9DOcWd_vJCGeY\":\"AnalyticsDataPoints-D2\",\"UK1MoBQnqOn55VUHq27YX\":\"AnalyticsDataPoints-D2\",\"jJq1q7z0NbC7ccukqf2Wm\":\"AnalyticsDataPoints-D2\",\"DwzzkuSYCHNTJ6X2Za0om\":\"AnalyticsDataPoints-D2\",\"IH3PsPfmgfhoO9Wwl7GnX\":\"AnalyticsDataPoints-D2\",\"z-wq-FbIUGepFZmJ6ats3\":\"AnalyticsDataPoints-D2\",\"RLFelldt62C8PsIfCUKHh\":\"AnalyticsDataPoints-D2\",\"gX-gVflIIzcfQ_oHFrYyG\":\"AnalyticsDataPoints-D2\",\"jliS0oUnkl55o1mzFcYhS\":\"AnalyticsDataPoints-D2\",\"44rAS9jmrG1005FaN9oiI\":\"AnalyticsDataPoints-D2\",\"vtasKfsfDmCoKvSNVPwxb\":\"iterate-queue-recursive-job-table\",\"zbu3TmnCNQUVsxkSqlX5Q\":\"iterate-queue-recursive-job-table\",\"PC6U_1_u0euNntML18-72\":\"Autohealer Fila Table\",\"5hl6C2D0TPhAWSiksMK6W\":\"Autohealer Fila Table\",\"aC3j9dC67rqOn1cdZPbac\":\"Autohealer Fila Table\",\"jsVFQ5q7h6h4-8EMkF_af\":\"Feature Flags Table\",\"jNl90EUcAsXjTns_ffGkw\":\"Feature Flags Table\",\"-k9J0VR0Qwnhy3J6iPOGe\":\"AR - All interactions - Forms\",\"SZmMUQjCQVUF7IOinPys3\":\"AR - All interactions - Forms\",\"gEjmr7rOdDeHlA8mlQTAc\":\"AR - All interactions - Forms\",\"mpY2hYuTiQehDC8n_NgKo\":\"AR - All interactions - Forms\",\"2ogV1hMmzR3G3A9Iv6DJU\":\"AR - All interactions - Forms\",\"x_CGgVnaJcoXFRnZ7gB5O\":\"AR - All options - Forms\",\"U-N0pkipl-XoJy7DT4XVt\":\"AR - All options - Forms\",\"j3pWVQs5WebJI5iUgDlNB\":\"AR - All options - Forms\",\"NslFL5Syl9NxV16tJy3QM\":\"MAIA + AVI - pruebas - TempData\",\"-tljEFxBJxzC7e4l5C_5M\":\"MAIA + AVI - pruebas - TempData\",\"DOq7tLR-pL6eMPwKUOylm\":\"Temporal_Inside\",\"DOq7tLR-pL6eMPwKUOylmDataSet\":\"Temporal_Inside\",\"8fTwjIcFBArJTPNYB3glf\":\"AFV-LOCATION\",\"MAHPDAHBRMe0qkhTyA9l5\":\"AFV-LOCATION\",\"W_IoN5n96o2xPHV1ONGoz\":\"AFV-LOCATION\",\"6YiF65z4GoSeqJ8KWcmdc\":\"beedoo-faqs\",\"rLhjVDf3yOomashE5TICR\":\"beedoo-faqs\",\"R0Q1i3twPMu3l5O5I81kw\":\"beedoo-faqs\",\"dK3wTn30LNexGCBoZWTWD\":\"beedoo-faqs\",\"w6IcSHa7RvZwt97136IgH\":\"beedoo-faqs\",\"oFYra06X4M3715ttQ5mP8\":\"beedoo-faqs\",\"AnK6ZYeBIWUAjU7eE_rje\":\"beedoo-faqs\",\"c72bQ8o2m70MDxRtCzc1x\":\"beedoo-faqs\",\"3EZwbu4DMquuTo3hsKMXZ\":\"beedoo-faqs\",\"CKHowF2sRR_7uZg8yWR8R\":\"beedoo-faqs\",\"aBEEDuSPs25NVHCjxLh2R\":\"beedoo-faqs\",\"bvMtLki_mHkmStDu1xu7b\":\"beedoo-faqs\",\"d3B3cmU3sKXZojfDSnN8V\":\"beedoo-faqs\",\"m5ZQrM5X0OhibGbrAh-IC\":\"beedoo-faqs\",\"hlN8CZRCASELzkhv0iNMI\":\"beedoo-faqs\",\"dJqeY_HW-iUbxhnAxfLCO\":\"agent-tools-surveys\",\"cB1gq-RN26JQ6-Q9NE36a\":\"agent-tools-surveys\",\"HczDUlOgBeOk7z_Is4qHC\":\"agent-tools-surveys\",\"y6_EJiJnM8n8A2_KfRQ1m\":\"agent-tools-surveys\",\"3QPeMNf2ijUvdN4o8CReK\":\"analiticsdatapointsforms\",\"rnyYOSSl9C2umxvZyDdVB\":\"analiticsdatapointsforms\",\"Yw7PTM7IIQvsz_vuyQxdI\":\"analiticsdatapointsforms\",\"qLIbZiyvohljgX9_15d2W\":\"agent-tools-events\",\"mWgRNb5KZabt3BnnWDUqs\":\"agent-tools-events\",\"K6aM_zOwRiIxZTjVuqIiO\":\"agent-tools-events\",\"4YF64xYVf6ZmFCPp4agJk\":\"agent-tools-events\",\"xHZ9cc_FpkEzaWWKpl3Rx\":\"agent-tools-events\",\"peSZeE6tp_wBKNgDpUyys\":\"teste gui\",\"peSZeE6tp_wBKNgDpUyysDataSet\":\"teste gui\",\"VZ7KSWk5lQc8NDDgzpFSu\":\"test_loop_on_json\",\"BKNfsOb-pvwBxHzH7CZMX\":\"test_loop_on_json\",\"-lci4Nan7jQkleVaehcZ7\":\"word-tools-monitoring\",\"1Fr4SIxVVrrTkocJ6jUyL\":\"word-tools-monitoring\",\"nIb6l56ET0fyMwSqaxdb5\":\"word-tools-monitoring\",\"xFbRDScPQzCJSDbYPav0N\":\"word-count\",\"g_8_wpg__U_-ihJ4pSh3R\":\"word-count\",\"CN7S-6LL2XDqJM8qYE3OO\":\"word-count\",\"v7YYvzzAxEnfKApEJr-hx\":\"Reclamos\",\"-RAi6VPWJOVgyNPJl8VIH\":\"Reclamos\",\"cj5yhgQBORuEFnA8HayYn\":\"Reclamos\",\"83mS8jI7YitaXwUR_Y77n\":\"Reclamos\",\"CQC0gmiK4hccKBgK3Qu0w\":\"Reclamos\",\"nqsrzEe2l6i53peuYo40Y\":\"Reclamos\",\"acsdnbu0aivFUYbH9ZKKb\":\"Reclamos\",\"AljAFF8Y41ND_rgs4QegB\":\"Reclamos\",\"wwgkYoaHCQ6AVXEX3sFbk\":\"Reclamos\",\"wgLaaE-S0jWgfr9uJPpA2\":\"Reclamos\",\"lsBdlnDTClvfjyf62bAj2\":\"Reclamos\",\"K6qhJfUdv2of5SU7DfyhG\":\"Reclamos\",\"xCkcN0gl6CVWIaHjOxcV8\":\"Reclamos\",\"NxQe5hjpM3zUQ9Jc7Ys_O\":\"Reclamos\",\"-k1u-p3Qpz-OISp2DJMXS\":\"Reclamos\",\"AIoSiB4oPuGmoNl-q6DSQ\":\"Reclamos\",\"WUULFW9Z9kXDEDWVpCaM9\":\"Reclamos\",\"cg-NTdKM6Wf1VIPJC-N9v\":\"Reclamos\",\"ppz90GdPZGdzy60UycDP_\":\"Reclamos\",\"uTnBcElaE1LVzLHKNHVog\":\"Reclamos\",\"LFyN67aeh6pmT2AR62JSS\":\"Reclamos\",\"6JUoiKV-4GhJGXEDq-s0m\":\"Users-Avi-Gestion\",\"UQSI2K3srRRatRzW9bAWn\":\"Users-Avi-Gestion\",\"LFpHQWpwfUNurBZ-Dp0ut\":\"Users-Avi-Gestion\",\"u78Mk-VgU__GZ3nusHOM5\":\"multi_usecase_tester\",\"d63-saQN8kfciaC2jdwN8\":\"multi_usecase_tester\",\"xr1lUSgFPIgaKUJ52PpjC\":\"cruce-estado-pedido\",\"_PgYVj8hPBMe-gMyjT3Kl\":\"cruce-estado-pedido\",\"TgtXV2LHkIZpUdB2nL5F1\":\"cruce-estado-pedido\",\"ckfAtcX98dpvWNjbZLF3P\":\"cruce-estado-pedido\",\"FNx0gg0FUUdR99JU_hzoa\":\"beedoo-interface-ds\",\"FNx0gg0FUUdR99JU_hzoaDataSet\":\"beedoo-interface-ds\",\"YQqxFfPpow9OFqr6drPyE\":\"external-transfer-users\",\"T-cCtftfBp5ZlJh1zcQBG\":\"external-transfer-users\",\"-VLqGm0wsaQUhRQxLiDKr\":\"external-transfer-users\",\"DTarnyooO9Zts6f3aUrD_\":\"788_ds\",\"9xhO7OzWTKl8oQ-SydqLU\":\"788_ds\",\"pO6lbxUr5bOD_ec7ndhU6\":\"788_ds\",\"qNgsNLfrx0T18MS2ygum0\":\"DataSalesAssistant\",\"Iefd1Dsyc_LIliY9jPe0n\":\"DataSalesAssistant\",\"y98H13Gy-eXhvkMlGtyNA\":\"DataSalesAssistant\",\"WcxdGfxuvLwHARkcTIA4c\":\"Users Auth Table\",\"7e1XEAhJRJViyBqfRf_0S\":\"Users Auth Table\",\"JVWOmHWxizG5Bz3knSF3P\":\"Users Auth Table\",\"nh454JZc0N8jKaiSbWutH\":\"SubmonitoringForms\",\"sCb5Ac76kaRINgLQRT86J\":\"SubmonitoringForms\",\"UE4Kb961FSP1wAvcwxH0o\":\"SubmonitoringForms\",\"ig1hdgk_G-doTBPc86pVt\":\"SubmonitoringForms\",\"V4AfVyYlFiHE1z-eihUV7\":\"beedoo-ecommerce-faqs\",\"VfhEC7yoxzVgIsnaYFlb_\":\"beedoo-ecommerce-faqs\",\"ijWsRAQhrnvZZMtOxiby1\":\"beedoo-ecommerce-faqs\",\"6bq2lvwUaSG7bpsJlIUub\":\"RPA-BOT\",\"jMKfOZ66UkQpz2vt-sSqD\":\"RPA-BOT\",\"Tm9zzXRWQ987LX86XGJhz\":\"RPA-BOT\",\"r7c_R0Pq42eSls0ZHUFvH\":\"RPA-BOT\",\"DsvcmfcEdh--oULxsT1DI\":\"consultant-code-ivr\",\"nXMiwTK56Y77ITlwfV8oz\":\"consultant-code-ivr\",\"6j8EDvyRBXzkKaaxO4QrY\":\"gerencia_ar\",\"-eyjjQdR3jGzvAe8wHE6U\":\"gerencia_ar\",\"bt3tsMchwpVChqMG_yavh\":\"API_Execution_Central\",\"bnd5Bx7iA70PamX584g4M\":\"API_Execution_Central\",\"4V0yZwkJAzHvFZiuPmEfF\":\"API_Execution_Central\",\"pgNPySOMNwM2bl48w4wTr\":\"API_Execution_Central\",\"K6vtYxGq21IyQLCHOq24r\":\"API_Execution_Central\",\"znbq3rmEf75Mg0pJO8bGn\":\"API_Execution_Central\",\"zW7_Wk5eFA1U791scb7YQ\":\"API_Execution_Central\",\"haIujUPNIivJ5D8-GDbzk\":\"API_Execution_Central\",\"G9bPAOZDUJedGOBtiHJlY\":\"API_Execution_Central\",\"iIUwGkbFwg-LCJXd-UEyf\":\"API_Execution_Central\",\"NPYaf5CqgTKcctGn4860c\":\"API_Execution_Central\",\"WMc_Gg7E1YoCv7MLAUWwp\":\"API_Execution_Central\",\"T5gfPJFyvBsdxEB1VXG6m\":\"API_Execution_Central\",\"-XkMR8_PipkwSdmhW0byn\":\"Execution_API_AVI\",\"a7vOg7extkRt9FtyD1Ugn\":\"Execution_API_AVI\",\"gO579xYGZMqQq9Mmc4xMD\":\"Execution_API_AVI\",\"a96h5sWr0mBGdNNgLo9UD\":\"Execution_API_AVI\",\"UEljrYMGlBbJxQPqNAaHd\":\"Execution_API_AVI\",\"wI0yt0noWa4kdIlZBQKQH\":\"Execution_API_AVI\",\"zLYu1UYNQbDeK49rBP8DR\":\"Execution_API_AVI\",\"uQvH_EwEznUkX7JroieOE\":\"Execution_API_AVI\",\"7YJdM28quKbQyBw8KE_aA\":\"Execution_API_AVI\",\"Rayvtn-L2au_JgA90xDrh\":\"Execution_API_AVI\",\"sMzIruzDv9tnZOOj2kVfj\":\"Execution_API_AVI\",\"udbim-n7L9QUfWRVNDJ0V\":\"Execution_API_AVI\",\"sBt1PGlDTaeu3fDsx02-h\":\"Execution_API_AVI\",\"1a-5STGu5fYRrQRkEHlL9\":\"Execution_API_AVI\",\"cOJ1Gn2ke9YVn5ESqKgRY\":\"Execution_API_AVI\",\"sfE2uVR_5bLCjhFQ93Sin\":\"Execution_API_AVI\",\"RqiCq3JHfgXEYIZSHbgvn\":\"Csat_admin_filters\",\"ujNE-yVzYkXDt3OdS5hug\":\"Csat_admin_filters\",\"ZL6ces4sdKpk7umyVBvdH\":\"usuarios/hispanica\",\"JR-LDSL40aUYET6lrQIDN\":\"usuarios/hispanica\",\"EvLz5aJm0RGpgOodoEXxB\":\"usuarios/hispanica\",\"ppUKoTP-czVM8-0RqtHMw\":\"usuarios/hispanica\",\"sX6C3jOkxXe-oK-dFi4nr\":\"usuarios/hispanica\",\"eW_cp7JiLda4o1gJhnTW1\":\"usuarios/hispanica\",\"GJKPuTqxFNAwGV6WQ-ft2\":\"cache\",\"CC3J_icGpd5_-_kJ1tTjc\":\"cache\",\"0bmyNuKMqxVSYnuSar8kN\":\"Execution_API_Forms\",\"GmhsTMW1JSF8HG-85okpS\":\"Execution_API_Forms\",\"N7j7a9oot_tFtpLNlpCDw\":\"Execution_API_Forms\",\"NhYX5Bw9PTwZIstF37Pk6\":\"Execution_API_Status-Order\",\"ZZFsBwMsqSgZaYE_apWct\":\"Execution_API_Status-Order\",\"97PZOYdaXL-mQs8Em9Aeq\":\"Execution_API_Status-Order\",\"PvFYbNNqX_YoxOPNzMvOz\":\"testemail\",\"PvFYbNNqX_YoxOPNzMvOzDataSet\":\"testemail\",\"ZRIyWno2QfkMvu2Kqdlww\":\"RPADS\",\"q7Ash6nqQktqYPpBxLS59\":\"RPADS\",\"xaFiSNmZZJpho5l1H-YOg\":\"MisReclamosRPATEST\",\"cOGnLajCvf4Qv9uIZ1AKu\":\"MisReclamosRPATEST\",\"LSwdxLdoWAqZtijTZqE7u\":\"keycloak-oauth-tokens & pins\",\"7r_Var-lGoZLtONW58J1w\":\"keycloak-oauth-tokens & pins\",\"0dO4fghiHwmClvtfSVdpH\":\"Identification User Avi en Mi Negocio\",\"f1Ncdfme2aNKquwccAyc-\":\"Identification User Avi en Mi Negocio\",\"VECOrUvxj5rn01wwJRnFs\":\"test cases\",\"YJh10RQ-ROJpl1DZNQ9cA\":\"test cases\",\"gV8RtCBN3fBDJW9pguaX4\":\"test cases\",\"CIaTJECYhLJD9FWEo0sdv\":\"test cases\",\"69Fg1zleDjov8FlSpiTDb\":\"test cases\",\"OXQrpKoh_HDSx6KavU6iT\":\"test cases\",\"LrYTLairLoWR17hp6dPbp\":\"Related-CsatCountrys by Supplier\",\"XcajC4VDybdCoQ4mUNgT5\":\"Related-CsatCountrys by Supplier\",\"-qjhScbnNWM9p_HMd3fjN\":\"refactoring faq data souce\",\"6t0lsngPdvPoKM4nZsfcr\":\"refactoring faq data souce\",\"qUETLalnZPym_IePfL2I8\":\"refactoring faq data souce\",\"_Sap-4AGe2OzHyWKSQV3W\":\"refactoring faq data souce\",\"rB5c5bZtrQ2tlasgKGaVj\":\"refactoring faq data souce\",\"i2d8TDPb_muWf59qfjyi0\":\"generate-answer-data-source\",\"BSFcPGYWyu5OfpquNxB9c\":\"generate-answer-data-source\",\"I4BG2EUpCrjIjOVdTxUsp\":\"generate-answer-data-source\",\"lCEMcuqCu3PiglB0EeU57\":\"generate-answer-faq-data-source\",\"0JZVJaAAXOt_MnNCmkFov\":\"generate-answer-faq-data-source\",\"n9PBWVAnuIc99EdwSTgXU\":\"generate-answer-faq-data-source\",\"QGVCgSseGiWtvSRuE9S9z\":\"generate-answer-faq-data-source\",\"Ghsf2YdfB2WV1dQamC9yt\":\"yadi tests\",\"WR_MvoPeDHZtcVORc9MSB\":\"yadi tests\",\"jj95nSCsjVQnC6M-PuJca\":\"Activity Log\",\"_HiwkqaE6tL140snRngx3\":\"Activity Log\",\"U-FkIWlFy8Tw_16kEw13O\":\"Activity Log\",\"I94TpZZUS9i315lH04LhA\":\"debug_juan_auth\",\"QDMiPWdvHXiPqIIxcVn5t\":\"debug_juan_auth\",\"8jNWG1d8GMZv75PPzYcGo\":\"integrity_automation_tests\",\"pidHmpJgQaZadLB_ERozo\":\"integrity_automation_tests\",\"dKhFLAHX-kvfaSuZWImjL\":\"tapume_data\",\"ENkf_NOvDrQ68FlBpcyH2\":\"tapume_data\",\"XTDyEVvr6pOf-NqhvsXfY\":\"integrity_automation_logs\",\"C4njiJBvC_-jYrMcjmfRd\":\"integrity_automation_logs\",\"Y8n4_m_Ngn5aa4qiEODL5\":\"mx-filtred-chatid\",\"W6_b0DUXCachjEGXyVw0m\":\"mx-filtred-chatid\",\"1t5p4Yw3hr5EUeJJcHE7Y\":\"mx-filtred-chatid\",\"6jP7UpCEBBTUNTJ6EDyE7\":\"mx-filtred-chatid\",\"58UpR6aXm1c73TYJUgG72\":\"cl-filtred-chatid\",\"-0xGMdPCystZCmgUjQAny\":\"cl-filtred-chatid\",\"UD3bbRv8mmBtN8HesCpuL\":\"co-filtred-chatid\",\"tAAkx5lvxET4P31uftsFQ\":\"co-filtred-chatid\",\"tHHTsPYsD5gAwbnjPNVF1\":\"pe-filtred-chatid\",\"VGr5VUWI_l8tOMGT4BlgL\":\"pe-filtred-chatid\",\"9fTC1m7f6l1bzu7NtDHQi\":\"pe-filtred-chatid\",\"Urs9ESzqkZSb88s7Bl2Rc\":\"ar-filtred-chatid\",\"Bpc5b-Ib3VHG1x5ykIfQR\":\"ar-filtred-chatid\",\"mnZ97WcDCei8yLn1X7fpg\":\"ar-filtred-chatid\",\"zs5uZQRHFPh0vF6SRuYe0\":\"ar-filtred-chatid\",\"qePcX_SakQd5PA_91dEWx\":\"ec-filtred-chatid\",\"KD7F3SmdgyrQwS3937frs\":\"ec-filtred-chatid\",\"vhrlBO9xhrYaxt8m4d2RD\":\"ec-filtred-chatid\",\"0Kl6j9YeJowSLoGajBwDe\":\"ec-filtred-chatid\",\"uclq0rSYc-5Q-0VJ9XWc7\":\"sinch-ar-filtred\",\"YorwlYy7MZNc_-mGS27F2\":\"sinch-ar-filtred\",\"fC6MF07TQz8VtO6OtfQZX\":\"cari-rush-filtred\",\"qIbYT3ytrJ_4mf5co-ZCO\":\"cari-rush-filtred\",\"T840JiI8gZ4G3ynoo7Oby\":\"stefpruebas\",\"1pYPi4zec9PSZQ90hjWpJ\":\"stefpruebas\",\"_jtImXwtyDSF3MHIcaN8R\":\"stefpruebas\",\"1DmgT9yjDlv5SnVwUg_bP\":\"stefpruebas\",\"bscDqFn_AFBlzGEpWCCAa\":\"stefpruebas\",\"FddcuPiV8rUEqVKUG8r2L\":\"stefpruebas\",\"0gXfQGRocfkibq1rEZZYT\":\"paises_configuracion\",\"dczCj7IlncrUmnKX4Uldl\":\"paises_configuracion\",\"6Q2vukZ3W0hLA4Fb55opl\":\"paises_configuracion\",\"uUjYdSIdatYwBeoVlvg9S\":\"paises_configuracion\",\"E4xqDeJPxV2rY0WW0hCzf\":\"paises_configuracion\",\"twdzekRafm4m2wP4H2of4\":\"paises_configuracion\",\"sIg6CY50DnTmZCt4UeQCT\":\"RoleAauthorizedContacts\",\"hk3St1Pf-gplEjwXfGs98\":\"RoleAauthorizedContacts\",\"EziykAB_ezPsQQ4CFKtMJ\":\"RoleAauthorizedContacts\",\"h1RaseWJBiAkS9HZamA5H\":\"RoleAauthorizedContacts\",\"ab2TnlF90uyNXXzBb9GY_\":\"tapume_activity_log\",\"o-r6n41Lpw7FkOr8ooU71\":\"tapume_activity_log\",\"k1dBmU2RODmDUg99N19pa\":\"Indicadores_Digitales_Forms\",\"5OpY9kOGNuA7HawDDRx9M\":\"Indicadores_Digitales_Forms\",\"2L5GMh7PsPGenY-YEgdLO\":\"Indicadores_Digitales_Forms\",\"0nGd0jPiO-SUxdDYuTKd9\":\"CSAT_Mediciones_Formularios\",\"P973ZckOBqviQY7_b-sx6\":\"CSAT_Mediciones_Formularios\",\"-qA-rqrBcLvMOqnF6pweT\":\"CSAT_Mediciones_Formularios\",\"QaaqPVfO2eQv98OXAyo6B\":\"CSAT_Mediciones_Formularios\",\"1om1K5xbvEPWxGAxc7iCu\":\"CSAT_Mediciones_Formularios\",\"a7Ql614hAWHLZpfeQpKoG\":\"CSAT_Mediciones_Formularios\",\"jBIBb1vlAy0PnZVBFl5Kf\":\"CSAT_Mediciones_Formularios\",\"nn0vAL21Wh7qQaSFCZPuR\":\"CSAT_Mediciones_Formularios\",\"E6XbKoN9EujWxgrLDxPJ-\":\"CSAT_Mediciones_Formularios\",\"yVFrNeYuIt1s2D8QkbISf\":\"CSAT_Mediciones_Formularios\",\"uBsqnjbkDkwYBQIYfUUNw\":\"CSAT-API-Logs\",\"C2575Mf4JgkbBKy9V8A1h\":\"CSAT-API-Logs\",\"gxZnOv1qEGmr9WsZFOtqy\":\"Cancel_Order_specificLogs\",\"W42eOD1D80_Ie1HIE6mpW\":\"Cancel_Order_specificLogs\",\"AR_dXo2lHzx1GFasdl_wl\":\"Cancel_Order_specificLogs\",\"svyxg5sCG6U1MznwlrEOp\":\"Cancel_Order_specificLogs\",\"NrHZJDfpEXTc85rJ6cZjP\":\"Cancel_Order_specificLogs\",\"u8bOSEWickcx5SceC2FWB\":\"Cancel_Order_specificLogs\",\"CLc_VWhtNK4BbrVEk1b6G\":\"Cancel_Order_specificLogs\",\"i9mNdVTuL9PvtSxGubeWZ\":\"Cancel_Order_specificLogs\",\"0H8BB07cc1XpIEjpVqYmI\":\"Cancel_Order_specificLogs\",\"S1V2f4AF2kScAR_iYDQOh\":\"Cancel_Order_specificLogs\",\"bkwnFzbCWV62XUE6hYmHr\":\"Cancel_Order_specificLogs\",\"rMnK_K9qC5_qq0234dBF3\":\"Cancel_Order_specificLogs\",\"9gvjjlnY27HXOctu1Ngx2\":\"Cancel_Order_specificLogs\",\"1P5pEF4jbFnwrfgAdSCgT\":\"Numeros Admin AVI\",\"pdzvHHQ_FSFtKAzjoOpiQ\":\"Numeros Admin AVI\",\"VI-X9KdbOWlrrz0eaw90s\":\"Numeros Admin AVI\",\"QjxztAVftpJ5xzvcYBksJ\":\"Numeros Admin AVI\",\"nYVgllPZYaCiFTE9FmMlT\":\"Numeros Admin AVI\",\"1KGPCM2JTo4xfBikEI8S-\":\"ALEX TABLE\",\"KKNYY7nVMG3D8InmqzFLO\":\"ALEX TABLE\",\"TimzI1kV4Vc0Z064qHe56\":\"Control Diario de Pagos y Ajuste de Crédito\",\"-zsqUgrV3ctSzAYMKBL3u\":\"Control Diario de Pagos y Ajuste de Crédito\",\"VkQO3ChilcahxwQGAEU66\":\"faq_suggestions\",\"buMIvYIgcp4Szwb4WjEAy\":\"faq_suggestions\",\"y2Gp88N1IKIHDdHp-0zZk\":\"faq_suggestions\",\"TbYj0-jRQkH9CUkeyhI57\":\"Copy of chats_gym\",\"lHiJ5ep-HQOvaVAKIoPc1\":\"Copy of chats_gym\",\"Nweqo1wYp1U-4O2dgwvhu\":\"Copy of chats_gym\",\"QimdIhidXNEUiCTlfGef4\":\"Reset log\",\"Omqhy3vKOb0lP9KRw8yxe\":\"Reset log\",\"cjL2bSPJ9K4Tr5b01RYv6\":\"New Data2TableDataSource (361)\",\"j4xG8pflakYr5u5EIgi2d\":\"New Data2TableDataSource (361)\",\"PhE7JmUXLX4YBHmVZPzn4\":\"New Data2TableDataSource (361)\",\"JEequoS4cDPBw44u0_I6u\":\"Teste format Table\",\"UIxUessGG2isDffz1djPS\":\"Teste format Table\",\"epAdAAyfDp1vN6TVzsLp6\":\"Teste format Table\",\"ubh1S8-pjzHg9RRXqz0a8\":\"New Data2TableDataSource (363)\",\"F3CRfABXa4sGJuzPL8P4X\":\"New Data2TableDataSource (363)\",\"fNbtfErAyqz2j3hW0usC1\":\"New Data2TableDataSource (363)\",\"7Eiq8OgfxSyLcBx-Al6iq\":\"OrderOptionsUsecases_CN\",\"_6L_D-0enouanyBAPwKXA\":\"OrderOptionsUsecases_CN\",\"CVwr9Btx27wOHJg8F7QG3\":\"OrderOptionsUsecases_CN\",\"no36vR3jVbwGnT2FUCvuV\":\"OrderOptionsUsecases_LN\",\"S29PiiZJmxHQY-T_wG8fe\":\"OrderOptionsUsecases_LN\",\"c-VsG2E8KcKOUb8UOpR9W\":\"OrderOptionsUsecases_LN\",\"AL9OZS9joXG4KI72T5Ab8\":\"OrderOptionsUsecases_GN\",\"gkCxBFlKtOOBIdhocA1gn\":\"OrderOptionsUsecases_GN\",\"IVUQnEMipJyXa6eX0uTMe\":\"OrderOptionsUsecases_GN\",\"bTElqE9OlyaSnfzCLJj4A\":\"Last Messages On Whatsapp\",\"0Cr6WmIz6x2uPDlCIZhF8\":\"Last Messages On Whatsapp\",\"-PdRzI1WJmWTudfOHDB_u\":\"Last Messages On Whatsapp\",\"JlIY5MsYz6YmxC2BvJ4lW\":\"Last Messages On Whatsapp\",\"r7OJkqw80Wqh7hPrOhIGd\":\"New Data2TableDataSource (376)\",\"tW-KjwCwXAEhm26NKgY5P\":\"New Data2TableDataSource (376)\",\"_4ikPaT5L-5C5krC4F3Yw\":\"New Data2TableDataSource (376)\",\"RnSxOYWe_AIKozE8k-ncl\":\"New Data2TableDataSource (376)\",\"uZWvdDKVI_FVMnmPJf5_C\":\"New Data2TableDataSource (376)\",\"Owp5bNCaaEATN7XzGwewr\":\"No Error Tags\",\"-lAfKyJuDcGC4DqKXlBP_\":\"No Error Tags\",\"VmqGQO0MY22UAJ6N8RwFx\":\"Marcos Teste\",\"gfYAtotG4qaI26rKv_-6k\":\"Marcos Teste\",\"HEBmnFMXTdJAYGQMKukkG\":\"Marcos Teste\",\"A-LPpvcL6BhyeMsALA4a3\":\"Natura Auth Strategy\",\"NmciGBhPeF4YPa7WFcwAA\":\"✅ ai-portal-authStrategy\",\"S90XiGh93fT0KnBVr-0Lj\":\"✅ Ecuador AuthStrategy\",\"0vLbtvkp24gw72HzUGo8E\":\"✅ MX Agents Tools Auth\",\"OzjbVWfqBgPZAvmcKhHRd\":\"Agents Auth\",\"DyyTEBU6g1LXIpn1l4TBb\":\"✅ faq users auth\",\"ioKl06rqQrmsX1VUqK5-c\":\"✅ Ecuador Admin Auth\",\"Om722U4XMUcQx6O6BHkzT\":\"✅ Agents Tools Auth\",\"vob324h71FgO0vmCGOEaq\":\"✅ Gestion-authStrategy\",\"KYzYFIQHIE-oASV_Ur4uK\":\"✅ externalTransfer-authenticationStrategy\",\"APYJSnxU5Mv12fk9JOV4f\":\"auth 788\",\"ZhuDnlYl9xhSfP2jAt51O\":\"✅ Authenticate\",\"Xw4nk036Fjt_p3ddVEQll\":\"✅ authStrategy - Hispanica\",\"pASZhw5Jbv4t-JTGbPOOv\":\"Copywriting\",\"nfFaXZLQH-leaW_Qv_ORH\":\"WSDL\",\"smI-d-ImcTi9v6sUimWO_\":\"SalesForce Query Assistant\",\"7fCWMgFbt3srKOPtf2kDJ\":\"chatroom-assistant\",\"YGFSyubfcgzlLSYo86GZx\":\"ZapAssistant\",\"SBK-xNNx8RgUmgRnKRwsu\":\"Search Products\",\"VA2rNWBqOnWtyQy2uxflK\":\"imagem ou informações\",\"eSdLF7Om6x_s0OHEZVSyu\":\"Especialista Natura\",\"r_oUqNMuQkgzHezPDcAAx\":\"context-action-assistant\",\"oOX8k1N0QsfKH2wa8wNj9\":\"[hispana]chatroom-assistant\",\"f5rC6a0JvmhcVW_q9NWY7\":\"[hispana]context-action-assistant\",\"iGKHk5kadFvVeookjOzlc\":\"chile - context-action-assistant\",\"AivRcw5taDpmCGya_vvfU\":\"chile - chatroom-assistant\",\"HvWaQGZDuPf13CtnpAT1D\":\"DMA-BOT\",\"GBFF-sdWMv_RLBHst9cRA\":\"UseCase Assistant\",\"amoBPQMMezGLBuZZVAFPV\":\"FAQ Detector Assistant\",\"K0lEpGHSC2OZndmZVObSS\":\"FAQ Conversation Assistant\",\"dGlpROdO8iqokf2Pd8T2q\":\"AVI - Conversation Assistant\",\"eI30A1iEtF8iQeEFtJSVI\":\"Cria mock pedidos\",\"-YIFugqZ5yHyWG3fOu9So\":\"Cria Deudas\",\"vXnLDrq3uZdf34ZBeBWpt\":\"MessagesAudioSpeaker\",\"aB0-MQdaQH_w5_7HiNgq0\":\"AtendimentoAssistant\",\"C-itKJ9Jp5psbaBqnIwrg\":\"chatroom-assistant\",\"yITzqpUUnlXKumhgnUnZC\":\"stef-detector-casos-de-uso\",\"ilvXJEH9yj7NKun3Xje1C\":\"chatroom-assistant\",\"sXMteZakDYp6y51LmOrzM\":\"chatroom-assistant\",\"6SYTA99hNlvvtJZrBMrqC\":\"chatroom-assistant\",\"lGBqj7BTgrtHCBdmqeao3\":\"FAQ Detector Assistant+\",\"Iu3ZSXZjoieYqLHej6QYI\":\"FAQ Conversation Assistant+\",\"cv_3p8KMvU2TSeHvkrT2F\":\"Conversation Assistant+\",\"Ej5y48JDemqtob2MM9VKa\":\"UseCase Assistant+\",\"hMZ6_N1_k3oAC58cpUM1k\":\"Location_Conversation\",\"HEp-5shOWvvOEHC0eQP5a\":\"Send Package Code\",\"1ixwszvwLdmk_AEPh7VPo\":\"FAQs Counter\",\"uZJlLsWK26OLFgZl8y7FU\":\"Multi response FAQs\",\"eGZ_8YhkLGFAqN3XedDxk\":\"Nasvi (Jazz) Assistant\",\"pWLZ1z5x2lx9AqSIqp0up\":\"Specific Intention\",\"GICc_tqXSbOoaWSiQb11V\":\"check AVI content\",\"elh1ns1V-lKz4WieAO5oG\":\"Mix all information in a single answer\",\"qK_iHrmSrV8O-J4Y1iLa5\":\"step assistant - hispana\",\"iR3ThAylpuCQi6-tIYvTQ\":\"ASSISTANT - set category/context\",\"3yxzjf-l5V43iKXhhxSjS\":\"multiple_usecases_check\",\"BWdUSUGN3Xd67pWCpwigW\":\"SentenceSeparator Assistant\",\"Nt5sWx8QmyDoHSE2AFGXM\":\"Mix Answers assistant\",\"mDierjbW5J8La1MxHZ3B6\":\"B - Mix Answers assistant\",\"AW4XVDj_rBKR_frcMMTU7\":\"C - Mix Answers assistant\",\"2EYDhTi1g5tWEcZvveZuI\":\"wiki_assistant\",\"eNLMtizZAWdA8DXbBsCpq\":\"especialista-ecuador\",\"R81yu-Ghj-ZO_oJHw3-MB\":\"Skip Preload FAQ Conversation Assistant\",\"mXXLmSXihOdZJPHYySCM-\":\"Asistant_Test_Miguel\",\"zkBULFdDhtMpRawOMRAYl\":\"Assistant_Mejorada_Miguel\",\"0IPlavInQy5Zgsw20wzyZ\":\"Conversation_Assistant_Miguel_test\",\"Z9d_E6mfMCsz0TSuqKENf\":\"assist - refactoring - select usecase\",\"LLZDNXMBsm6Rdvxi8j604\":\"assist - refactoring - generate answer - usecase\",\"59DtUttJ8aR15k1Ruuxbl\":\"assist - refactoring - generate answer - FAQ\",\"Nz9lpnEoZe8gFOnTjUenw\":\"assist - refactoring - select FAQ\",\"cfKJaohLsmVRyK2e9Bv1F\":\"Conversation_assistant_antigua\",\"s3qOEa5O0wzAabmrIB2Ch\":\"assist - refactoring - compare answers FAQ\",\"TAFADwOQ79raIo3ipvVFo\":\"new - assist - refactoring - compare answers FAQ\",\"ig_mujwIc6nnvVKdXrpjJ\":\"beedooNormalizer\",\"Jvh9TNi0x_y9BwoDWqCne\":\"Copy of Copy of Asistente_chat\",\"1qS0VoejtWTERV1EpSox3\":\"sum_history\",\"z8AUipV10wPunaHcpK0Fp\":\"New Assistant (63)\",\"U9BRNWhKyh0mTogrRuMds\":\"New Assistant (63)\",\"wMvPaYzDmLfpxgFX4vDRy\":\"OtherDrive\",\"nuteSRebg7GwXm207jMOE\":\"assets\",\"BvjucKU_YVef_GEzfQMCA\":\"Temp Screenshots\",\"9DcvOcRiCx0ZeCyBQsdC_\":\"Sprint Sessions Drive\",\"Qbb37K4v-nUDpOUkvPnbB\":\"prints-endpoints 2 session\",\"oLuENtDSEhYdaB5r3j4AM\":\"FAQ Drive\",\"efKKF_frpix4xMZfAMd1T\":\"demo-atendimento-colombia-drive\",\"zRnXbqY-8m_HQq_EAoPp0\":\"MessagesAudio\",\"jeG6uJRaeVcE6MVlVudDV\":\"Natura Audios\",\"RYULth72jWwHf_sbAvwWc\":\"Print Attachments\",\"7VnQJph6VqgXjEyPfciRo\":\"live-assets\",\"hfGlZx2fGP7IjdSHuNCMC\":\"Docs Drive\",\"laPtxgWAtASsuPkg8szTo\":\"Natura Shared Assets\",\"ursSebQzAyGczDa9Bb9PZ\":\"Ecuador Documentation Drive\",\"_jLgPbv8OSmZ2IGj5v6A0\":\"atendimento-docs-assets\",\"L7oOSFzf31uZNC7rrSv8Y\":\"Natura Chat Drive\",\"xYwNCypurPRVny8bY96zL\":\"faq_request_updates_drive\",\"OVWWvaQcVOiBfolO_d6Oy\":\"Drive - Invoices CL\",\"3kHR5uvuTqd_z2nte1vkn\":\"Imagenes Varias\",\"lYOUPhtU8PjvgrllO0LQX\":\"Forms\",\"d2-rush-email-drive--i5W24aHHKkJ5EKal7HyMd\":\"[Data2] Drive for Rush EmailListener i5W24aHHKkJ5EKal7HyMd\",\"2xqXvDfqPxwhtcOjhoxhN\":\"Faq Operador Drive\",\"d2-rush-email-drive--aAuM_I24P322rGD0eZ-UG\":\"[Data2] Drive for Rush EmailListener aAuM_I24P322rGD0eZ-UG\",\"IwngrVMQ-KA88dbUEWyS4\":\"multicase_drive\",\"d2-rush-email-drive--4sJtg3IlZKvtb4rD8if4l\":\"[Data2] Drive for Rush EmailListener 4sJtg3IlZKvtb4rD8if4l\",\"jaLck-NZE5_9NUNg3kXOg\":\"Drive perro\",\"MsCz4pHpRTLPsPpyJ8RtB\":\"drive_avi_propuesta\",\"2oLDqqtLsVE2zVybJxXXy\":\"AssetRepository\",\"gASxnb9ssA7xj5ndhdI-d\":\"FAQs Drive\",\"noTNF3fE7LGuYWBq_uhVp\":\"external_transfer_drive\",\"mNL-PfXuHfR3jX37AVdDK\":\"New Drive (34)\",\"kZo5qMX3JsrHbSQsHMNaM\":\"EstadoPedidoDrive\",\"nDHBaAmxgmjmaO0yNbPoW\":\"Drive Explanation 1290\",\"Ai8ULy7sOmXXSCfKhwUiL\":\"dj_analysis_drive\",\"wnjbKgJYUdqni-dDyRyNm\":\"Copy of Copy of drive_chat\",\"yccTgOYe6M94GhdD8udsg\":\"ivr_assistant\",\"RCiLYfZ9yJeGeCnxcWrI4\":\"ChatHook\",\"mg2bxYVq7OiC4BPR_yCAH_\":\"DMA-Gateway\",\"reply-entrypoint\":\"colombia-reply-entrypoint-hml\",\"reply-entrypoint-prod\":\"colombia-reply-entrypoint-prod\",\"DKnDLQUtDA6mNdYkT8sh1\":\"audio-generator\",\"peru-reply-entrypoint-prod\":\"peru-reply-entrypoint-prod\",\"peru-reply-entrypoint\":\"peru-reply-entrypoint-hml\",\"mexico-reply-entrypoint\":\"mexico-reply-entrypoint-hml\",\"mexico-reply-entrypoint-prod\":\"mexico-reply-entrypoint-prod\",\"health-check-colombia\":\"health-check-colombia\",\"health-check-peru\":\"health-check-peru\",\"health-check-mexico\":\"health-check-mexico\",\"monitoring-colombia\":\"monitoring-colombia\",\"monitoring-peru\":\"monitoring-peru\",\"monitoring-mexico\":\"monitoring-mexico\",\"chile-reply-entrypoint\":\"chile-reply-entrypoint-hml\",\"chile-reply-entrypoint-prod\":\"chile-reply-entrypoint-prod\",\"health-check-chile\":\"health-check-chile\",\"monitoring-chile\":\"monitoring-chile\",\"ecuador-entrypoint-hml\":\"ecuador-entrypoint-hml\",\"ecuador-entrypoint-prod\":\"ecuador-entrypoint-prod\",\"health-check-ecuador\":\"health-check-ecuador\",\"monitoring-ecuador\":\"monitoring-ecuador\",\"di5-RlyrJ7LB5lv9z26xa\":\"Sync Agents Metadata Job HML\",\"ecuador-reply-entrypoint-hml\":\"whatsapp-reply-entrypoint-hml\",\"ecuador-reply-entrypoint-prod\":\"whatsapp-reply-entrypoint-prod\",\"5MryyxAbTHsb89yVx3FAe\":\"agent-send-message-entrypoint\",\"TZtOuDXGQh24VDN9CGov0\":\"PingPong KeyVault\",\"13rnkmw96E5MrquKnnI_M\":\"agent-logs-entrypoint\",\"3NwM07fRAAi2cON2KpJi_\":\"Expire Inactive Transfers Job HML\",\"ZOpJU13MlsqJNZNScpZrs\":\"Sync Agents Metadata Job PRD\",\"0ScwQIaOMBUIdDb1x5q9R\":\"transfer-operation-entrypoint\",\"JtLDId2qjiu0sNsULsD4E\":\"Expire Inactive Transfers Job PRD\",\"PqjPbzpg-bFvxXV9qSA8T\":\"chat-ping-entrypoint\",\"vC7Xapg8Lx7g4wfd0e_jR\":\"Detect Abandon Job HML\",\"GFNOx0PYwKYjS8oFd7U6l\":\"Detect Abandon Job PRD\",\"argentina-reply-entrypoint\":\"argentina-reply-entrypoint-hml\",\"argentina-reply-entrypoint-prod\":\"argentina-reply-entrypoint-prod\",\"health-check-argentina\":\"health-check-argentina\",\"monitoring-argentina\":\"monitoring-argentina\",\"authenticate-user-argentina\":\"authenticate-user-argentina\",\"VXbg6FJbalOwrIJnvZIRC\":\"Copy of Peru-Authentication\",\"kr62-VRyfj1Rd58JKRaCP\":\"Copy of semantic-faq\",\"Rdzbz0sYIWKJo-DOGAS4L\":\"colombia-reply-tests\",\"VBe5cffOny7xgAxAsNd_F\":\"semantic-faq\",\"_lFzbsy_N19dug6wI83S6\":\"DEV CAll Manual Only Iterate-Queue-recursive-JOB-DEV\",\"-_As0_UIvL1WZLhYkvnEP\":\"Iterate-Queue-Auto-Healer Scheduler <- on pause due external_transfer\",\"1knLGAxuaCD2fsuMbZBH5\":\"Iterate-Queue-recursive-JOB-PRD <- on pause due external_transfer\",\"n7l-E7afYNCPYtaJzn3o_\":\"ocular-kb-entrypoint\",\"Br8NJWvy7UDUC3dKFs-De\":\"ocular-answer-entrypoint\",\"4D2touS1KIk735CijlzJu\":\"FormsSendEmail HML\",\"oN7ZFszCha8Cx6I3WefPa\":\"MAIN RUSH Forms\",\"f9mn0XCtwjCiwA2buisUM\":\"Get Seller - Rush Invoke\",\"OXlr-pFHOSL3TyqbG_C29\":\"SubMonitoringRush\",\"SSkgjL2C2_pha_4D8YoYV\":\"Recips_infobit\",\"iZpq-fSmeaLLWLeQGh2zv\":\"Reciept-Email\",\"4G3HkuB0vUfSPAjUvThB9\":\"Webhook_InfobitData2\",\"S5Smf36KVnpAgdlXt9xU4\":\"d2-chile-entrypoint-hml\",\"vRtbsuzMqlbWrwKCvjeS6\":\"d2-mexico-entrypoint-hml\",\"xZ07GjXF6J-UE889zvhNv\":\"d2-colombia-entrypoint-hml\",\"gJnyPaWs1JQSQ8CPGfzCO\":\"d2-peru-entrypoint-hml\",\"hrfpkEcajH1t8HR61CXM2\":\"d2-no-country-entrypoint-hml\",\"pzwhsvhUFlNTNfalRoReJ\":\"[IVR] - FAQ Service - Colombia\",\"6dGeYqg30On7Vl_cYSh0G\":\"voice-entrypoint-use-cases\",\"DwKOpCazNMaMtUTpBhqC2\":\"debug-voice-entrypoint-use-cases\",\"uFHAeNtMdNZXgVqN2vxit\":\"[IVR]voice-entrypoint-use-cases\",\"LmElMY813TKSrFwDI9BFd\":\"UPLOAD WIKIS Rush\",\"X0Zduvi81uXolo0eTY6Tk\":\"cari-rush\",\"flQBZ9yWONn7dYrW-6SaO\":\"cari-data2Sec\",\"2dQ8tHgWFqO0ULXJVmfvB\":\"BUG Auth Strategy\",\"Y1O0oTlVo3601Jif1BxNB\":\"d2-Argentina-entrypoint-hml\",\"5paWQC7PQmA6TT7O8jfN1\":\"Get Csat Agent\",\"p9IwlndVIpNoYWO3V0RaX\":\"Auth Endpoint\",\"fNnAMBBznAO9wvOX5V9Jf\":\"d2-peru-entrypoint-prd\",\"W_7c3KvURCeog4RW9kzlh\":\"d2-no-country-entrypoint-prd\",\"yrzdo5Ewbnp3ZaiLXV2rz\":\"d2-chile-entrypoint-prd\",\"v4LXRPYyAgktlbttvZyTN\":\"d2-Argentina-entrypoint-prd\",\"oV37yW1tSod7tpFyqKkxy\":\"d2-colombia-entrypoint-prd\",\"AKlWpcgDjGl6n-TK_j4OY\":\"d2-mexico-entrypoint-prd\",\"NDp1EgGs3Xn4IUTan3yht\":\"debug ec get titles rush\",\"vwkNOgpr4qzNJdhY4JorT\":\"MotherTester\",\"mKozj09NcLOagalg-VqXa\":\"rush-ana-monthly-report-numbers\",\"TUdsI1xRqd9fQj-2U-Sab\":\"EmailListenerRPA\",\"m3l1c0HBBXSL5ZRt4ZWsc\":\"Scheduler Close Session\",\"pEaGKSVNxejOXqsg3EGmr\":\"RPAEmailListener\",\"HmLW40Dug8t3rvhMmfXWY\":\"keycloak-oauth-validate-pin\",\"fU6X_mrSStaevLVbZqAKX\":\"keycloak-oauth-generate-pin\",\"JVf9xTeGFXyQYrusdL4TS\":\"contesta-preguntas-ecuador\",\"uDBjs8wkDtrxMpysFrTEF\":\"refactoring - select usecase\",\"L322Lg0jWzCvY6vhp-XRd\":\"refactoring - select FAQ\",\"DU7pDuhF-5peX6NA2K0XH\":\"refactoring - generate answer - usecase\",\"pvPH-HtYlHprLUXXnUzvr\":\"refactoring - generate answer - FAQ\",\"tl059gO4UkSn6l-rMKTX3\":\"Csat Endpoint FullD2\",\"ubp46aOre3B349mcfAeZy\":\"FAQs-entrypoint-maia\",\"9EjGJRul_CfpxuxkH8uuH\":\"New HTTPListener (84)\",\"bo3vvYdwZei_1njBiiiby\":\"Do NOT DELETE! - New HTTPListener (84)\",\"sNLVZ6BazGIEpFzeJUMs0\":\"EcuadorByToken\",\"QOPIchK_LVwWCzYZFRpsF\":\"NI Sessions\",\"20Q518uE-sB06h4JJRm-q\":\"New HTTPListener (88)\",\"rbeu3OSYD9XTKdkEKhS0d\":\"New EmailListener (3)\",\"WQVnm9MIxst3JmmSsNrjq\":\"Debug not found\",\"Q86M0LYUybVvjS0DCOmK1\":\"New Scheduler (9)\",\"AZ9vggxaGj14m9wlr4jJH\":\"Scheduler Agent External Catch\",\"rM74WoHiOjwclT9OqQfSN\":\"mx-filtred-chatid-logs\",\"7hjRu6sUdMD0rREQDhF5m\":\"cl-filtred-chatid-logs\",\"Pfgl_XbCvP_MbD16we1_k\":\"co-filtred-chatid-logs\",\"ngcyRzaTIYvr455tIF1kz\":\"pe-filtred-chatid-logs\",\"fr53sxeRFZ4z97aNvd0fs\":\"ar-filtred-chatid-logs\",\"XyWU5I35B-xol8vj6E68A\":\"ec-filtred-chatid-logs\",\"JJMYzP0l4WL2syVjR3Le5\":\"sinch-ar-filtred-logs\",\"jC_thuwiDg9xvEdlEv2W4\":\"cari-rush-filtred-logs\",\"JOBH4QXjA1tsc4k_iGjyw\":\"getData-infoMaia\",\"3O7jD2oTEfg5C-guYPntf\":\"fix env null\",\"GYCxKrHJQUBAN7ELG_6cp\":\"getTime - Forms-Rush\",\"llsRwO29b2x_HqelU2ZwK\":\"Messages Controler in Queue\",\"zKLNT-goyFRBK6gnQMwFa\":\"New HTTPListener (98)\",\"M4ZqtoUWz2tvH6eepqVIx\":\"scheduler fix complete session ended data\",\"9GWeJ6GdPB6e2N1ILfQuR\":\"Close Session Manually\",\"t318vm-mHbHCN0mCySu5g\":\"Api Executions with NO NAME\",\"SInspt5Zks0QPlfoBB3Ia\":\"API_CSAT_QUALTRICS_FORMS\",\"lGLpiM1QQvJwQKrxn8NcT\":\"Comment Duplicator <31/03\",\"zqWM3dlAnLhOUVTw_vRNS\":\"Scheduler Catch data to report cancel Order\",\"RAcxhTZr3D7Hmj37bgkCc\":\"Copy of d2-colombia-entrypoint-hml\",\"QrT1-4d2hrSFAZMKGbtA3\":\"New HTTPListener (102)\",\"5iNOjWfW6z69unNZhpPxW\":\"API_AVI_KPIS\",\"pqwZ2g4lvKhd7c1-7FltP\":\"API_AVI_KPIS_INDICADORES\",\"M8Hu0reR498Ik_baDQGtB\":\"New HTTPListener (105)\",\"fWvjFunmTK_hY0lFQbjR6\":\"Delete Old Entries In - Last Messages On Whatsapp\",\"marcos.goulart@compasso.com.br\":\"marcos.goulart@compasso.com.br\",\"lucas.quiceno@konecta.com\":\"lucas.quiceno@konecta.com\",\"juannaffah120274@gmail.com\":\"juannaffah120274@gmail.com\",\"yadirahernandez.konecta@natura.net\":\"yadirahernandez.konecta@natura.net\",\"matiasdonoso@natura.net\":\"matiasdonoso@natura.net\",\"miguelbanaga.konecta@natura.net\":\"miguelbanaga.konecta@natura.net\",\"mario.hernandez.montez@konecta.com\":\"mario.hernandez.montez@konecta.com\",\"beatrizblas@natura.net\":\"beatrizblas@natura.net\",\"franzarredondo.konecta@natura.net\":\"franzarredondo.konecta@natura.net\",\"7S7Hyo83QcGUTiqlInuJ6\":\"Recover Session Cognito SSO\",\"jHH4vH-HfLIqjKwuRVnFN\":\"ByApp Get Access Token\",\"95PiFygoYiWmUMlXyUaVO\":\"Get Identification\",\"0lwqRkwQuKtdvTxUVGkLc\":\"ByApp Login Cognito\",\"nAbzjin-OAtGLyjT4HBcS\":\"auth\",\"Ab7idSsG5qnCuHnSt4Sth\":\"auth\",\"_Y8ErcnjwjFDJGoypCku-\":\"Simulated RNG\",\"n5V4pDXzViovabrABJ3Sa\":\"Login AuthStrategy\",\"xTGDjqJnZlkEr-Efnd_PA\":\"Get Commercial Plan Level\",\"gX1XZ1myNOJE8eWrqzf5E\":\"Forms - Get Order Status\",\"-sW9Cwx3Poy_sC--_eFGV\":\"ByApp Get Orders\",\"5xhtg5iciI4GAusd7mM8j\":\"Get Order\",\"EgVlonyaVuTjEinkMfwee\":\"Get Order Tracking\",\"ZKBfYYV6nJA6U8YuXUnOU\":\"ByUser Get Orders\",\"rdYXqMVMxvrmygC1ljokS\":\"Pruebas - avi-maia-summary\",\"Z0z2Ad3BWuifDoSvwg9bk\":\"Forms - Get Identification\",\"cPtXjN-uKuTjMmjz4iq4k\":\"Logout Cognito SSO\",\"QeVl2zKh7-Zw_LfDBcweV\":\"setSessionSinch\",\"PnM2XdWvKd1htfdws3fj3\":\"getAccessToken\",\"cPvq-1-UoVxmU4SSWvsQA\":\"Get Commercial Info/Cycle\",\"wiZulzXutcFa2uHNdBlAn\":\"Get Debts\",\"yF6mezl6qJSS2xBYsEpLb\":\"ByUser Get Titles\",\"I_8ajVIPH9ac2P38SZSx1\":\"openInstallments\",\"mHfIv05ajjXgUyiWKU0OF\":\"Forms - Get Commercial Info/Cycle\",\"ZDl5ybjN8IBaNMaG3a5IM\":\"Renegociated Confirm\",\"aFS_P9us1rGe-685INq0Y\":\"SendEmail\",\"53RsFIjttHcjRUlzSv6YT\":\"Get Invoice List - CL\",\"C9hY252FyX2F2bIo0tv-n\":\"Get_invoice_List\",\"nUZes_SiNTol9ElyUEGMk\":\"Send Message Generic\",\"xVjkn6C5qVKs2IKrEqtSD\":\"Get Consultant RPP\",\"9q60S-3lhT5aRGq8FI_Cl\":\"Orders PLO\",\"servicodados.ibge.gov.br\":\"servicodados.ibge.gov.br\",\"minegocio.natura.cl\":\"minegocio.natura.cl\",\"Ki_ulS3kgjJNX2hQ7pqa6\":\"Pruebas - avi-maia-response\",\"uFJ_RvYtV4K7ajLL62OFu\":\"ByApp Get Titles\",\"api.ipify.org\":\"api.ipify.org\",\"7PsCaVB318fFoI6Ayriuq\":\"ByUser Get Personal Data\",\"7QamP75O45S6Pe3_N9040\":\"API DAP Natura Argentina (Solicitar Status Reclamo)\",\"kSbRDpqY6Gjv8_a-A_Trl\":\"Forms Get Order Tracking\",\"V0TLUGWMKmvfqFiKuysYN\":\"ByApp Get Personal Data\",\"4TQRG38mpESi3kn90-MlX\":\"Installments RNG\",\"1h0QH6HlitlnZAZjSD6cG\":\"Forms - Get Debts\",\"Ydoyan7ka7qNLs9ADJ_Cw\":\"createToken\",\"GLjWiNVRi5t_4hUxPbwZ1\":\"PlansPayments\",\"UbBSYq36erHgfXz7XmqYT\":\"UbBSYq36erHgfXz7XmqYT\",\"TXHZKimIWspmFkqiJtpQd\":\"Get Order Status\",\"natura.data2.link\":\"natura.data2.link\",\"xMGTflJ1gKnNzxm9egYDt\":\"sendMsg\",\"13d0zDdtNFu0zJELsTakH\":\"getConversationStatus\",\"MqFGADgqzYcg2STmlisyV\":\"Get Credits Available\",\"YJgVwnTOFE_vAQFvJen-y\":\"ByUser Get Access Token\",\"fSv4XHNVXa19WyF3oh3TH\":\"cari-reports-transbordo conversationsAgente\",\"L-GMwJEzcjUX-uy_CQ7oC\":\"Forms - Get Commercial Plan Level\",\"71HLZyuKp3QFckaxb_337\":\"Get Seller\",\"01goN8nVKyDKItUYKfQPC\":\"cari-reports-transbordo create token\",\"jB-I4tPIUNi-bw5FEakNK\":\"Send Message Text\",\"_jyjA1UbujSVgM7SHW8fM\":\"Get Orders to Cancel\",\"ncf-apigw.prd.naturacloud.com\":\"ncf-apigw.prd.naturacloud.com\",\"TlHD_VRaJVvvdIsqYRuP6\":\"Forms - Get Item Request\",\"FvP2nDE_XMFHbhqwXrhOv\":\"createConversation\",\"cgF0jMqZZLP3Txu-PAQVP\":\"Get Invoice 2\",\"PDlvBO7dv7xycUSzhCurQ\":\"Create Case\",\"natura.data2apis.com\":\"natura.data2apis.com\",\"vxMiuzvIMupWAYtrZEG2H\":\"openDebtInstallments\",\"d2-rush-http-logs--V8QitQ-Oe6s4HIBAaMK2M\":\"[Data2] Table for Rush HTTPListener V8QitQ-Oe6s4HIBAaMK2M\",\"d2-rush-scheduler-logs--vC7Xapg8Lx7g4wfd0e_jR\":\"[Data2] Table for Rush Scheduler vC7Xapg8Lx7g4wfd0e_jR\",\"d2-rush-scheduler-logs--3NwM07fRAAi2cON2KpJi_\":\"[Data2] Table for Rush Scheduler 3NwM07fRAAi2cON2KpJi_\",\"d2-rush-scheduler-logs--ZOpJU13MlsqJNZNScpZrs\":\"[Data2] Table for Rush Scheduler ZOpJU13MlsqJNZNScpZrs\",\"d2-rush-http-logs--1KOo2Xz5PfRHlG3X0fQza\":\"[Data2] Table for Rush HTTPListener 1KOo2Xz5PfRHlG3X0fQza\",\"d2-rush-http-logs--mg2bxYVq7OiC4BPR_yCAH\":\"[Data2] Table for Rush HTTPListener mg2bxYVq7OiC4BPR_yCAH\",\"d2-rush-email-logs--TUdsI1xRqd9fQj-2U-Sab\":\"[Data2] Table for Rush EmailListener TUdsI1xRqd9fQj-2U-Sab\",\"d2-rush-scheduler-logs--t318vm-mHbHCN0mCySu5g\":\"[Data2] Table for Rush Scheduler t318vm-mHbHCN0mCySu5g\",\"d2-rush-http-logs--9dUswBQShRRYePEdnupQd\":\"[Data2] Table for Rush HTTPListener 9dUswBQShRRYePEdnupQd\",\"d2-rush-email-logs--rbeu3OSYD9XTKdkEKhS0d\":\"[Data2] Table for Rush EmailListener rbeu3OSYD9XTKdkEKhS0d\",\"d2-rush-scheduler-logs--3O7jD2oTEfg5C-guYPntf\":\"[Data2] Table for Rush Scheduler 3O7jD2oTEfg5C-guYPntf\",\"d2-rush-email-logs--pEaGKSVNxejOXqsg3EGmr\":\"[Data2] Table for Rush EmailListener pEaGKSVNxejOXqsg3EGmr\",\"d2-rush-scheduler-logs--AZ9vggxaGj14m9wlr4jJH\":\"[Data2] Table for Rush Scheduler AZ9vggxaGj14m9wlr4jJH\",\"d2-rush-http-logs--2QF7k4LV6r3lNKD2DQIY-\":\"[Data2] Table for Rush HTTPListener 2QF7k4LV6r3lNKD2DQIY-\",\"d2-rush-http-logs--mje-uNOteDESvSNXHLZLT\":\"[Data2] Table for Rush HTTPListener mje-uNOteDESvSNXHLZLT\",\"d2-rush-http-logs--az8YMS-6UsEk3gOxh7SR9\":\"[Data2] Table for Rush HTTPListener az8YMS-6UsEk3gOxh7SR9\",\"d2-rush-http-logs--e4HeHh8afr0hGzLYIOudK\":\"[Data2] Table for Rush HTTPListener e4HeHh8afr0hGzLYIOudK\",\"d2-rush-http-logs--bLxMPfXtKiY54py-DIndG\":\"[Data2] Table for Rush HTTPListener bLxMPfXtKiY54py-DIndG\",\"d2-rush-scheduler-logs--fWvjFunmTK_hY0lFQbjR6\":\"[Data2] Table for Rush Scheduler fWvjFunmTK_hY0lFQbjR6\",\"d2-rush-http-logs--urEFLyhpWyo4tai4rgsu4\":\"[Data2] Table for Rush HTTPListener reply-entrypoint-hml\",\"d2-rush-http-logs--ZOKczRUH8FwbvRdp293FH\":\"[Data2] Table for Rush HTTPListener ZOKczRUH8FwbvRdp293FH\",\"d2-rush-http-logs--HpaVieRijvQmlDd_Fq3Ik\":\"[Data2] Table for Rush HTTPListener HpaVieRijvQmlDd_Fq3Ik\",\"d2-rush-http-logs--SUrn4fG4goJSOSiy27kA-\":\"[Data2] Table for Rush HTTPListener reply-entrypoint-prod\",\"d2-rush-scheduler-logs--y4A1QBJj6PZjGjz0P5SzW\":\"[Data2] Table for Rush Scheduler y4A1QBJj6PZjGjz0P5SzW\",\"d2-rush-scheduler-logs--GFNOx0PYwKYjS8oFd7U6l\":\"[Data2] Table for Rush Scheduler GFNOx0PYwKYjS8oFd7U6l\",\"d2-rush-http-logs--J7aRLvdtz2jswiaKO6LbB\":\"[Data2] Table for Rush HTTPListener J7aRLvdtz2jswiaKO6LbB\",\"d2-rush-http-logs--4qw7g77IOEF6FhhXPBl0h\":\"[Data2] Table for Rush HTTPListener 4qw7g77IOEF6FhhXPBl0h\",\"1y2dVFvEk9dRjkXAgCJhS\":\"DEBUG -  Gabz + Lucas - websocket\",\"d2-rush-http-logs--y_Cx63lH0OkYZL9Fwh_vt\":\"[Data2] Table for Rush HTTPListener y_Cx63lH0OkYZL9Fwh_vt\",\"d2-rush-scheduler-logs--llsRwO29b2x_HqelU2ZwK\":\"[Data2] Table for Rush Scheduler llsRwO29b2x_HqelU2ZwK\",\"d2-rush-scheduler-logs--lGLpiM1QQvJwQKrxn8NcT\":\"[Data2] Table for Rush Scheduler lGLpiM1QQvJwQKrxn8NcT\",\"d2-rush-scheduler-logs--JtLDId2qjiu0sNsULsD4E\":\"[Data2] Table for Rush Scheduler JtLDId2qjiu0sNsULsD4E\",\"d2-rush-http-logs--VAuRdAFOi6fWHKsyNSDzu\":\"[Data2] Table for Rush HTTPListener VAuRdAFOi6fWHKsyNSDzu\",\"d2-rush-http-logs--cFIbi56WTtLzvP98SJGm8\":\"[Data2] Table for Rush HTTPListener cFIbi56WTtLzvP98SJGm8\",\"d2-rush-scheduler-logs---qI3WX_wNZ89B9SFb-IR6\":\"[Data2] Table for Rush Scheduler -qI3WX_wNZ89B9SFb-IR6\",\"d2-rush-http-logs--kknIKeY43H6sF45qYklwG\":\"[Data2] Table for Rush HTTPListener kknIKeY43H6sF45qYklwG\",\"d2-rush-http-logs--FMBXNnA9VIwNuFZwLR5l-\":\"[Data2] Table for Rush HTTPListener FMBXNnA9VIwNuFZwLR5l-\",\"d2-rush-scheduler-logs--M4ZqtoUWz2tvH6eepqVIx\":\"[Data2] Table for Rush Scheduler M4ZqtoUWz2tvH6eepqVIx\",\"d2-rush-scheduler-logs---_As0_UIvL1WZLhYkvnEP\":\"[Data2] Table for Rush Scheduler -_As0_UIvL1WZLhYkvnEP\",\"d2-rush-email-logs--iZpq-fSmeaLLWLeQGh2zv\":\"[Data2] Table for Rush EmailListener iZpq-fSmeaLLWLeQGh2zv\",\"d2-rush-scheduler-logs--Q86M0LYUybVvjS0DCOmK1\":\"[Data2] Table for Rush Scheduler Q86M0LYUybVvjS0DCOmK1\",\"d2-rush-scheduler-logs--m3l1c0HBBXSL5ZRt4ZWsc\":\"[Data2] Table for Rush Scheduler m3l1c0HBBXSL5ZRt4ZWsc\",\"d2-rush-http-logs--Opk-wkyI836qAOLiMO74w\":\"[Data2] Table for Rush HTTPListener Opk-wkyI836qAOLiMO74w\",\"d2-rush-http-logs--wWGIe_bke4WM_zXH1L1aX\":\"[Data2] Table for Rush HTTPListener wWGIe_bke4WM_zXH1L1aX\",\"d2-rush-http-logs--CrHuxYw5OO6UhaArGlI5k\":\"[Data2] Table for Rush HTTPListener CrHuxYw5OO6UhaArGlI5k\",\"d2-rush-scheduler-logs--zqWM3dlAnLhOUVTw_vRNS\":\"[Data2] Table for Rush Scheduler zqWM3dlAnLhOUVTw_vRNS\",\"d2-rush-http-logs--8m5oC8BGWY9_2gDfSX4Iz\":\"[Data2] Table for Rush HTTPListener 8m5oC8BGWY9_2gDfSX4Iz\",\"d2-rush-http-logs--1qcOzRJwbJOernHNczqre\":\"[Data2] Table for Rush HTTPListener 1qcOzRJwbJOernHNczqre\",\"d2-rush-http-logs--kQOalUChW01nEtOgbyvhR\":\"[Data2] Table for Rush HTTPListener kQOalUChW01nEtOgbyvhR\",\"d2-rush-http-logs--bkvgqkvfFtk47z85tdiFi\":\"[Data2] Table for Rush HTTPListener bkvgqkvfFtk47z85tdiFi\",\"d2-rush-http-logs--vw5jqNOxmOIGuMTAPu0-N\":\"[Data2] Table for Rush HTTPListener vw5jqNOxmOIGuMTAPu0-N\",\"d2-rush-scheduler-logs--di5-RlyrJ7LB5lv9z26xa\":\"[Data2] Table for Rush Scheduler di5-RlyrJ7LB5lv9z26xa\",\"d2-rush-http-logs--FZopMq_0TAjKgwluiTO2O\":\"[Data2] Table for Rush HTTPListener FZopMq_0TAjKgwluiTO2O\",\"d2-rush-scheduler-logs--QOPIchK_LVwWCzYZFRpsF\":\"[Data2] Table for Rush Scheduler QOPIchK_LVwWCzYZFRpsF\",\"d2-rush-http-logs--\":\"d2-rush-http-logs--\",\"d2-rush-http-logs--nHyPjGyfyfZBDbjEAVajv\":\"[Data2] Table for Rush HTTPListener nHyPjGyfyfZBDbjEAVajv\",\"E3oMcXu2Bz9ePMI65_C22\":\"E3oMcXu2Bz9ePMI65_C22\",\"-nMQ7qe1f0wjvb0ZvWN7A\":\"-nMQ7qe1f0wjvb0ZvWN7A\",\"7S26W0shHU9I5Ebt_WENT\":\"7S26W0shHU9I5Ebt_WENT\",\"Gtdb73GMSQ-A7BRxxfQN_\":\"Gtdb73GMSQ-A7BRxxfQN_\",\"d2-rush-http-logs--vE0mWtHWUlK5Ewz_03Tmw\":\"[Data2] Table for Rush HTTPListener vE0mWtHWUlK5Ewz_03Tmw\",\"d2-rush-http-logs--NvYvE6tTC1rTOFw_fBnql\":\"[Data2] Table for Rush HTTPListener NvYvE6tTC1rTOFw_fBnql\",\"66DHzRqOE1a1tRB0zq0ZY\":\"66DHzRqOE1a1tRB0zq0ZY\",\"UVM5gJF_mQ7tMG-YqS2nS\":\"UVM5gJF_mQ7tMG-YqS2nS\",\"d2-rush-http-logs--SH-rJXkaDoNGb7t6iobR-\":\"[Data2] Table for Rush HTTPListener SH-rJXkaDoNGb7t6iobR-\",\"d2-rush-http-logs--uBEjYWYbhiaNoFYM7dRdM\":\"[Data2] Table for Rush HTTPListener uBEjYWYbhiaNoFYM7dRdM\",\"Qpiav1VtDm8y85OYEoKaJ\":\"Qpiav1VtDm8y85OYEoKaJ\",\"d2-rush-http-logs--xTZ1L6-oE99tVZXJKSEC5\":\"d2-rush-http-logs--xTZ1L6-oE99tVZXJKSEC5\",\"WTiB4xcYb-Ice4p8GdC6z\":\"WTiB4xcYb-Ice4p8GdC6z\",\"d2-rush-http-logs--7FsZi-TUPSiSL4AmoUn7G\":\"[Data2] Table for Rush HTTPListener 7FsZi-TUPSiSL4AmoUn7G\",\"ic2M2f2TdC-Zn2TTHl59p\":\"ic2M2f2TdC-Zn2TTHl59p\",\"d2-rush-http-logs--DR7-lmFBnrdB09Sn4y5xW\":\"[Data2] Table for Rush HTTPListener DR7-lmFBnrdB09Sn4y5xW\",\"r9JxZDCF29EVaqVsssCBC\":\"r9JxZDCF29EVaqVsssCBC\",\"B1nSxZNv12IEgCn-WaYDp\":\"B1nSxZNv12IEgCn-WaYDp\",\"d2-rush-http-logs--4v3kV-PEoKcs1Wzw7zZpS\":\"[Data2] Table for Rush HTTPListener 4v3kV-PEoKcs1Wzw7zZpS\",\"d2-rush-http-logs--DKxN41CrxkSmxm3s7uB7o\":\"[Data2] Table for Rush HTTPListener DKxN41CrxkSmxm3s7uB7o\",\"d2-rush-http-logs--6j8Rw4f3JbnLu_A521iUs\":\"[Data2] Table for Rush HTTPListener 6j8Rw4f3JbnLu_A521iUs\",\"d2-rush-http-logs--uU8XSekkEUy7S8ANqlxYw\":\"[Data2] Table for Rush HTTPListener uU8XSekkEUy7S8ANqlxYw\",\"2wqCAk3cQqrAzjxc6yWDd\":\"2wqCAk3cQqrAzjxc6yWDd\",\"d2-rush-http-logs--AKIWpcgDjIGn-TK_j4OY\":\"d2-rush-http-logs--AKIWpcgDjIGn-TK_j4OY\",\"pBNp_Dx5lVVKPG26mvda7\":\"pBNp_Dx5lVVKPG26mvda7\",\"fvb1vSq-BsQ5G8WSGRNm_\":\"fvb1vSq-BsQ5G8WSGRNm_\",\"d2-rush-http-logs--hnZ2W96iFh-TqHnkCoG9K\":\"[Data2] Table for Rush HTTPListener hnZ2W96iFh-TqHnkCoG9K\",\"d2-rush-http-logs--OawAp0CkoHZ39VXUJFx14\":\"[Data2] Table for Rush HTTPListener OawAp0CkoHZ39VXUJFx14\",\"gmNR7mtDr3FudrjncHH1o\":\"gmNR7mtDr3FudrjncHH1o\",\"c1OFEYxWyhrQBxg7EQj0Q\":\"c1OFEYxWyhrQBxg7EQj0Q\",\"UvUbQsAi1RlnrX0AgGJep\":\"UvUbQsAi1RlnrX0AgGJep\",\"d2-rush-email-logs--4sJtg3IlZKvtb4rD8if4l\":\"[Data2] Table for Rush EmailListener 4sJtg3IlZKvtb4rD8if4l\",\"Q_X3yLfPYSkT1_iQnvwb5\":\"Q_X3yLfPYSkT1_iQnvwb5\",\"d2-rush-http-logs--YeUPT1GLa2TSUqjIEVHT2\":\"[Data2] Table for Rush HTTPListener YeUPT1GLa2TSUqjIEVHT2\",\"d2-rush-http-logs--DPqgghE_YHVgl5KEqwnIr\":\"[Data2] Table for Rush HTTPListener DPqgghE_YHVgl5KEqwnIr\",\"d2-rush-http-logs--jBoEeRucjC6BQ3jF4-X52\":\"[Data2] Table for Rush HTTPListener jBoEeRucjC6BQ3jF4-X52\",\"d2-rush-http-logs--k2TMMnbqr_Y_GYcoTZBHg\":\"[Data2] Table for Rush HTTPListener k2TMMnbqr_Y_GYcoTZBHg\",\"d2-rush-http-logs--1STC6oYDzP4UMk3y374qF\":\"[Data2] Table for Rush HTTPListener 1STC6oYDzP4UMk3y374qF\",\"d2-rush-http-logs--IHLWZillcz2W9ubjBx_01\":\"[Data2] Table for Rush HTTPListener IHLWZillcz2W9ubjBx_01\",\"QfISC3DFnqpEyd9TRJAzZ\":\"QfISC3DFnqpEyd9TRJAzZ\",\"d2-rush-http-logs--puL6J6fn-ylMtJ9n-o7KY\":\"d2-rush-http-logs--puL6J6fn-ylMtJ9n-o7KY\",\"d2-rush-scheduler-logs--U0oxiXTH_M-3ikGqZYXjd\":\"[Data2] Table for Rush Scheduler U0oxiXTH_M-3ikGqZYXjd\",\"Moiuq_CX03a61mFT7Klnp\":\"Moiuq_CX03a61mFT7Klnp\",\"L_C3xozqUlk-A8px5QLTb\":\"L_C3xozqUlk-A8px5QLTb\",\"HVxgx7XA2lKt_KnBjUBTB\":\"HVxgx7XA2lKt_KnBjUBTB\",\"TYxV2_I-GPn8oDPUky0fN\":\"TYxV2_I-GPn8oDPUky0fN\",\"d2-rush-http-logs--qDCzU0TUwF0l-yJ8O7gmu\":\"d2-rush-http-logs--qDCzU0TUwF0l-yJ8O7gmu\",\"d2-rush-http-logs--B3TO32lZOPuzEW-QljeZ3\":\"d2-rush-http-logs--B3TO32lZOPuzEW-QljeZ3\",\"d2-rush-http-logs--4h7IkBcYJymBD6yYUMM6W\":\"[Data2] Table for Rush HTTPListener 4h7IkBcYJymBD6yYUMM6W\",\"l2jV2RWHvaB4mr_C4ScGk\":\"l2jV2RWHvaB4mr_C4ScGk\",\"YbeJHrjYPYEphsGItCIb-\":\"YbeJHrjYPYEphsGItCIb-\",\"BMHq0NizHgDZZWeyl-0T0\":\"BMHq0NizHgDZZWeyl-0T0\",\"y1Tzq6qstzA17p_5zl-tZ\":\"y1Tzq6qstzA17p_5zl-tZ\",\"OT856KO8nvmNWfitOmOyK\":\"OT856KO8nvmNWfitOmOyK\",\"d2-rush-http-logs--arcr-7cfBBuibKldVkW5u\":\"d2-rush-http-logs--arcr-7cfBBuibKldVkW5u\",\"d2-rush-scheduler-logs--42jKFZ2UKFWGKH8fqix61\":\"d2-rush-scheduler-logs--42jKFZ2UKFWGKH8fqix61\",\"Dggo063CYnSQhYdR1Hmwu\":\"Dggo063CYnSQhYdR1Hmwu\",\"Ha0OqTqmskfV76pmwQlnT\":\"Ha0OqTqmskfV76pmwQlnT\",\"d2-rush-email-logs--aAuM_I24P322rGD0eZ-UG\":\"[Data2] Table for Rush EmailListener aAuM_I24P322rGD0eZ-UG\",\"k9Sux5mYrSspxhuCiDvTh\":\"k9Sux5mYrSspxhuCiDvTh\",\"HgbZAnQAkWOiuMaE6oZ8g\":\"HgbZAnQAkWOiuMaE6oZ8g\",\"jsX0qbjTb9gardw8U-Vie\":\"jsX0qbjTb9gardw8U-Vie\",\"d2-rush-http-logs--ZcM9eKM2Ya4g7QyZobKFr\":\"[Data2] Table for Rush HTTPListener ZcM9eKM2Ya4g7QyZobKFr\",\"2AETGaOTk7ZGHxtqmDW7v\":\"2AETGaOTk7ZGHxtqmDW7v\",\"THatiho8WzGHrJLPdMwp6\":\"THatiho8WzGHrJLPdMwp6\",\"d2-rush-http-logs--w87kUf8cR2vVtWvmlX5hT\":\"[Data2] Table for Rush HTTPListener w87kUf8cR2vVtWvmlX5hT\",\"pB5o5dIVOI3dFPYh4Z5-6\":\"pB5o5dIVOI3dFPYh4Z5-6\",\"d2-rush-scheduler-logs--AJXZof1A1pfyQzfgrFTHX\":\"[Data2] Table for Rush Scheduler AJXZof1A1pfyQzfgrFTHX\",\"d2-rush-http-logs--RXY80sBle-SCzqcktQdVA\":\"d2-rush-http-logs--RXY80sBle-SCzqcktQdVA\",\"d2-rush-http-logs--oJaRNIv2Ecb1vbhAPs9_-\":\"[Data2] Table for Rush HTTPListener oJaRNIv2Ecb1vbhAPs9_-\",\"vWu3lcHae7GkAjo23jxe4\":\"vWu3lcHae7GkAjo23jxe4\",\"d2-rush-http-logs--8lEbF3hNdqDbHJWIrBRgh\":\"d2-rush-http-logs--8lEbF3hNdqDbHJWIrBRgh\",\"Cc19NKR5j4Bo17AMwT3mL\":\"Cc19NKR5j4Bo17AMwT3mL\",\"09_HqPBV3JTqoThr3oJ7o\":\"09_HqPBV3JTqoThr3oJ7o\",\"y8sxzSs2FFwpETtX4V_AA\":\"y8sxzSs2FFwpETtX4V_AA\",\"EXGIIYT_N9-Iu78i15bQJ\":\"EXGIIYT_N9-Iu78i15bQJ\",\"d2-rush-http-logs--Ocxwf0-R7gpqn6mVrHBDu\":\"[Data2] Table for Rush HTTPListener Ocxwf0-R7gpqn6mVrHBDu\",\"8eNIKjTqm0_aqg0aZBblJ\":\"8eNIKjTqm0_aqg0aZBblJ\",\"zQRMlahLTX69P7LDPxVVS\":\"zQRMlahLTX69P7LDPxVVS\",\"rz80Il3C4_IbjUhlBB0N-\":\"rz80Il3C4_IbjUhlBB0N-\",\"d2-rush-http-logs--EHk7sTcGvfp2_VaaGHEk1\":\"[Data2] Table for Rush HTTPListener EHk7sTcGvfp2_VaaGHEk1\",\"KzZA2Ck_fPz8-mIUWrmPD\":\"KzZA2Ck_fPz8-mIUWrmPD\",\"YwJapjarCLY13f0zc6IIR\":\"YwJapjarCLY13f0zc6IIR\",\"d2-rush-http-logs--eBYfi0salfrHMOi2kgw6m\":\"[Data2] Table for Rush HTTPListener eBYfi0salfrHMOi2kgw6m\",\"d2-rush-http-logs--BN8jO-BDUEpxB9_AcM6Kg\":\"[Data2] Table for Rush HTTPListener BN8jO-BDUEpxB9_AcM6Kg\",\"IiqUenZ70kgMYe3d_kBBl\":\"IiqUenZ70kgMYe3d_kBBl\",\"d2-rush-email-logs--i5W24aHHKkJ5EKal7HyMd\":\"[Data2] Table for Rush EmailListener i5W24aHHKkJ5EKal7HyMd\",\"WUN-aj69Vl9R3yWSlkcd8\":\"WUN-aj69Vl9R3yWSlkcd8\",\"69WbiV9UOJ3xZwQeeFDs2\":\"69WbiV9UOJ3xZwQeeFDs2\",\"ZlA4-caSzV3wDlK3MlRjk\":\"ZlA4-caSzV3wDlK3MlRjk\",\"F89DTQF9zQ44GlaDFSEfL\":\"F89DTQF9zQ44GlaDFSEfL\",\"d2-rush-http-logs--lgu1CDTK7tymaAKKAgQA7\":\"[Data2] Table for Rush HTTPListener lgu1CDTK7tymaAKKAgQA7\",\"zMFaDYo-12Ow6ANJN53jE\":\"zMFaDYo-12Ow6ANJN53jE\",\"rjtnt4zvEXGBf6iilTUKx\":\"rjtnt4zvEXGBf6iilTUKx\",\"d2-rush-http-logs--hRYzykKXhz5KAbvFgGFg-\":\"d2-rush-http-logs--hRYzykKXhz5KAbvFgGFg-\",\"d2-rush-http-logs--THZG9ndzCZtpFiaFkGq8Z\":\"[Data2] Table for Rush HTTPListener THZG9ndzCZtpFiaFkGq8Z\",\"Kp3gg4R5MJ4_hUqPVvCMu\":\"Kp3gg4R5MJ4_hUqPVvCMu\",\"d2-rush-http-logs--d7DkLtfBjAMC83AedZQFe\":\"d2-rush-http-logs--d7DkLtfBjAMC83AedZQFe\",\"d2-rush-scheduler-logs--0Rpoh-mHN6e4fkJYzRu6P\":\"d2-rush-scheduler-logs--0Rpoh-mHN6e4fkJYzRu6P\",\"FapLkdYHTOx5Ucn-XSEou\":\"FapLkdYHTOx5Ucn-XSEou\",\"SbaOQyd4ieq-3j5KFzur0\":\"SbaOQyd4ieq-3j5KFzur0\",\"d2-rush-http-logs--PBVmWvvcmhTuhT3FRYbsK-prod\":\"[Data2] Table for Rush HTTPListener PBVmWvvcmhTuhT3FRYbsK-prod\",\"fU-sT2IR-BpT0CcgFpUil\":\"fU-sT2IR-BpT0CcgFpUil\",\"d2-rush-http-logs--9C7zGkNKkKdVqzd3zqNjM\":\"d2-rush-http-logs--9C7zGkNKkKdVqzd3zqNjM\",\"EbvJyddiPgCCuTUdSfw9S\":\"EbvJyddiPgCCuTUdSfw9S\",\"d2-rush-http-logs--a97y0eYSXO7J9ozN4oTz1\":\"[Data2] Table for Rush HTTPListener a97y0eYSXO7J9ozN4oTz1\",\"d2-rush-http-logs--py881QfJuzQ72gtNZPcXD\":\"[Data2] Table for Rush HTTPListener py881QfJuzQ72gtNZPcXD\",\"d2-rush-http-logs--rLSU_-d3Wm3ANqAZyAD4l\":\"[Data2] Table for Rush HTTPListener rLSU_-d3Wm3ANqAZyAD4l\",\"d2-rush-http-logs--Co8i3u9GVuwSQZLlVdC_7\":\"[Data2] Table for Rush HTTPListener Co8i3u9GVuwSQZLlVdC_7\",\"ilBDwH1WEmRRZlm6cKipM\":\"ilBDwH1WEmRRZlm6cKipM\",\"gHWkvNwOYN_Es8XlXojWf\":\"gHWkvNwOYN_Es8XlXojWf\",\"pExvlFJA9RI_MG7Vv1nna\":\"pExvlFJA9RI_MG7Vv1nna\",\"jkCAKezI3sXJzA1x4GzNY\":\"jkCAKezI3sXJzA1x4GzNY\",\"99Mz4W3ULyVDSXo6oz1Io\":\"99Mz4W3ULyVDSXo6oz1Io\",\"d2-rush-http-logs--ALEeEFrl8TvthcCSejWl9\":\"d2-rush-http-logs--ALEeEFrl8TvthcCSejWl9\",\"8OpRfxW3awFos-II88jvs\":\"8OpRfxW3awFos-II88jvs\",\"3OSaEvz9hY2EWfhC1JOAM\":\"3OSaEvz9hY2EWfhC1JOAM\",\"d2-rush-http-logs--1L2mlmHPJfV-WnkP6aScQ\":\"[Data2] Table for Rush HTTPListener 1L2mlmHPJfV-WnkP6aScQ\",\"e475439WuGwTt9OHsX0Z1\":\"e475439WuGwTt9OHsX0Z1\",\"19B_46q6_lF93v6zW139k\":\"19B_46q6_lF93v6zW139k\",\"d2-rush-http-logs--Do0_kxMBctk9pzxR2MwMj\":\"[Data2] Table for Rush HTTPListener Do0_kxMBctk9pzxR2MwMj\",\"cgGv9RZJuHKysdIsa3Cos\":\"cgGv9RZJuHKysdIsa3Cos\",\"w5KcP0bHW4jRhbLBRar7C\":\"w5KcP0bHW4jRhbLBRar7C\",\"d2-rush-http-logs--YPN1DYpUMI_ziUXiBHQqa\":\"d2-rush-http-logs--YPN1DYpUMI_ziUXiBHQqa\",\"d2-rush-http-logs--x6ZIVYSadsQLQ4DPBdmzr\":\"[Data2] Table for Rush HTTPListener x6ZIVYSadsQLQ4DPBdmzr\",\"d2-rush-http-logs--rToYJDwuN6bpnaAmN4v0h\":\"[Data2] Table for Rush HTTPListener rToYJDwuN6bpnaAmN4v0h\",\"d2-rush-http-logs--iMJWRn1GPjx1dQd46vueZ\":\"[Data2] Table for Rush HTTPListener iMJWRn1GPjx1dQd46vueZ\",\"1HmzK7srgRVGEc_TG4XMP\":\"1HmzK7srgRVGEc_TG4XMP\",\"d2-rush-http-logs--vJOJE-UAe7qHpAHqfQF1v\":\"[Data2] Table for Rush HTTPListener vJOJE-UAe7qHpAHqfQF1v\",\"AAIsd-mcUjlkQvZNKKBvl\":\"AAIsd-mcUjlkQvZNKKBvl\",\"DQ1Es4gTIzPags8NRUX6M\":\"DQ1Es4gTIzPags8NRUX6M\",\"d2-rush-http-logs--rQSpj5Fdj0KlsOHGk4mkQ\":\"[Data2] Table for Rush HTTPListener rQSpj5Fdj0KlsOHGk4mkQ\",\"d2-rush-scheduler-logs--woRd0L6WbsPVs2Gu1S6Zb\":\"[Data2] Table for Rush Scheduler woRd0L6WbsPVs2Gu1S6Zb\",\"gd9Tkk1HBicRB_pUH4xpv\":\"gd9Tkk1HBicRB_pUH4xpv\",\"d2-rush-scheduler-logs--b-5nsf8z83C9P54GttaGJ\":\"[Data2] Table for Rush Scheduler b-5nsf8z83C9P54GttaGJ\",\"d2-rush-http-logs--WIUoOAOAW0Q_yVVjz5xK_\":\"[Data2] Table for Rush HTTPListener WIUoOAOAW0Q_yVVjz5xK_\",\"d2-rush-http-logs--R2d3AKF6hJrr7XFB8zXVG\":\"[Data2] Table for Rush HTTPListener R2d3AKF6hJrr7XFB8zXVG\",\"d2-rush-http-logs--VVaPfPD880sI8jg-I7uE3\":\"[Data2] Table for Rush HTTPListener VVaPfPD880sI8jg-I7uE3\",\"d2-rush-http-logs--lQGCU0VVQfx9EgU1ERS1t\":\"[Data2] Table for Rush HTTPListener lQGCU0VVQfx9EgU1ERS1t\",\"aXKRU2mu8lP4ylgWJ8IzH\":\"aXKRU2mu8lP4ylgWJ8IzH\",\"d2-rush-http-logs--AtoOjhTYyjsFt7H10IS-d\":\"[Data2] Table for Rush HTTPListener AtoOjhTYyjsFt7H10IS-d\",\"J3WGi_61Hi1Ho258nhXno\":\"J3WGi_61Hi1Ho258nhXno\",\"d2-rush-http-logs--r6p6XcD4pyYrl0ZQ1svcN\":\"[Data2] Table for Rush HTTPListener r6p6XcD4pyYrl0ZQ1svcN\",\"d2-rush-http-logs--WR38gPHj6UDqYgwMG54vG\":\"d2-rush-http-logs--WR38gPHj6UDqYgwMG54vG\",\"d2-rush-http-logs--f7NKCp31JgkniBUFh76Qt\":\"d2-rush-http-logs--f7NKCp31JgkniBUFh76Qt\",\"d2-rush-http-logs--DS6I60MyNlCW9qVQEtdUw\":\"[Data2] Table for Rush HTTPListener DS6I60MyNlCW9qVQEtdUw\",\"d2-rush-http-logs---yK2Se0PzErtXYQwVvbpN\":\"d2-rush-http-logs---yK2Se0PzErtXYQwVvbpN\",\"d2-rush-http-logs--W4KLoZStjqg1g_fFeBoWz\":\"[Data2] Table for Rush HTTPListener W4KLoZStjqg1g_fFeBoWz\",\"lylWySeuwO3drhvaYZs6H\":\"lylWySeuwO3drhvaYZs6H\",\"2b6T0x4ySG0AXYFSLZmOJ\":\"2b6T0x4ySG0AXYFSLZmOJ\",\"d2-rush-http-logs--Ccz2M5lWNLMhp4f3ii05A\":\"[Data2] Table for Rush HTTPListener Ccz2M5lWNLMhp4f3ii05A\",\"d2-rush-http-logs--HlECseNcp8wUWsOh_lucr\":\"[Data2] Table for Rush HTTPListener HlECseNcp8wUWsOh_lucr\",\"iFEU_mSrXE43PQ2ADfi0F\":\"iFEU_mSrXE43PQ2ADfi0F\",\"d2-rush-http-logs--PepDJspgKp7pVswDg1CNJ\":\"[Data2] Table for Rush HTTPListener PepDJspgKp7pVswDg1CNJ\",\"KYA09k5GT8j6NTOG8fsFj\":\"KYA09k5GT8j6NTOG8fsFj\",\"d2-rush-http-logs--BSXPYZFmF9GAk_wqnDNq4\":\"[Data2] Table for Rush HTTPListener BSXPYZFmF9GAk_wqnDNq4\",\"d2-rush-http-logs--PBVmWvvcmhTuhT3FRYbsK\":\"[Data2] Table for Rush HTTPListener PBVmWvvcmhTuhT3FRYbsK\",\"66Dq5yLMEe7WQzqtzoc-4\":\"66Dq5yLMEe7WQzqtzoc-4\",\"d2-rush-scheduler-logs--BCSNMxS5gMDw4XY10fgLI\":\"d2-rush-scheduler-logs--BCSNMxS5gMDw4XY10fgLI\",\"eWUNQliaRsYHtJ99734b0\":\"eWUNQliaRsYHtJ99734b0\",\"d2-rush-scheduler-logs--o46JnsW3-7zXSdF_w8iz7\":\"[Data2] Table for Rush Scheduler o46JnsW3-7zXSdF_w8iz7\",\"NokKcBgImfpRXCPMUSTbk\":\"NokKcBgImfpRXCPMUSTbk\",\"mEXV29FcrK02IYkEgCZ9H\":\"mEXV29FcrK02IYkEgCZ9H\",\"d2-rush-http-logs--t1XgMbwbnyg-UQkXHUzqH\":\"[Data2] Table for Rush HTTPListener t1XgMbwbnyg-UQkXHUzqH\",\"d2-rush-http-logs--YbIPAstsvgrtaf87Mxa4H\":\"d2-rush-http-logs--YbIPAstsvgrtaf87Mxa4H\",\"d2-rush-http-logs--70J44PXYXQSK6F-G0nG8r\":\"[Data2] Table for Rush HTTPListener 70J44PXYXQSK6F-G0nG8r\",\"sFsZOK_SLDVu2OwQfXNte\":\"sFsZOK_SLDVu2OwQfXNte\",\"d2-rush-http-logs--7RRHaFm33n5OWAPx_DNkg\":\"[Data2] Table for Rush HTTPListener 7RRHaFm33n5OWAPx_DNkg\",\"rUb_9gg90TVTZs5Ddc-Oc\":\"rUb_9gg90TVTZs5Ddc-Oc\",\"_fBWjM5U5nYmKXqN1DJDM\":\"_fBWjM5U5nYmKXqN1DJDM\",\"d2-rush-http-logs--1woS_1lYRZbkht2UVXh-v\":\"[Data2] Table for Rush HTTPListener 1woS_1lYRZbkht2UVXh-v\",\"d2-rush-http-logs--V3Z8Jja5renlPZNRXwYie\":\"[Data2] Table for Rush HTTPListener V3Z8Jja5renlPZNRXwYie\",\"5wbmLets06QNQBfpkt8A2\":\"5wbmLets06QNQBfpkt8A2\",\"WqYUG3VpN5SOLzjd5yFnY\":\"WqYUG3VpN5SOLzjd5yFnY\",\"mUeNCdtFlWIYAYDxG_tqi\":\"mUeNCdtFlWIYAYDxG_tqi\",\"d2-rush-http-logs--cB6tEhvTiN0hcZBBwFMZv\":\"[Data2] Table for Rush HTTPListener cB6tEhvTiN0hcZBBwFMZv\",\"d2-rush-scheduler-logs--rXtMaRcc427yv0jRnOOTe\":\"[Data2] Table for Rush Scheduler rXtMaRcc427yv0jRnOOTe\",\"d2-rush-http-logs--jyCdZdlXHNB7vLOxeJjd6\":\"[Data2] Table for Rush HTTPListener jyCdZdlXHNB7vLOxeJjd6\",\"uSxYU_rPqTeBVYxU-dkMw\":\"uSxYU_rPqTeBVYxU-dkMw\",\"gsojmb-KWvMDTOIsi7n-p\":\"gsojmb-KWvMDTOIsi7n-p\",\"d2-rush-scheduler-logs--dx6SmK5gwf-W2kfTyPF4X\":\"[Data2] Table for Rush Scheduler dx6SmK5gwf-W2kfTyPF4X\",\"d2-rush-http-logs--naeyel1lCtn2Yzk4rpcRz\":\"[Data2] Table for Rush HTTPListener naeyel1lCtn2Yzk4rpcRz\",\"pIkuHqBZUvRSWtOvS6Z8I\":\"pIkuHqBZUvRSWtOvS6Z8I\",\"d2-rush-http-logs--50ZNZk9nkuyDu85-nI5ug\":\"[Data2] Table for Rush HTTPListener 50ZNZk9nkuyDu85-nI5ug\",\"fxNu5KIm858hftHzuoZp6\":\"fxNu5KIm858hftHzuoZp6\",\"tpuhR-FpIRiJbeE7IjQX4\":\"tpuhR-FpIRiJbeE7IjQX4\",\"QX_tWcOdZLbVi73z0EAJH\":\"QX_tWcOdZLbVi73z0EAJH\",\"oscvxjwysYDhQ04GAylOk\":\"oscvxjwysYDhQ04GAylOk\",\"d2-rush-scheduler-logs--jhUhf-0hRjw63ljH5qCB6\":\"[Data2] Table for Rush Scheduler jhUhf-0hRjw63ljH5qCB6\",\"rlB3BHambYG3CPR4ZOPwc\":\"rlB3BHambYG3CPR4ZOPwc\",\"d2-rush-http-logs--7DG2U7MM-pf1OS-Gnn67L\":\"[Data2] Table for Rush HTTPListener 7DG2U7MM-pf1OS-Gnn67L\",\"d2-rush-http-logs--VMM-xjy5uXiFcpBfh98Za\":\"[Data2] Table for Rush HTTPListener VMM-xjy5uXiFcpBfh98Za\",\"d2-rush-http-logs--33aqtYKKd7G5AW3yRezeA\":\"[Data2] Table for Rush HTTPListener 33aqtYKKd7G5AW3yRezeA\",\"3zuZiGtaM2sIN4ZUyt4-l\":\"3zuZiGtaM2sIN4ZUyt4-l\",\"T6QI-cT9lDbBdMxcCICkj\":\"T6QI-cT9lDbBdMxcCICkj\",\"d2-rush-http-logs--mQ7fJAIywQCq2R4ONiMVM\":\"d2-rush-http-logs--mQ7fJAIywQCq2R4ONiMVM\",\"E0-sVRq13htpYHEE5qdbX\":\"E0-sVRq13htpYHEE5qdbX\",\"d2-rush-http-logs--_RaNU58Pp4WjIXUdFxRbt\":\"d2-rush-http-logs--_RaNU58Pp4WjIXUdFxRbt\",\"d2-rush-http-logs--8dqrP8Gsd030mo5Wp35CD\":\"[Data2] Table for Rush HTTPListener 8dqrP8Gsd030mo5Wp35CD\",\"d2-rush-http-logs--EDWOEeXuK6tWJ643YZeaJ\":\"[Data2] Table for Rush HTTPListener EDWOEeXuK6tWJ643YZeaJ\",\"3XgUIwamOv6IhtGpgQfCY\":\"3XgUIwamOv6IhtGpgQfCY\",\"d2-rush-http-logs--FzZMY2uB8JrPlFu88tZp-\":\"[Data2] Table for Rush HTTPListener FzZMY2uB8JrPlFu88tZp-\",\"d2-rush-http-logs--wdCFXJClIy2iCSmGScriE\":\"[Data2] Table for Rush HTTPListener wdCFXJClIy2iCSmGScriE\",\"d2-rush-http-logs--8P22dDeq8AAEGroxRuqBg\":\"[Data2] Table for Rush HTTPListener 8P22dDeq8AAEGroxRuqBg\",\"mPW92Cz-BIokQbWnVDSGL\":\"mPW92Cz-BIokQbWnVDSGL\",\"d2-rush-http-logs--ChAi8o2yG594CfBKKCnrq\":\"[Data2] Table for Rush HTTPListener ChAi8o2yG594CfBKKCnrq\",\"d2-rush-http-logs--LCUDoFxRkXkGyiUqzUPy2\":\"[Data2] Table for Rush HTTPListener LCUDoFxRkXkGyiUqzUPy2\",\"d2-rush-http-logs--8BbxBrlug4ffVBhEEneOr\":\"[Data2] Table for Rush HTTPListener 8BbxBrlug4ffVBhEEneOr\",\"jdRJv_yqldmMKJNHp-wLa\":\"jdRJv_yqldmMKJNHp-wLa\",\"d2-rush-scheduler-logs--iLqFlsIZybFSzlTVtKNnB\":\"[Data2] Table for Rush Scheduler iLqFlsIZybFSzlTVtKNnB\",\"d2-rush-http-logs--pNhQmYeAfxRVX_Pv6jdPw\":\"[Data2] Table for Rush HTTPListener pNhQmYeAfxRVX_Pv6jdPw\",\"p7Df4gBAqIj5pqKmIpwco\":\"p7Df4gBAqIj5pqKmIpwco\",\"d2-rush-http-logs---MPi8E2jq90tIWkio4czK\":\"[Data2] Table for Rush HTTPListener -MPi8E2jq90tIWkio4czK\",\"mh4jf3eB8kSSvrA1Hrvlr\":\"mh4jf3eB8kSSvrA1Hrvlr\",\"d2-rush-http-logs--ReKM8guLSeGSgJTrovOvg\":\"[Data2] Table for Rush HTTPListener ReKM8guLSeGSgJTrovOvg\",\"tv6Z2Cgp5ONzJZw4TiLUs\":\"tv6Z2Cgp5ONzJZw4TiLUs\",\"d2-rush-http-logs--cMghiRbGAsO4-UKPNS66d\":\"d2-rush-http-logs--cMghiRbGAsO4-UKPNS66d\",\"d2-rush-http-logs--yi3c-GepkmvjFUYggnl9j\":\"[Data2] Table for Rush HTTPListener yi3c-GepkmvjFUYggnl9j\",\"d2-rush-http-logs--qx8Aw2NW2pySfInBXA51X\":\"d2-rush-http-logs--qx8Aw2NW2pySfInBXA51X\",\"d2-rush-scheduler-logs--hZltBTTitA5c9vqeHOpAm\":\"[Data2] Table for Rush Scheduler hZltBTTitA5c9vqeHOpAm\",\"d2-rush-http-logs--aXXHhwbpE141SbgXQYtp_\":\"[Data2] Table for Rush HTTPListener aXXHhwbpE141SbgXQYtp_\",\"d2-rush-http-logs--v_4RAktPKDcC6q7IRAQ5s\":\"[Data2] Table for Rush HTTPListener v_4RAktPKDcC6q7IRAQ5s\",\"d2-rush-http-logs--TYMe9YwS84ML8i5hZAdSw\":\"d2-rush-http-logs--TYMe9YwS84ML8i5hZAdSw\",\"d2-rush-http-logs--0GyUzp4cAnSnLh8Q0v6aC\":\"d2-rush-http-logs--0GyUzp4cAnSnLh8Q0v6aC\",\"d2-rush-http-logs--rYMDbMntkEtmoXgpWKAZB\":\"[Data2] Table for Rush HTTPListener rYMDbMntkEtmoXgpWKAZB\",\"d2-rush-http-logs--kSpcay0CWMjK2PKxgzHNo\":\"d2-rush-http-logs--kSpcay0CWMjK2PKxgzHNo\",\"T5JNebX6FYRCFv01uksnX\":\"T5JNebX6FYRCFv01uksnX\",\"d2-rush-scheduler-logs--X38EYIWasHbPIuB_2BIoD\":\"[Data2] Table for Rush Scheduler X38EYIWasHbPIuB_2BIoD\",\"OqhTp_--6AeqyYqyY-sSe\":\"OqhTp_--6AeqyYqyY-sSe\",\"d2-rush-http-logs--iLeIqxpZdOgIvwp1KT03y\":\"[Data2] Table for Rush HTTPListener iLeIqxpZdOgIvwp1KT03y\",\"d2-rush-http-logs--h0xNpWu_qksX7ULLXGJ5U\":\"[Data2] Table for Rush HTTPListener h0xNpWu_qksX7ULLXGJ5U\",\"aenRe-3InW5rnaLcqa6VG\":\"aenRe-3InW5rnaLcqa6VG\",\"d2-rush-http-logs--7-iMNFazSWBgePYHOHMe8\":\"[Data2] Table for Rush HTTPListener 7-iMNFazSWBgePYHOHMe8\",\"d2-rush-http-logs--EgPM4kddN0U-urWFTY3ux\":\"[Data2] Table for Rush HTTPListener EgPM4kddN0U-urWFTY3ux\",\"Jjt3fjc77UA_xoLob6BvN\":\"Jjt3fjc77UA_xoLob6BvN\",\"d2-rush-http-logs--kE00jmOKFLyK21NYsyMDt\":\"[Data2] Table for Rush HTTPListener kE00jmOKFLyK21NYsyMDt\",\"JVixTj_DaxK49U_iUKX6h\":\"JVixTj_DaxK49U_iUKX6h\",\"d2-rush-http-logs--kNC2M25EuyYpLJZJ5kYs7\":\"d2-rush-http-logs--kNC2M25EuyYpLJZJ5kYs7\",\"blFP_7Q7TR05zTcfYa5Xn\":\"blFP_7Q7TR05zTcfYa5Xn\",\"S_nydCRoy0Pi7Eq6fsQ1c\":\"S_nydCRoy0Pi7Eq6fsQ1c\",\"HqDZ6vn8eD4Clqrp4jbLN\":\"HqDZ6vn8eD4Clqrp4jbLN\",\"wXNT3DsHyGKu1gDpVv2Qe\":\"wXNT3DsHyGKu1gDpVv2Qe\",\"VnaKoTAw17mGUg4NUaFZ7\":\"VnaKoTAw17mGUg4NUaFZ7\",\"fELj4e-PGguRt7uv0NNmQ\":\"fELj4e-PGguRt7uv0NNmQ\",\"assets\":\"assets\",\"-108q7RFWJya1KlO2lG_p\":\"New HTTPListener (106)\",\"Iuei0_9jOCSaC8p5wqE5I\":\"API_AVI_KPIS_INDICADORES_V2\",\"WmEWgHBof4tZ20sVpm9wK\":\"New HTTPListener (106)\",\"y4A1QBJj6PZjGjz0P5SzW\":\"Scheduler Agent External Catch in chat survey\",\"-qI3WX_wNZ89B9SFb-IR6\":\"Scheduler Fixer Agent Null\",\"kwd1xJwIeeVUKs83AkLZx\":\"EC CognitoIdentityProviderGetPayload\",\"LgIQ9eyXolMb_TATBdRF7\":\"cognitoSetupAccessTokens\",\"RlpaJ8M3tCp2Q4yueyDFR\":\"MiNegocioInitializeSession\",\"uii8bQSNQbfTHXyZHmmbB\":\"MiNegocioGetIdentification\",\"KEETg5uE6acsP2uYArc7C\":\"SaveReclaimRUSH\",\"9dhucdyA3IRnRa9naVQ0q\":\"Marcos Teste\",\"TSwY-BGDyOGOLaXa5t6fW\":\"KACE Identity Provider\",\"KEZqqKYwFxE2lzg1t_6lo\":\"Rework Errors in No Errors\",\"5hKTExBiwtBc3EodqGBBY\":\"Sending CSAT data to Qualtrics\",\"X3G1YtIaNDGJ1EJi_VRgv\":\"cost-report-kpi-data\",\"Data2BuilderAuthentication\":\"Autenticação do Builder (data2)\",\"645b823726c1280637133f0e\":\"Natura Hispana AVI\",\"Eu-4BuvCT5Pv589p75jeM\":\"Rush_APP\",\"RCiLYfZ9yJeGeCnxcWrI4LogReadDataSet\":\"Logs · ChatHook\",\"DKnDLQUtDA6mNdYkT8sh1LogReadDataSet\":\"Logs · audio-generator\",\"di5-RlyrJ7LB5lv9z26xaLogReadDataSet\":\"Logs · Sync Agents Metadata Job HML\",\"5MryyxAbTHsb89yVx3FAeLogReadDataSet\":\"Logs · agent-send-message-entrypoint\",\"TZtOuDXGQh24VDN9CGov0LogReadDataSet\":\"Logs · PingPong KeyVault\",\"13rnkmw96E5MrquKnnI_MLogReadDataSet\":\"Logs · agent-logs-entrypoint\",\"3NwM07fRAAi2cON2KpJi_LogReadDataSet\":\"Logs · Expire Inactive Transfers Job HML\",\"ZOpJU13MlsqJNZNScpZrsLogReadDataSet\":\"Logs · Sync Agents Metadata Job PRD\",\"0ScwQIaOMBUIdDb1x5q9RLogReadDataSet\":\"Logs · transfer-operation-entrypoint\",\"JtLDId2qjiu0sNsULsD4ELogReadDataSet\":\"Logs · Expire Inactive Transfers Job PRD\",\"PqjPbzpg-bFvxXV9qSA8TLogReadDataSet\":\"Logs · chat-ping-entrypoint\",\"PqjPbzpg-bFvxXV9qSA8TLogInsertDataSet\":\"Logs · chat-ping-entrypoint\",\"vC7Xapg8Lx7g4wfd0e_jRLogReadDataSet\":\"Logs · Detect Abandon Job HML\",\"GFNOx0PYwKYjS8oFd7U6lLogReadDataSet\":\"Logs · Detect Abandon Job PRD\",\"VXbg6FJbalOwrIJnvZIRCLogReadDataSet\":\"Logs · Copy of Peru-Authentication\",\"kr62-VRyfj1Rd58JKRaCPLogReadDataSet\":\"Logs · Copy of semantic-faq\",\"Rdzbz0sYIWKJo-DOGAS4LLogReadDataSet\":\"Logs · colombia-reply-tests\",\"VBe5cffOny7xgAxAsNd_FLogReadDataSet\":\"Logs · semantic-faq\",\"_lFzbsy_N19dug6wI83S6LogReadDataSet\":\"Logs · DEV CAll Manual Only Iterate-Queue-recursive-JOB-DEV\",\"-_As0_UIvL1WZLhYkvnEPLogReadDataSet\":\"Logs · Iterate-Queue-Auto-Healer Scheduler <- on pause due external_transfer\",\"1knLGAxuaCD2fsuMbZBH5LogReadDataSet\":\"Logs · Iterate-Queue-recursive-JOB-PRD <- on pause due external_transfer\",\"n7l-E7afYNCPYtaJzn3o_LogReadDataSet\":\"Logs · ocular-kb-entrypoint\",\"Br8NJWvy7UDUC3dKFs-DeLogReadDataSet\":\"Logs · ocular-answer-entrypoint\",\"4D2touS1KIk735CijlzJuLogReadDataSet\":\"Logs · FormsSendEmail HML\",\"4D2touS1KIk735CijlzJuLogInsertDataSet\":\"Logs · FormsSendEmail HML\",\"oN7ZFszCha8Cx6I3WefPaLogReadDataSet\":\"Logs · MAIN RUSH Forms\",\"oN7ZFszCha8Cx6I3WefPaLogInsertDataSet\":\"Logs · MAIN RUSH Forms\",\"f9mn0XCtwjCiwA2buisUMLogReadDataSet\":\"Logs · Get Seller - Rush Invoke\",\"f9mn0XCtwjCiwA2buisUMLogInsertDataSet\":\"Logs · Get Seller - Rush Invoke\",\"OXlr-pFHOSL3TyqbG_C29LogReadDataSet\":\"Logs · SubMonitoringRush\",\"SSkgjL2C2_pha_4D8YoYVLogReadDataSet\":\"Logs · Recips_infobit\",\"iZpq-fSmeaLLWLeQGh2zvLogReadDataSet\":\"Logs · Reciept-Email\",\"4G3HkuB0vUfSPAjUvThB9LogReadDataSet\":\"Logs · Webhook_InfobitData2\",\"S5Smf36KVnpAgdlXt9xU4LogReadDataSet\":\"Logs · d2-chile-entrypoint-hml\",\"S5Smf36KVnpAgdlXt9xU4LogInsertDataSet\":\"Logs · d2-chile-entrypoint-hml\",\"vRtbsuzMqlbWrwKCvjeS6LogReadDataSet\":\"Logs · d2-mexico-entrypoint-hml\",\"vRtbsuzMqlbWrwKCvjeS6LogInsertDataSet\":\"Logs · d2-mexico-entrypoint-hml\",\"xZ07GjXF6J-UE889zvhNvLogReadDataSet\":\"Logs · d2-colombia-entrypoint-hml\",\"xZ07GjXF6J-UE889zvhNvLogInsertDataSet\":\"Logs · d2-colombia-entrypoint-hml\",\"gJnyPaWs1JQSQ8CPGfzCOLogReadDataSet\":\"Logs · d2-peru-entrypoint-hml\",\"gJnyPaWs1JQSQ8CPGfzCOLogInsertDataSet\":\"Logs · d2-peru-entrypoint-hml\",\"hrfpkEcajH1t8HR61CXM2LogReadDataSet\":\"Logs · d2-no-country-entrypoint-hml\",\"hrfpkEcajH1t8HR61CXM2LogInsertDataSet\":\"Logs · d2-no-country-entrypoint-hml\",\"pzwhsvhUFlNTNfalRoReJLogReadDataSet\":\"Logs · [IVR] - FAQ Service - Colombia\",\"LmElMY813TKSrFwDI9BFdLogReadDataSet\":\"Logs · UPLOAD WIKIS Rush\",\"LmElMY813TKSrFwDI9BFdLogInsertDataSet\":\"Logs · UPLOAD WIKIS Rush\",\"X0Zduvi81uXolo0eTY6TkLogReadDataSet\":\"Logs · cari-rush\",\"X0Zduvi81uXolo0eTY6TkLogInsertDataSet\":\"Logs · cari-rush\",\"flQBZ9yWONn7dYrW-6SaOLogReadDataSet\":\"Logs · cari-data2Sec\",\"flQBZ9yWONn7dYrW-6SaOLogInsertDataSet\":\"Logs · cari-data2Sec\",\"2dQ8tHgWFqO0ULXJVmfvBLogReadDataSet\":\"Logs · BUG Auth Strategy\",\"Y1O0oTlVo3601Jif1BxNBLogReadDataSet\":\"Logs · d2-Argentina-entrypoint-hml\",\"Y1O0oTlVo3601Jif1BxNBLogInsertDataSet\":\"Logs · d2-Argentina-entrypoint-hml\",\"5paWQC7PQmA6TT7O8jfN1LogReadDataSet\":\"Logs · Get Csat Agent\",\"p9IwlndVIpNoYWO3V0RaXLogReadDataSet\":\"Logs · Auth Endpoint\",\"fNnAMBBznAO9wvOX5V9JfLogReadDataSet\":\"Logs · d2-peru-entrypoint-prd\",\"fNnAMBBznAO9wvOX5V9JfLogInsertDataSet\":\"Logs · d2-peru-entrypoint-prd\",\"W_7c3KvURCeog4RW9kzlhLogReadDataSet\":\"Logs · d2-no-country-entrypoint-prd\",\"W_7c3KvURCeog4RW9kzlhLogInsertDataSet\":\"Logs · d2-no-country-entrypoint-prd\",\"yrzdo5Ewbnp3ZaiLXV2rzLogReadDataSet\":\"Logs · d2-chile-entrypoint-prd\",\"yrzdo5Ewbnp3ZaiLXV2rzLogInsertDataSet\":\"Logs · d2-chile-entrypoint-prd\",\"v4LXRPYyAgktlbttvZyTNLogReadDataSet\":\"Logs · d2-Argentina-entrypoint-prd\",\"v4LXRPYyAgktlbttvZyTNLogInsertDataSet\":\"Logs · d2-Argentina-entrypoint-prd\",\"oV37yW1tSod7tpFyqKkxyLogReadDataSet\":\"Logs · d2-colombia-entrypoint-prd\",\"oV37yW1tSod7tpFyqKkxyLogInsertDataSet\":\"Logs · d2-colombia-entrypoint-prd\",\"AKlWpcgDjGl6n-TK_j4OYLogReadDataSet\":\"Logs · d2-mexico-entrypoint-prd\",\"AKlWpcgDjGl6n-TK_j4OYLogInsertDataSet\":\"Logs · d2-mexico-entrypoint-prd\",\"NDp1EgGs3Xn4IUTan3yhtLogReadDataSet\":\"Logs · debug ec get titles rush\",\"vwkNOgpr4qzNJdhY4JorTLogReadDataSet\":\"Logs · MotherTester\",\"mKozj09NcLOagalg-VqXaLogReadDataSet\":\"Logs · rush-ana-monthly-report-numbers\",\"TUdsI1xRqd9fQj-2U-SabLogReadDataSet\":\"Logs · EmailListenerRPA\",\"m3l1c0HBBXSL5ZRt4ZWscLogReadDataSet\":\"Logs · Scheduler Close Session\",\"m3l1c0HBBXSL5ZRt4ZWscLogInsertDataSet\":\"Logs · Scheduler Close Session\",\"pEaGKSVNxejOXqsg3EGmrLogReadDataSet\":\"Logs · RPAEmailListener\",\"pEaGKSVNxejOXqsg3EGmrLogInsertDataSet\":\"Logs · RPAEmailListener\",\"HmLW40Dug8t3rvhMmfXWYLogReadDataSet\":\"Logs · keycloak-oauth-validate-pin\",\"fU6X_mrSStaevLVbZqAKXLogReadDataSet\":\"Logs · keycloak-oauth-generate-pin\",\"JVf9xTeGFXyQYrusdL4TSLogReadDataSet\":\"Logs · contesta-preguntas-ecuador\",\"JVf9xTeGFXyQYrusdL4TSLogInsertDataSet\":\"Logs · contesta-preguntas-ecuador\",\"uDBjs8wkDtrxMpysFrTEFLogReadDataSet\":\"Logs · refactoring - select usecase\",\"uDBjs8wkDtrxMpysFrTEFLogInsertDataSet\":\"Logs · refactoring - select usecase\",\"L322Lg0jWzCvY6vhp-XRdLogReadDataSet\":\"Logs · refactoring - select FAQ\",\"DU7pDuhF-5peX6NA2K0XHLogReadDataSet\":\"Logs · refactoring - generate answer - usecase\",\"pvPH-HtYlHprLUXXnUzvrLogReadDataSet\":\"Logs · refactoring - generate answer - FAQ\",\"tl059gO4UkSn6l-rMKTX3LogReadDataSet\":\"Logs · Csat Endpoint FullD2\",\"tl059gO4UkSn6l-rMKTX3LogInsertDataSet\":\"Logs · Csat Endpoint FullD2\",\"ubp46aOre3B349mcfAeZyLogReadDataSet\":\"Logs · FAQs-entrypoint-maia\",\"ubp46aOre3B349mcfAeZyLogInsertDataSet\":\"Logs · FAQs-entrypoint-maia\",\"9EjGJRul_CfpxuxkH8uuHLogReadDataSet\":\"Logs · New HTTPListener (84)\",\"bo3vvYdwZei_1njBiiibyLogReadDataSet\":\"Logs · Do NOT DELETE! - New HTTPListener (84)\",\"sNLVZ6BazGIEpFzeJUMs0LogReadDataSet\":\"Logs · EcuadorByToken\",\"sNLVZ6BazGIEpFzeJUMs0LogInsertDataSet\":\"Logs · EcuadorByToken\",\"QOPIchK_LVwWCzYZFRpsFLogReadDataSet\":\"Logs · NI Sessions\",\"QOPIchK_LVwWCzYZFRpsFLogInsertDataSet\":\"Logs · NI Sessions\",\"20Q518uE-sB06h4JJRm-qLogReadDataSet\":\"Logs · New HTTPListener (88)\",\"rbeu3OSYD9XTKdkEKhS0dLogReadDataSet\":\"Logs · New EmailListener (3)\",\"WQVnm9MIxst3JmmSsNrjqLogReadDataSet\":\"Logs · Debug not found\",\"Q86M0LYUybVvjS0DCOmK1LogReadDataSet\":\"Logs · New Scheduler (9)\",\"AZ9vggxaGj14m9wlr4jJHLogReadDataSet\":\"Logs · Scheduler Agent External Catch\",\"AZ9vggxaGj14m9wlr4jJHLogInsertDataSet\":\"Logs · Scheduler Agent External Catch\",\"rM74WoHiOjwclT9OqQfSNLogReadDataSet\":\"Logs · mx-filtred-chatid-logs\",\"7hjRu6sUdMD0rREQDhF5mLogReadDataSet\":\"Logs · cl-filtred-chatid-logs\",\"Pfgl_XbCvP_MbD16we1_kLogReadDataSet\":\"Logs · co-filtred-chatid-logs\",\"ngcyRzaTIYvr455tIF1kzLogReadDataSet\":\"Logs · pe-filtred-chatid-logs\",\"fr53sxeRFZ4z97aNvd0fsLogReadDataSet\":\"Logs · ar-filtred-chatid-logs\",\"XyWU5I35B-xol8vj6E68ALogReadDataSet\":\"Logs · ec-filtred-chatid-logs\",\"JJMYzP0l4WL2syVjR3Le5LogReadDataSet\":\"Logs · sinch-ar-filtred-logs\",\"jC_thuwiDg9xvEdlEv2W4LogReadDataSet\":\"Logs · cari-rush-filtred-logs\",\"jC_thuwiDg9xvEdlEv2W4LogInsertDataSet\":\"Logs · cari-rush-filtred-logs\",\"JOBH4QXjA1tsc4k_iGjywLogReadDataSet\":\"Logs · getData-infoMaia\",\"3O7jD2oTEfg5C-guYPntfLogReadDataSet\":\"Logs · fix env null\",\"GYCxKrHJQUBAN7ELG_6cpLogReadDataSet\":\"Logs · getTime - Forms-Rush\",\"GYCxKrHJQUBAN7ELG_6cpLogInsertDataSet\":\"Logs · getTime - Forms-Rush\",\"llsRwO29b2x_HqelU2ZwKLogReadDataSet\":\"Logs · Messages Controler in Queue\",\"llsRwO29b2x_HqelU2ZwKLogInsertDataSet\":\"Logs · Messages Controler in Queue\",\"zKLNT-goyFRBK6gnQMwFaLogReadDataSet\":\"Logs · New HTTPListener (98)\",\"M4ZqtoUWz2tvH6eepqVIxLogReadDataSet\":\"Logs · scheduler fix complete session ended data\",\"M4ZqtoUWz2tvH6eepqVIxLogInsertDataSet\":\"Logs · scheduler fix complete session ended data\",\"9GWeJ6GdPB6e2N1ILfQuRLogReadDataSet\":\"Logs · Close Session Manually\",\"9GWeJ6GdPB6e2N1ILfQuRLogInsertDataSet\":\"Logs · Close Session Manually\",\"t318vm-mHbHCN0mCySu5gLogReadDataSet\":\"Logs · Api Executions with NO NAME\",\"t318vm-mHbHCN0mCySu5gLogInsertDataSet\":\"Logs · Api Executions with NO NAME\",\"SInspt5Zks0QPlfoBB3IaLogReadDataSet\":\"Logs · API_CSAT_QUALTRICS_FORMS\",\"SInspt5Zks0QPlfoBB3IaLogInsertDataSet\":\"Logs · API_CSAT_QUALTRICS_FORMS\",\"lGLpiM1QQvJwQKrxn8NcTLogReadDataSet\":\"Logs · Comment Duplicator <31/03\",\"zqWM3dlAnLhOUVTw_vRNSLogReadDataSet\":\"Logs · Scheduler Catch data to report cancel Order\",\"zqWM3dlAnLhOUVTw_vRNSLogInsertDataSet\":\"Logs · Scheduler Catch data to report cancel Order\",\"RAcxhTZr3D7Hmj37bgkCcLogReadDataSet\":\"Logs · Copy of d2-colombia-entrypoint-hml\",\"RAcxhTZr3D7Hmj37bgkCcLogInsertDataSet\":\"Logs · Copy of d2-colombia-entrypoint-hml\",\"QrT1-4d2hrSFAZMKGbtA3LogReadDataSet\":\"Logs · New HTTPListener (102)\",\"5iNOjWfW6z69unNZhpPxWLogReadDataSet\":\"Logs · API_AVI_KPIS\",\"5iNOjWfW6z69unNZhpPxWLogInsertDataSet\":\"Logs · API_AVI_KPIS\",\"pqwZ2g4lvKhd7c1-7FltPLogReadDataSet\":\"Logs · API_AVI_KPIS_INDICADORES\",\"pqwZ2g4lvKhd7c1-7FltPLogInsertDataSet\":\"Logs · API_AVI_KPIS_INDICADORES\",\"M8Hu0reR498Ik_baDQGtBLogReadDataSet\":\"Logs · New HTTPListener (105)\",\"fWvjFunmTK_hY0lFQbjR6LogReadDataSet\":\"Logs · Delete Old Entries In - Last Messages On Whatsapp\",\"fWvjFunmTK_hY0lFQbjR6LogInsertDataSet\":\"Logs · Delete Old Entries In - Last Messages On Whatsapp\",\"-qI3WX_wNZ89B9SFb-IR6LogReadDataSet\":\"Logs · Scheduler Fixer Agent Null\",\"kwd1xJwIeeVUKs83AkLZxLogReadDataSet\":\"Logs · EC CognitoIdentityProviderGetPayload\",\"kwd1xJwIeeVUKs83AkLZxLogInsertDataSet\":\"Logs · EC CognitoIdentityProviderGetPayload\",\"LgIQ9eyXolMb_TATBdRF7LogReadDataSet\":\"Logs · cognitoSetupAccessTokens\",\"LgIQ9eyXolMb_TATBdRF7LogInsertDataSet\":\"Logs · cognitoSetupAccessTokens\",\"RlpaJ8M3tCp2Q4yueyDFRLogReadDataSet\":\"Logs · MiNegocioInitializeSession\",\"RlpaJ8M3tCp2Q4yueyDFRLogInsertDataSet\":\"Logs · MiNegocioInitializeSession\",\"uii8bQSNQbfTHXyZHmmbBLogReadDataSet\":\"Logs · MiNegocioGetIdentification\",\"uii8bQSNQbfTHXyZHmmbBLogInsertDataSet\":\"Logs · MiNegocioGetIdentification\",\"KEETg5uE6acsP2uYArc7CLogReadDataSet\":\"Logs · SaveReclaimRUSH\",\"KEETg5uE6acsP2uYArc7CLogInsertDataSet\":\"Logs · SaveReclaimRUSH\",\"9dhucdyA3IRnRa9naVQ0qLogReadDataSet\":\"Logs · Marcos Teste\",\"9dhucdyA3IRnRa9naVQ0qLogInsertDataSet\":\"Logs · Marcos Teste\",\"TSwY-BGDyOGOLaXa5t6fWLogReadDataSet\":\"Logs · KACE Identity Provider\",\"TSwY-BGDyOGOLaXa5t6fWLogInsertDataSet\":\"Logs · KACE Identity Provider\",\"KEZqqKYwFxE2lzg1t_6loLogReadDataSet\":\"Logs · Rework Errors in No Errors\",\"KEZqqKYwFxE2lzg1t_6loLogInsertDataSet\":\"Logs · Rework Errors in No Errors\",\"5hKTExBiwtBc3EodqGBBYLogReadDataSet\":\"Logs · Sending CSAT data to Qualtrics\",\"5hKTExBiwtBc3EodqGBBYLogInsertDataSet\":\"Logs · Sending CSAT data to Qualtrics\",\"X3G1YtIaNDGJ1EJi_VRgvLogReadDataSet\":\"Logs · cost-report-kpi-data\",\"X3G1YtIaNDGJ1EJi_VRgvLogInsertDataSet\":\"Logs · cost-report-kpi-data\",\"Eu-4BuvCT5Pv589p75jeMLogReadDataSet\":\"Logs · Rush_APP\",\"roE_vjIAYEoYdX9ymhaUJ\":\"AgentSocketClient\",\"lTQRb152dXh5ddifgEM_J\":\"UserSocketClient\"};\n\nconst PRICES = {\"BUILDER|sessions\":{\"p\":null,\"u\":\"sessões\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"não faturado por dia — a fatura cobra BUILDER/logged_users (US$ 10/usuário/mês)\",\"dim\":\"authentication_strategy.reference\"},\"BUILDER|login\":{\"p\":null,\"u\":\"logins\",\"k\":\"flow\",\"agg1\":\"avg\",\"n\":\"nível 1 é MÉDIA por estratégia — só o detalhe soma\",\"dim\":\"authentication_strategy.reference\"},\"ID|sessions\":{\"p\":null,\"u\":\"sessões\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"não faturado por dia — a fatura cobra ID/logged_users (US$ 0,10/usuário/mês)\",\"dim\":\"system.id\"},\"ID|login\":{\"p\":null,\"u\":\"logins\",\"k\":\"flow\",\"agg1\":\"avg\",\"n\":\"nível 1 é MÉDIA por estratégia — só o detalhe soma\",\"dim\":\"authentication_strategy.reference\"},\"TABLE|read\":{\"p\":1e-05,\"u\":\"leituras\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"dataset.reference\"},\"TABLE|write\":{\"p\":0.0005,\"u\":\"escritas\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"dataset.reference\"},\"TABLE|semantic_search_read_tokens\":{\"p\":6.5e-07,\"u\":\"tokens\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"dataset.reference\"},\"TABLE|semantic_search_write_tokens\":{\"p\":6.5e-07,\"u\":\"tokens\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"dataset.reference\"},\"TABLE|storage\":{\"p\":25,\"u\":\"B\",\"d\":1073741824,\"k\":\"stock\",\"agg1\":\"sum\",\"mo\":true,\"nodrill\":true,\"n\":\"Desde 02/10/26 o nível 1 traz o TOTAL de storage das tabelas (antes era a média por DataSource). Preço da fatura de ago/26: US$ 25/GiB-mês, rateado pelos dias do período. O detalhe (nível 3) só traz a média por system — a API não quebra este par por tabela.\",\"dim\":\"system.id\"},\"DRIVE|storage\":{\"p\":1,\"u\":\"B\",\"d\":1073741824,\"k\":\"stock\",\"agg1\":\"sum\",\"mo\":true,\"n\":\"Desde 02/10/26 o nível 1 traz o TOTAL (a soma dos drives; antes era a média por drive). O detalhe por drive soma o mesmo valor.\",\"dim\":\"drive.reference\"},\"DRIVE|read_size\":{\"p\":1,\"u\":\"B\",\"d\":1073741824,\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"detalhe por arquivo: >500 linhas, precisa paginar; arquivos não têm nome\",\"dim\":\"file.reference\"},\"DRIVE|screenshot\":{\"p\":null,\"u\":\"prints\",\"k\":\"flow\",\"agg1\":\"sum\"},\"DRIVE|screenshot_size\":{\"p\":null,\"u\":\"B\",\"d\":1073741824,\"k\":\"flow\",\"agg1\":\"sum\"},\"DRIVE|screenshot_time\":{\"p\":null,\"u\":\"ms\",\"k\":\"flow\",\"agg1\":\"sum\"},\"DOMAIN|ai_tokens\":{\"p\":0.000008,\"u\":\"tokens\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"assistant.reference\",\"neg\":true,\"n\":\"Preço acordado (private deal): US$ 0,000008 por token. As faturas de mai a ago/26 registram US$ 0,0001, que é o preço de tabela — exatamente 12,5x maior. Por isso o total desta tela é o custo efetivo e NÃO reconcilia com o valor faturado.\"},\"DOMAIN|ai_speech_to_text_seconds\":{\"p\":0.0003,\"u\":\"seg\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"assistant.reference\"},\"DOMAIN|ai_speech_to_text_minutes\":{\"p\":null,\"u\":\"min\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"não faturado — a fatura cobra os SEGUNDOS; contar os dois seria dupla contagem\",\"dim\":\"assistant.reference\"},\"DOMAIN|ai_text_to_speech_characters\":{\"p\":4.8e-05,\"u\":\"chars\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"assistant.reference\"},\"DOMAIN|ai_ocr_pages\":{\"p\":null,\"u\":\"páginas\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"sem preço na fatura de ago/26\"},\"DOMAIN|ai_images\":{\"p\":null,\"u\":\"imagens\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"sem preço na fatura de ago/26\"},\"RUSH|http_listener\":{\"p\":0.0001,\"u\":\"execuções\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"listener.reference\"},\"RUSH|http_listener_duration\":{\"p\":6.68e-08,\"u\":\"ms\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"listener.reference\"},\"RUSH|scheduler\":{\"p\":0.0001,\"u\":\"execuções\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"scheduler.reference\"},\"RUSH|scheduler_duration\":{\"p\":6.68e-08,\"u\":\"ms\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"scheduler.reference\"},\"RUSH|email_listener\":{\"p\":0.00013,\"u\":\"execuções\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"listener.reference\"},\"RUSH|email_listener_chunks\":{\"p\":0.000468,\"u\":\"chunks\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"listener.reference\"},\"RUSH|email_listener_duration\":{\"p\":8.684e-08,\"u\":\"ms\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"listener.reference\"},\"CONNECT|request\":{\"p\":0.0001,\"u\":\"requisições\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"request.path\"},\"CONNECT|request_time\":{\"p\":6.68e-08,\"u\":\"ms\",\"k\":\"flow\",\"agg1\":\"sum\",\"dim\":\"request.path\"},\"CONNECT|socket_connection\":{\"p\":0,\"u\":\"conexões\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"preço 0 na fatura\",\"dim\":\"power_cord_socket.reference\"},\"CONNECT|socket_duration\":{\"p\":0,\"u\":\"ms\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"preço 0 na fatura\",\"dim\":\"power_cord_socket.reference\"},\"CONNECT|invite_send\":{\"p\":null,\"u\":\"convites\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"sem preço na fatura de ago/26\"},\"CONNECT|mail_send\":{\"p\":null,\"u\":\"e-mails\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"sem preço na fatura de ago/26\"},\"CONNECT|aws_s3_put\":{\"p\":null,\"u\":\"puts\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"sem preço na fatura de ago/26\"},\"CONNECT|aws_athena_query\":{\"p\":null,\"u\":\"queries\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"sem preço na fatura de ago/26\"},\"CONNECT|whatsapp_messenger_instance\":{\"p\":null,\"u\":\"instâncias\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"sem preço na fatura de ago/26\"},\"TABLE|key_used\":{\"p\":null,\"u\":\"usos\",\"k\":\"flow\",\"agg1\":\"sum\"},\"ID|key_used\":{\"p\":5e-05,\"u\":\"usos\",\"k\":\"flow\",\"agg1\":\"sum\",\"n\":\"preço da fatura de ago/26\"}};\n\n/*C0*/\nconst PROD_COLOR = {\n  DOMAIN: '#e619c8', TABLE: '#e11d48', ID: '#12c9b8', RUSH: '#8b5cf6',\n  CONNECT: '#14b3e8', DRIVE: '#eab308', BUILDER: '#64748b',\n  PESSOAS: '#22c55e'   // custo de alocação; sem esta cor o empilhado emitiria fill=\"undefined\"\n};\n// PESSOAS por último = faixa no TOPO da pilha. Sem a entrada aqui, chartStacked descarta o valor\n// EM SILÊNCIO (ele monta as faixas com PROD_ORDER.filter), e o gráfico deixaria de bater com o KPI.\nconst PROD_ORDER = ['DOMAIN', 'TABLE', 'RUSH', 'CONNECT', 'DRIVE', 'ID', 'BUILDER', 'PESSOAS'];\nconst BASE_KEYS = new Set(['date', 'product', 'measure_name', 'measure_value_type', 'sum_measure_value']);\n\nconst MAX_DAYS = 7;      // dias avulsos na visão diária\nconst MIN_DAYS = 2;      // mesmo piso do semanal: com UM período o único período é a base, e elasticidade,\n                         // impacto e tendência ficam indefinidos — o relatório sai sem comparação nenhuma\n// A visão semanal aceita de 2 a 4 janelas. O piso é 2 porque com UMA semana o período-base seria o\n// único período: não sobra nada contra o que comparar, e elasticidade/impacto ficariam indefinidos.\n// O teto é 4: com 4 janelas a leitura vira de PROGRESSÃO (a reta sobre os 4 pontos), que é o que\n// separa \"subiu e voltou\" de \"alta consistente\". A quinta só somaria carga sem mudar essa leitura.\nconst WEEKS_MIN = 2;\nconst WEEKS_MAX = 4;\nconst WEEK_LEN = 7;\nconst PAGE = 500;\nconst MAX_PAGES = 3;     // teto de páginas por (período, evento) — truncagem é SINALIZADA\nconst MAX_PAGES_W = 8;   // medido: uma semana de TABLE/read são 1.748 linhas = 4 páginas; com teto 2 o\n                         // drill-down via 2 dias de 7 (34,6% do volume) e subestimava o por-sessão em ~2,9×.\n                         // O laço sai sozinho na exaustão, então teto alto só custa chamada onde há dado:\n                         // dos pares pré-carregados (ago/26), só TABLE/read passava de 2; DRIVE/read_size passa de 8.\nconst POOL = 6;\n// Só o par de MAIOR CUSTO vem pronto; o resto é sob demanda pelo botão de cada linha. A fase 2 é um\n// produto cartesiano `pares × períodos` a ~17 s por chamada, então cada par a mais custa N chamadas:\n// com 8/4 dava 33 chamadas em 3 dias e 18 em 3 semanas, e passaria de 90 em 7 dias, sem teto.\n// ⚠ Isto NÃO tira custo nenhum da tela: o custo de alto nível dos 25 pares vem da fase 1 (nível 1).\n// A fase 2 só alimenta o destrinchamento por recurso. Desde 02/10/26 o `storage` (DRIVE e TABLE)\n// vem TOTAL no nível 1 e não precisa mais de pré-carga; pickEager só força pares de MÉDIA com preço.\nconst TOP_N = 1;\nconst TOP_N_W = 1;\n// Mensal: meses de CALENDÁRIO, que é o recorte da fatura. Medido hoje: 1 mês = 1 chamada de sessões\n// (3 s) + 1 de nível 1 (40 s), então 4 meses custam 8 chamadas contra 40 a 52 de 4 semanas.\n// TOP_N_M = 0 porque o mensal não pré-carrega detalhe nenhum: TABLE/read num mês são ~16 páginas.\nconst MONTHS_MIN = 2;\nconst MONTHS_MAX = 4;\nconst MAX_PAGES_M = 4;   // o detalhe por recurso é bloqueado no mensal; o teto só vale para pares de média\nconst TOP_N_M = 0;\n\n// ---------- alocações: custo de PESSOAS, digitado — não vem de API ----------\n// Um desenvolvedor contratado custa R$ 26.000/mês. A conta do portal é em US$ (a moeda da fatura), então a\n// alocação passa pelo CÂMBIO digitado no topo (G16) — e ele vai impresso no PDF: sem a taxa à vista, ninguém\n// refaz a conta. O câmbio começa VAZIO a cada abertura (decisão do dono): sem ele, as alocações ficam FORA dos\n// totais, com aviso — nunca convertidas por uma taxa que ninguém digitou.\n// ⚠️ Entra no TOTAL do mês, nunca no custo por sessão — ver o comentário em periodTotals.\nconst ALLOC_BRL = 26000;\nconst ALLOC_CUT = '2026-09-01';   // até aqui o histórico é 1 alocação; deste mês em diante, 2\nconst ALLOC_MAX = 99;\nconst allocDefault = (d0) => (d0 >= ALLOC_CUT ? 2 : 1);\nconst allocOf = (st, per) => ((st.mAlloc && st.mAlloc[per.k] !== undefined)\n  ? st.mAlloc[per.k] : allocDefault(per.d0));\nconst PARETO = 0.8;      // fração do custo que define os \"TOP CUSTO\"\nconst SESS_EPS = 0.02;   // abaixo disso as sessões não se moveram o bastante p/ dividir\n\n// ---------- utilitários ----------\nconst num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : 0; };\n// leitura ESTRITA de sum_measure_value (texto, notação científica ou número): '', null e \"1,5\" dão NaN — quem\n// chama transforma isso em ERRO, nunca em 0 silencioso como o num() acima\nconst numEstrito = (v) => (typeof v === 'number' ? v : (typeof v === 'string' && v.trim() !== '' ? Number(v) : NaN));\n// o valor ruim citado nas mensagens de erro: campo ausente, nulo, vazio ou número infinito (1e999) em português —\n// \"undefined\", \"null\" crus (e o \"null\" que o JSON.stringify dá para Infinity) seriam lixo na faixa, que imprime\nconst mostraValor = (v) => {\n  if (v === undefined) return 'ausente';\n  if (v === null) return 'nulo';\n  if (typeof v === 'string' && v.trim() === '') return 'vazio';\n  if (typeof v === 'number' && !Number.isFinite(v)) return 'não finito';\n  const j = String(JSON.stringify(v));\n  return j.length > 30 ? j.slice(0, 29) + '…' : j;\n};\n// sessão ausente (chamada que falhou) é SEM DADO: null, nunca 0 — com 0 o classify inventava −100%\nconst nz = (x) => (x === undefined || x === null ? null : x);\nconst normDays = (ds) => [...new Set((ds || []).filter(Boolean))].sort();\nconst key = (p, e) => p + '|' + e;\nconst meta = (p, e) => PRICES[key(p, e)] || { p: null, u: '', k: 'flow', agg1: 'sum' };\nconst isStock = (p, e) => meta(p, e).k === 'stock';\n\nfunction addDays(iso, n) {\n  const d = new Date(iso + 'T12:00:00Z');\n  d.setUTCDate(d.getUTCDate() + n);\n  return d.toISOString().slice(0, 10);\n}\nconst yesterday = () => addDays(new Date(Date.now()).toISOString().slice(0, 10), -1);\nfunction daysInMonth(iso) { const [y, m] = iso.split('-').map(Number); return new Date(y, m, 0).getDate(); }\nfunction dowPt(iso) {\n  return ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 'sáb'][new Date(iso + 'T12:00:00Z').getUTCDay()];\n}\nconst shortDay = (iso) => iso.slice(8, 10) + '/' + iso.slice(5, 7);\n\n// custo estimado em US$ a partir do volume bruto (unidade da API). A moeda de EXIBIÇÃO (G16) só entra nos\n// formatadores: nunca multiplique pela taxa aqui (daria ×taxa² na tela)\nfunction costOf(p, e, raw, refIso, dias) {\n  const m = meta(p, e);\n  if (m.p === null || m.p === undefined || raw === null) return null;\n  const q = raw / (m.d || 1);\n  // ⚠️ Preço mensal (mo:true: DRIVE/storage e TABLE/storage) é cobrado POR MÊS. Dividir por daysInMonth dá a\n  // cota de UM dia — certo quando o período É um dia, mas num período de 7 dias subestimava 7× e num\n  // mês ~31×, justo na visão que deve bater com a fatura. O nº de dias vem do próprio período.\n  if (!m.mo) return q * m.p;\n  // `dias` pode ser a LISTA de dias do período: aí o rateio é por dia, e uma janela que atravessa o\n  // mês usa o tamanho de cada mês (a fatura rateia o GiB-mês por dia)\n  const f = Array.isArray(dias) ? dias.reduce((s, d) => s + 1 / daysInMonth(d), 0) : (dias || 1) / daysInMonth(refIso);\n  return q * m.p * f;\n}\nfunction labelOf(ref, dim) {\n  if (ref === null || ref === undefined || ref === '') return '(sem dimensão)';\n  const s = String(ref);\n  if (dim === 'request.path') return s;\n  if (LABELS[s]) return LABELS[s];\n  // tabelas e DataSets de LOG dos rushes: o nome vem do listener/scheduler dono do log\n  const m = s.match(/^(.+?)Log(?:Insert|Read)DataSet$/) || s.match(/^d2-rush-(?:http|scheduler|email)-logs--(.+)$/);\n  if (m) return '[log] ' + (LABELS[m[1]] || m[1]);\n  return null;\n}\n\n// ---------- formatação ----------\n// pt-BR em TUDO (vírgula decimal, ponto de milhar). Antes toFixed punha PONTO decimal e o\n// toLocaleString punha PONTO de milhar: \"US$ 1.235\" e \"US$ 1.23\" conviviam na mesma coluna.\nconst NFC = {};\n// \"-0\", \"-0,0\", \"-0,00\": negativo que arredonda a zero perde o sinal (o zero não tem direção)\nconst semMenosZero = (s) => (/^-0(,0+)?$/.test(s) ? s.slice(1) : s);\nconst fx = (n, d) => semMenosZero((NFC[d] || (NFC[d] = new Intl.NumberFormat('pt-BR',\n  { minimumFractionDigits: d, maximumFractionDigits: d }))).format(n));\nconst fsig = (n, s) => semMenosZero(new Intl.NumberFormat('pt-BR', { maximumSignificantDigits: s }).format(n));\n// 2 dígitos significativos SEM perder o zero final (\"0,50\", não \"0,5\"): precisão igual na mesma linha\nconst fsig2 = (n) => semMenosZero(new Intl.NumberFormat('pt-BR',\n  { minimumSignificantDigits: 2, maximumSignificantDigits: 2 }).format(n));\nfunction fmtNum(n) {\n  if (n === null || n === undefined || !Number.isFinite(n)) return '—';\n  const a = Math.abs(n);\n  // limiares já ARREDONDADOS: 999.950 não vira \"1.000,0 mil\", vira \"1,00 mi\"\n  if (a >= 999995000) return fx(n / 1e9, 2) + ' bi';\n  if (a >= 999950) return fx(n / 1e6, 2) + ' mi';\n  if (a >= 999.5) return fx(n / 1e3, 1) + ' mil';\n  if (a >= 0.995) return fx(n, 0);\n  if (a === 0) return '0';\n  return fx(n, 2);\n}\n// Bytes em unidade BINÁRIA: a fatura cobra GiB (1024³). E 'B' de bilhão colidiria com 'B' de byte.\nfunction fmtBytes(n) {\n  if (n === null || n === undefined || !Number.isFinite(n)) return '—';\n  const a = Math.abs(n);\n  // limiar arredondado: 1.048.575 B não vira \"1.024,00 KiB\", vira \"1,00 MiB\"\n  const u = [[1073741824, 'GiB', 1073736581.12], [1048576, 'MiB', 1048570.88], [1024, 'KiB', 1023.5]];\n  for (const [d, s, lim] of u) if (a >= lim) return fx(n / d, 2) + ' ' + s;\n  // abaixo de 1 KiB: dígito significativo (1,13 B e 1,60 B viravam \"1 B\" e \"2 B\")\n  if (a === 0) return '0 B';\n  return (a >= 100 ? fx(n, 0) : fsig(n, 3)) + ' B';\n}\nconst fmtVal = (n, unit) => (unit === 'B' ? fmtBytes(n) : fmtNum(n));\n// O consumo por sessão virou o número grande da tabela, e quase sempre é um valor pequeno — fmtNum foi\n// feito para volume e destrói a leitura nessa faixa: 1.13 e 1.60 viram '1' e '2' (parece +100%, são\n// +42%) e 0.0062 vira '0.01', empatando com 0.0137. Aí a pontilhada mostra o movimento certo enquanto\n// o número diz outra coisa. Aqui o que importa é dígito significativo, não escala.\nfunction fmtPerS(n, unit) {\n  if (n === null || n === undefined || !Number.isFinite(n)) return '—';\n  if (unit === 'B') return fmtBytes(n);\n  const a = Math.abs(n);\n  if (a === 0) return '0';\n  // limiares arredondados (9,996 vira \"10,0\", não \"10,00\") e 2 dígitos significativos fixos abaixo de 1\n  if (a >= 999.5) return fmtNum(n);\n  if (a >= 99.95) return fx(n, 0);\n  if (a >= 9.995) return fx(n, 1);\n  if (a >= 0.995) return fx(n, 2);\n  return fsig2(n);\n}\n// ---------- moeda de exibição: US$ | R$ (G16) ----------\n// TODA conta continua em US$, a moeda da fatura (costOf, buildRows, periodTotals, costByProduct, sortRows, drill,\n// allocUsd). A moeda muda SÓ aqui, numa multiplicação dentro dos formatadores. Converter também na fonte daria\n// ×taxa² (×29,16 a 5,40): com allocUsd = ALLOC_BRL, 1 dev sairia \"R$ 140.400\".\n// O câmbio é digitado no topo a cada execução (começa vazio); sem ele, só US$.\nconst FX_MIN = 1;    // trava de digitação: \"540\" ou \"0,54\" no lugar de \"5,40\" não passam\nconst FX_MAX = 20;\nconst fxOk = (v) => Number.isFinite(v) && v >= FX_MIN && v <= FX_MAX;\n// número digitado em pt-BR ou em inglês: \"5,43\", \"5.43\", \"1.234,5\" (havendo vírgula, o ponto é milhar)\nconst numBr = (raw) => {\n  const s = String(raw === null || raw === undefined ? '' : raw).replace(/\\s/g, '');\n  // só dígitos com UM separador decimal, ou milhar pt-BR (\"1.234,5\"): Number() aceitaria \"0x14\", \"1e1\", \"Infinity\"\n  if (s === '' || !/^[+-]?(\\d+([.,]\\d*)?|[.,]\\d+|\\d{1,3}(\\.\\d{3})+(,\\d*)?)$/.test(s)) return NaN;\n  return Number(s.indexOf(',') >= 0 ? s.replace(/\\./g, '').replace(',', '.') : s);\n};\nconst MOEDAS = {\n  usd: { id: 'usd', sim: 'US$', alt: false },\n  brl: { id: 'brl', sim: 'R$', alt: true }   // alt: o modo que não é o padrão ganha marcas (h1 do PDF, rodapé, h2)\n};\nconst moedaOf = (st) => Object.assign({}, MOEDAS[st.moeda === 'brl' ? 'brl' : 'usd'],\n  { k: st.moeda === 'brl' ? st.fx : 1, taxa: st.fx });\n// trocado na 1ª linha de render(): render é síncrono, e mounted só formata contagens\nlet MOE = moedaOf({ moeda: 'usd', fx: null });\nconst conv = (usd) => usd * MOE.k;\n// a taxa sai com a precisão USADA na conta (2 a 4 casas, como a PTAX)\nconst fmtTaxa = (t) => semMenosZero(new Intl.NumberFormat('pt-BR',\n  { minimumFractionDigits: 2, maximumFractionDigits: 4 }).format(t));\n// o piso no campo: 3 dígitos significativos abaixo de 1; senão até 2 casas e SEM ponto de milhar (\"1000\", não \"1.000\", que numBr leria como 1)\nconst fmtPiso = (v) => (v > 0 && v < 1) ? fsig(v, 3) : semMenosZero(new Intl.NumberFormat('pt-BR',\n  { maximumFractionDigits: 2, useGrouping: false }).format(v));\n// v JÁ na moeda da tela: os limiares valem sobre o valor EXIBIDO (a 5,40, US$ 185 = R$ 1.000 perde as casas)\nfunction fmtDin(v, semSim) {\n  if (v === null || v === undefined || !Number.isFinite(v)) return '—';\n  const a = Math.abs(v), p = semSim ? '' : MOE.sim + ' ';\n  if (a === 0) return p + '0';\n  if (a >= 999.995) return p + fx(v, 0);   // limiar arredondado: 999,996 não vira \"1.000,00\"\n  if (a >= 0.99995) return p + fx(v, 2);\n  if (a >= 0.001) return p + fx(v, 4);\n  // abaixo de 0,001: dígitos significativos — 4 casas fixas imprimiam \"US$ 0,0000\" em vermelho/verde\n  return p + fsig(v, 3);\n}\n// recebe US$ (a unidade de toda conta) e imprime na moeda da tela\nfunction fmtUsd(n) { return (n === null || n === undefined || !Number.isFinite(n)) ? '—' : fmtDin(conv(n)); }\nfunction fmtPct(x) {\n  if (x === null || x === undefined || !Number.isFinite(x)) return '—';\n  const s = fx(x * 100, 1);\n  // zero arredondado não tem sinal: nada de \"+0,0%\" nem \"-0,0%\"\n  return (s === '0,0' ? '' : (x > 0 ? '+' : '')) + s + '%';\n}\n// US$/sessão é a métrica-âncora: 4 casas fixas zeravam projetos com cps < 0,00005. Abaixo de 0,001\n// passa a dígito significativo; acima, as mesmas 4 casas de sempre.\nfunction fmtCps(n) {\n  if (n === null || n === undefined || !Number.isFinite(n)) return '—';\n  const v = conv(n);\n  return MOE.sim + ' ' + (Math.abs(v) >= 0.001 || v === 0 ? fx(v, 4) : fsig(v, 3));\n}\n// só o eixo direito do HERO usa: converte o valor E o topo, senão as casas seguem a escala em US$\nconst fmtTick = (v, hi) => { const a = conv(v), h = conv(hi); return h >= 0.1 ? fx(a, 2) : fsig(a, 2); };\n// ---------- divisor: sessões TOTAIS × RETIDAS ----------\n// O switch \"SESSÕES totais | retidas\" troca o DIVISOR de todo número por sessão (US$/sessão, consumo por sessão,\n// elasticidade); o custo não muda. Retida = sessão resolvida sem atendimento humano (sessiones_retenidas da fonte\n// de sessões). Os textos que nomeiam o divisor vêm daqui — no modo totais são exatamente os de antes.\n// G15: o UNIVERSO \"só filas c/ IA\" usa as sessões REAIS (sessoes_reais / sessoes_reais_retenidas) — as de PRD das filas\n// que podem falar com a IA. ret = o divisor é uma contagem de retidas; real = universo das reais; alt = divisor que não é\n// o padrão (muda unidade, nota e cabeçalho). rl/ra/leg = rótulo da retenção do universo; ct/cr = contagens da faixa.\nconst ROT = {\n  total:   { ret: false, real: false, alt: false, sess: 'sessões', Sess: 'Sessões', sessao: 'sessão', cont: 'sessões', un: '/sess',\n             unE: '·dia/sess', tag: '/sessão', traf: 'o tráfego', tend: 'volume × tráfego',\n             rl: 'retenção', ra: 'ret.', leg: 'ret. = retenção (retidas ÷ totais)', ct: 'sessões', cr: 'retidas',\n             inc: 'Sessões retidas maiores que as totais' },\n  retidas: { ret: true, real: false, alt: true, sess: 'sessões retidas', Sess: 'Sessões retidas', sessao: 'sessão retida', cont: 'retidas',\n             un: '/sess. ret.', unE: '·dia/sess. ret.', tag: '/sess. ret.', traf: 'as sessões retidas', tend: 'volume × retidas',\n             rl: 'retenção', ra: 'ret.', leg: 'ret. = retenção (retidas ÷ totais)', ct: 'sessões', cr: 'retidas',\n             inc: 'Sessões retidas maiores que as totais', campo: 'sessiones_retenidas', errKey: 'RetErr',\n             semCampo: 'o total sem as retidas', zq: 'a retenção', valido: 'o modo totais' },\n  reais:   { ret: false, real: true, alt: true, sess: 'sessões reais', Sess: 'Sessões reais', sessao: 'sessão real', cont: 'reais',\n             un: '/sess. real', unE: '·dia/sess. real', tag: '/sess. real', traf: 'as sessões reais', tend: 'volume × reais',\n             rl: 'retenção real', ra: 'real', leg: 'real = retenção real (reais retidas ÷ reais)', ct: 'reais', cr: 'reais retidas',\n             inc: 'Sessões reais retidas maiores que as reais', campo: 'sessoes_reais', errKey: 'RealErr',\n             semCampo: 'o total sem as sessões reais', zq: 'o número', valido: 'o universo todas' },\n  reaisRet: { ret: true, real: true, alt: true, sess: 'sessões reais retidas', Sess: 'Sessões reais retidas', sessao: 'sessão real retida',\n             cont: 'reais retidas', un: '/sess. real ret.', unE: '·dia/sess. real ret.', tag: '/sess. real ret.',\n             traf: 'as sessões reais retidas', tend: 'volume × reais retidas',\n             rl: 'retenção real', ra: 'real', leg: 'real = retenção real (reais retidas ÷ reais)', ct: 'reais', cr: 'reais retidas',\n             inc: 'Sessões reais retidas maiores que as reais', campo: 'sessoes_reais_retenidas', errKey: 'RealRetErr',\n             semCampo: 'as reais sem as reais retidas', zq: 'a retenção real', valido: 'o modo totais' }\n};\n// retenção = retidas ÷ totais: um NÍVEL, sem sinal (fmtPct é de variação). Sem um dos dois, ou total zero: sem dado\nconst retOf = (tot, ret) => (tot > 0 && ret !== null && ret !== undefined ? ret / tot : null);\nfunction fmtRet(r) {\n  if (r === null || r === undefined || !Number.isFinite(r)) return '—';\n  const s = fx(r * 100, 1);\n  return (s === '100,0' && r < 1 ? '99,9' : s) + '%';   // abaixo de 100% nunca imprime \"100,0%\"\n}\nfunction esc(s) {\n  return String(s == null ? '' : s).replace(/[&<>\"']/g, (c) =>\n    ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]));\n}\n\n// ---------- elasticidade ----------\n// E = Δ%recurso ÷ Δ%sessões, sempre contra o PRIMEIRO período selecionado.\n// Os guarda-corpos são o que impede a tabela de mentir quando o divisor é minúsculo.\n// ⚠️ A DIREÇÃO NUNCA VEM DE E. E = Δ%recurso ÷ Δ%sessões é uma divisão, e quando as sessões CAEM o\n// divisor é negativo — aí a faixa de E inverte de sentido. Um recurso que caiu 36% com o tráfego\n// caindo 12,5% dá E = 2,88 e era rotulado \"cresceu o dobro\", quando na verdade ficou 27% MAIS\n// eficiente por sessão. Por isso: |E| dá a MAGNITUDE, e o sinal de Δ(consumo/sessão) dá a DIREÇÃO.\nconst BANDS = [\n  { cls: 'critico',      lab: '↑ forte',    txt: 'consumo por sessão SUBIU e o recurso se moveu ≥ 2× o tráfego' },\n  { cls: 'atencao',      lab: '↑ moderada', txt: 'consumo por sessão SUBIU, em ordem próxima à do tráfego' },\n  { cls: 'esperado',     lab: '≈ estável',   txt: 'consumo por sessão praticamente não mudou (±5%)' },\n  { cls: 'subutilizado', lab: '↓ caiu',     txt: 'consumo por sessão CAIU — o recurso ficou mais eficiente' },\n  { cls: 'divergente',   lab: 'DIVERGENTE', txt: 'recurso e sessões foram em direções opostas' }\n];\n\n// Tendência de uma série ao longo dos períodos, ignorando buracos (null = período sem dado).\n//   rel           = variação total IMPLÍCITA PELA RETA ajustada, relativa ao primeiro ponto\n//   mesmos/passos = degraus que seguem a direção dominante (consistência da progressão)\n// ⚠️ Com 2 pontos a reta passa EXATAMENTE por ambos, então usa-se o valor cru: o resultado fica\n// bit-idêntico ao cálculo antigo (base vs último) e a visão de 2 janelas NÃO regride. Verificado\n// offline em 50.000 pares aleatórios: diferença absoluta 0,0 (sem esse caso o erro era ~1e-11).\nfunction trend(serie) {\n  const pts = [];\n  (serie || []).forEach((v, i) => {\n    if (v !== null && v !== undefined && Number.isFinite(v)) pts.push([i, v]);\n  });\n  if (pts.length < 2) return null;\n  const n = pts.length;\n  const mx = pts.reduce((a, p) => a + p[0], 0) / n;\n  const my = pts.reduce((a, p) => a + p[1], 0) / n;\n  let num = 0, den = 0, sst = 0;\n  for (const pt of pts) {\n    num += (pt[0] - mx) * (pt[1] - my); den += (pt[0] - mx) * (pt[0] - mx); sst += (pt[1] - my) * (pt[1] - my);\n  }\n  const slope = den ? num / den : 0;\n  let y0, y1;\n  if (n === 2) { y0 = pts[0][1]; y1 = pts[1][1]; }\n  else { y0 = my + slope * (pts[0][0] - mx); y1 = my + slope * (pts[n - 1][0] - mx); }\n  let rel = y0 ? (y1 - y0) / Math.abs(y0) : null;   // base zero -> null: cai no ramo NOVO/SUMIU\n  // reta que cruza o zero numa queda convexa daria variação < −100%, impossível: vale a variação crua\n  // entre o 1º e o último ponto válidos\n  if (rel !== null && rel < -1) { const a0 = pts[0][1], a1 = pts[n - 1][1]; rel = a0 ? (a1 - a0) / Math.abs(a0) : null; }\n  let ss = 0;\n  for (const pt of pts) { const f = my + slope * (pt[0] - mx); ss += (pt[1] - f) * (pt[1] - f); }\n  let passos = 0, mesmos = 0;\n  for (let i = 1; i < n; i++) {\n    const d = pts[i][1] - pts[i - 1][1];\n    if (d === 0) continue;\n    passos++;\n    if (slope !== 0 && (d > 0) === (slope > 0)) mesmos++;\n  }\n  return { rel: rel, r2: sst ? 1 - ss / sst : 1, passos: passos, mesmos: mesmos };\n}\n\n// Sufixo de consistência no rótulo. Só aparece com 3+ períodos — com 2 janelas passos <= 1, então\n// a visão de 2 semanas continua visualmente idêntica à validada.\nconst sufProg = (o) => (o.prog && o.prog.passos > 1) ? ' ' + o.prog.mesmos + '/' + o.prog.passos : '';\n\nfunction classify(vBase, vCur, sBase, sCur, serie, rot) {\n  const R = rot || ROT.total;   // textos do divisor (sessões totais × retidas)\n  const out = { e: null, dRes: null, dSess: null, dPer: null, cls: 'estavel', tag: '', why: '' };\n  // ⚠️ Período sem dado NÃO é zero. Tratar como zero fazia toda linha virar NOVO/SUMIU e o impacto\n  // virar o custo integral com sinal negativo — pintado de verde, como se fosse economia.\n  if (vBase === null || vBase === undefined || vCur === null || vCur === undefined) {\n    out.cls = 'zero'; out.tag = 'sem dado';\n    out.why = 'um dos períodos não tem dado carregado — não dá para comparar';\n    return out;\n  }\n  const sOk = (x) => x !== null && x !== undefined && x > 0;\n  if (sOk(sBase) && sCur !== null && sCur !== undefined) out.dSess = (sCur - sBase) / sBase;\n  if (vBase > 0) out.dRes = (vCur - vBase) / vBase;\n  const perB = sOk(sBase) ? vBase / sBase : null;\n  const perC = sOk(sCur) ? vCur / sCur : null;\n  if (perB && perC !== null) out.dPer = (perC - perB) / perB;\n\n  // PROGRESSÃO (semanal): a leitura passa a vir da reta ajustada sobre TODOS os períodos, não de\n  // base-vs-último. Duas séries que terminam no mesmo lugar podem ter caminhos opostos —\n  // [10,18,14,12] e [10,11,12,12] dão ambas +20% no fim, mas +4,5% e +20,6% na reta ajustada.\n  // ⚠️ A árvore de decisão abaixo NÃO muda: só a ORIGEM de dRes/dSess/dPer.\n  if (serie) {\n    // suporte COMUM: cada reta só usa os períodos que têm recurso E sessão. Sem isso, com a sessão\n    // ausente na 1ª ou na última janela, Δrecurso cobria 3 degraus e Δsessões 2, e E saía inflado\n    // (e o mesmo, ao contrário, com o nível 1 de uma janela do meio faltando)\n    const okS = (i) => serie.sess[i] !== null && serie.sess[i] !== undefined && Number.isFinite(serie.sess[i]);\n    const okR = (i) => serie.res[i] !== null && serie.res[i] !== undefined && Number.isFinite(serie.res[i]);\n    const resC = serie.res.map((v, i) => (okS(i) ? v : null));\n    const sessC = serie.sess.map((v, i) => (okR(i) ? v : null));\n    const tr = trend(resC), ts = trend(sessC), tp = trend(serie.per);\n    if (tr && ts && tr.rel !== null && ts.rel !== null) {\n      out.dRes = tr.rel; out.dSess = ts.rel;\n      if (tp && tp.rel !== null) out.dPer = tp.rel;\n      out.prog = tp || tr;\n    }\n  }\n\n  // ruído de ponto flutuante (médias de estoque sobre 30 e 31 dias diferem no último bit) não é direção:\n  // sem isto um estoque parado, com as sessões caindo, saía DIVERGENTE\n  const ruido = (x) => (x !== null && x !== undefined && Math.abs(x) < 1e-9 ? 0 : x);\n  out.dRes = ruido(out.dRes); out.dSess = ruido(out.dSess); out.dPer = ruido(out.dPer);\n\n  if (vBase === 0 && vCur > 0) { out.cls = 'novo'; out.tag = 'NOVO'; out.why = 'não existia no período-base'; return out; }\n  if (vCur === 0 && vBase > 0) { out.cls = 'sumiu'; out.tag = 'SUMIU'; out.why = 'zerou em relação ao período-base'; return out; }\n  if (vBase === 0 && vCur === 0) { out.cls = 'zero'; out.tag = '—'; return out; }\n  // sessão AUSENTE num dos períodos (chamada que falhou) e sem reta que cubra o buraco: sem dado.\n  // Antes a ausência virava 0 → Δsessões −100% → DIVERGENTE / \"E x ↓\" falsos em toda a tabela.\n  if (!out.prog && (sBase === null || sBase === undefined || sCur === null || sCur === undefined)) {\n    out.cls = 'zero'; out.tag = 'sem dado';\n    out.why = R.sess + ' do período não carregadas — sem denominador não há comparação';\n    return out;\n  }\n\n  if (out.dSess === null || Math.abs(out.dSess) < SESS_EPS) {\n    out.why = R.sess + ' praticamente paradas (' + fmtPct(out.dSess) + ') — a divisão explodiria, então a classificação vem do consumo por ' + R.sessao;\n    if (out.dPer === null) { out.cls = 'estavel'; out.tag = '—'; return out; }\n    // ⚠️ MESMA armadilha do ramo de E: usar Math.abs aqui classificava uma QUEDA de 53% no consumo\n    // por sessão — que é ganho de eficiência — como crítico, em vermelho. A magnitude vem do módulo,\n    // a direção vem do SINAL.\n    const ap = Math.abs(out.dPer);\n    if (ap < 0.10) { out.cls = 'estavel'; out.tag = 'esperado'; return out; }\n    out.cls = out.dPer > 0 ? (ap >= 0.30 ? 'critico' : 'atencao') : 'subutilizado';\n    out.tag = fmtPct(out.dPer) + ' ' + R.tag + sufProg(out);\n    return out;\n  }\n  out.e = out.dRes / out.dSess;\n  if (out.e === 0) out.e = 0;   // normaliza -0, que toFixed imprimiria como \"-0.00\"\n  if (out.dRes * out.dSess < 0) {\n    out.cls = 'divergente'; out.tag = 'DIVERGENTE';\n    out.why = 'recurso ' + fmtPct(out.dRes) + ' vs ' + R.sess + ' ' + fmtPct(out.dSess) + ' — direções opostas';\n    return out;\n  }\n  // magnitude por |E|, DIREÇÃO por Δ(consumo/sessão) — ver o comentário em BANDS\n  const a = Math.abs(out.e);\n  const p = (out.dPer === null || !Number.isFinite(out.dPer)) ? 0 : out.dPer;\n  if (Math.abs(p) < 0.05) out.cls = 'esperado';\n  else if (p > 0) out.cls = a >= 2 ? 'critico' : 'atencao';\n  else out.cls = 'subutilizado';\n  out.tag = 'E ' + fx(out.e, 2) + (Math.abs(p) < 0.05 ? '' : (p > 0 ? ' ↑' : ' ↓')) + sufProg(out);\n  out.why = 'recurso ' + fmtPct(out.dRes) + ' ÷ ' + R.sess + ' ' + fmtPct(out.dSess)\n    + ' — consumo por ' + R.sessao + ' ' + fmtPct(out.dPer)\n    + (out.dSess < 0 ? ' (' + R.sess + ' caindo: E alto aqui significa que o recurso caiu MAIS que ' + R.traf + ')' : '');\n  return out;\n}\n\n// ---------- períodos ----------\n// Um \"período\" é a unidade de comparação: 1 dia no diário, 7 dias no semanal, 1 mês de calendário no\n// mensal. Tudo abaixo (agregação, elasticidade, tabela, gráficos) opera sobre períodos, não sobre dias —\n// é o que faz as três seções compartilharem o mesmo código.\nfunction dayPeriods(days) {\n  return days.map((d) => ({ k: d, lab: shortDay(d), sub: dowPt(d), d0: d, d1: d, days: [d] }));\n}\nfunction weekPeriods(endIso, n) {\n  const qtd = Math.max(WEEKS_MIN, Math.min(WEEKS_MAX, n || WEEKS_MAX));\n  // ontem é o último dia (o de hoje ainda está incompleto no warehouse): data no futuro é presa a ontem\n  const ont = yesterday();\n  const fim = (!endIso || endIso > ont) ? ont : endIso;\n  const out = [];\n  for (let i = qtd - 1; i >= 0; i--) {\n    const d1 = addDays(fim, -i * WEEK_LEN);\n    const d0 = addDays(d1, -(WEEK_LEN - 1));\n    const days = [];\n    for (let j = 0; j < WEEK_LEN; j++) days.push(addDays(d0, j));\n    out.push({ k: 'S' + d0, lab: shortDay(d0) + '–' + shortDay(d1), sub: '7 dias', d0, d1, days });\n  }\n  return out;\n}\n// Meses de CALENDÁRIO (01→fim): é o recorte em que a fatura cobra, e o preço do storage já é mensal.\n// O mês corrente entra PARCIAL (d1 = ontem) e marcado: o custo por sessão segue comparável, porque\n// numerador e denominador encolhem juntos, mas o custo TOTAL do mês fica menor só por faltar dia.\nconst MES_PT = ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'];\nfunction monthStart(iso) { return iso.slice(0, 8) + '01'; }\nfunction addMonths(iso, n) {\n  const [y, m] = iso.split('-').map(Number);\n  const d = new Date(y, m - 1 + n, 1);\n  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-01';\n}\nfunction monthLab(iso) { const [y, m] = iso.split('-').map(Number); return MES_PT[m - 1] + '/' + String(y).slice(2); }\nfunction monthPeriods(endIso, n, ontRef) {\n  const qtd = Math.max(MONTHS_MIN, Math.min(MONTHS_MAX, n || MONTHS_MAX));\n  const vivo = yesterday();\n  const base = monthStart((!endIso || endIso > vivo) ? vivo : endIso);   // mês no futuro: preso a ontem\n  // Corte CONGELADO na carga (mRef = último dia de fato carregado): sem isso, às 21h de Brasília (00h UTC)\n  // o mês parcial já carregado ganhava um dia que nunca foi buscado e o storage era rateado por N+1 dias.\n  // Vale para todo mês que começa até ele — um mês carregado como parcial segue parcial mesmo se o mês\n  // final andar —; os meses posteriores (ainda não carregados) usam o ontem de agora.\n  const out = [];\n  for (let i = qtd - 1; i >= 0; i--) {\n    const d0 = addMonths(base, -i);\n    const nd = daysInMonth(d0);\n    const fim = d0.slice(0, 8) + String(nd).padStart(2, '0');\n    const lim = (ontRef && ontRef <= vivo && d0 <= ontRef) ? ontRef : vivo;\n    const parcial = fim > lim;\n    const d1 = parcial ? lim : fim;\n    const days = [];\n    for (let d = d0; d <= d1; d = addDays(d, 1)) days.push(d);\n    out.push({ k: 'M' + d0, lab: monthLab(d0) + (parcial ? '*' : ''),\n      sub: parcial ? days.length + ' de ' + nd + ' dias' : nd + ' dias',\n      d0, d1, days, parcial });\n  }\n  return out;\n}\n\n// ---------- nome do PDF ----------\n// <N>months+<N>weeks+<DD-MM-AA de cada dia do Diário>_NaturaHispana (pedido do dono, 23/10/26). Só entra o\n// que está CARREGADO (a mesma regra do `carr` do printhead), na ordem Mensal, Semanal, Diário; seção sem nada\n// carregado não entra, e sem nada carregado o nome é só NaturaHispana. O Chrome usa o título da página como\n// nome sugerido do arquivo, por isso o título troca só durante a impressão.\nfunction nomePdf(st) {\n  const nM = monthPeriods(st.mEnd, st.mCount, st.mRef).filter((p) => st.mAgg[p.k]).length;\n  const nW = weekPeriods(st.wEnd, st.wCount).filter((p) => st.wAgg[p.k]).length;\n  const dias = Array.from(new Set(dayPeriods(st.days).filter((p) => st.dAgg[p.k]).map((p) => p.d0))).sort();\n  const ddmmaa = (iso) => iso.slice(8, 10) + '-' + iso.slice(5, 7) + '-' + iso.slice(2, 4);\n  const partes = [];\n  if (nM) partes.push(nM + (nM === 1 ? 'month' : 'months'));\n  if (nW) partes.push(nW + (nW === 1 ? 'week' : 'weeks'));\n  for (const d of dias) partes.push(ddmmaa(d));\n  return (partes.length ? partes.join('+') + '_' : '') + 'NaturaHispana';\n}\nlet tituloAntes = null, tituloTimer = null;\nfunction trocaTitulo(novo) {\n  if (typeof document === 'undefined') return;\n  if (tituloAntes === null) tituloAntes = document.title;\n  document.title = novo;\n  clearTimeout(tituloTimer);\n  tituloTimer = setTimeout(restauraTitulo, 60000);   // reserva para navegador sem afterprint\n}\nfunction restauraTitulo() {\n  clearTimeout(tituloTimer);\n  if (typeof document === 'undefined' || tituloAntes === null) return;\n  document.title = tituloAntes;\n  tituloAntes = null;\n}\n\n// ---------- camada de dados ----------\nfunction makeApi(d2) {\n  const ref = d2.param('listenerRef', 'sLSYzN3EtX3kpGdQzYwov');\n  // A chamada vai por d2.rush().invoke(): o POST sai com a autenticação do próprio runtime, e é a SESSÃO\n  // do visitante (login no Authenticator Strategy, role admin) que a policy do listener exige. Nenhum\n  // segredo neste código: o bundle de qualquer app é público. (Até 23/10/26 ia aqui um token fixo\n  // comparado com um KeyVault; vazou pelo bundle e foi aposentado.)\n  // ⚠️ `from`/`until` voltam undefined no body do handler (fato medido; a causa não foi isolada:\n  // product/event/page_size/skip têm o mesmo nome nos requestParameters e funcionam) — por isso a\n  // janela viaja como d0/d1.\n  // Semáforo GLOBAL: o POOL de cada seção e de cada drill é local, então 3 seções + drills chegavam a\n  // 18+ chamadas em voo. As APIs só foram medidas até 6 em paralelo.\n  let emVoo = 0; const fila = [];\n  const abortado = () => Object.assign(new Error('This operation was aborted'), { name: 'AbortError' });\n  // a espera na fila respeita o cancelar: quem foi abortado sai na hora, sem esperar a vaga de outra seção\n  const pega = (signal) => new Promise((res, rej) => {\n    if (signal && signal.aborted) return rej(abortado());\n    if (emVoo < POOL) { emVoo++; return res(); }\n    const w = {};\n    const sair = () => { const i = fila.indexOf(w); if (i >= 0) fila.splice(i, 1); rej(abortado()); };\n    w.vez = () => { if (signal) signal.removeEventListener('abort', sair); res(); };\n    if (signal) signal.addEventListener('abort', sair, { once: true });\n    fila.push(w);\n  });\n  const solta = () => { const prox = fila.shift(); if (prox) prox.vez(); else emVoo--; };\n  const call = async (payload, signal) => {\n    await pega(signal);   // se rejeitar, não pegou vaga: não chama solta()\n    try { return await call0(payload, signal); } finally { solta(); }\n  };\n  // invoke() não recebe AbortSignal: a corrida com o signal devolve o controle na hora do cancelar (a fila\n  // já não dispara as próximas); só as até POOL chamadas em voo terminam em segundo plano. O resultado vira\n  // { ok, status, txt } para o resto do call0 seguir igual ao tempo do fetch.\n  const invocar = (payload, signal) => new Promise((res, rej) => {\n    if (signal && signal.aborted) return rej(abortado());\n    const sair = () => rej(abortado());\n    if (signal) signal.addEventListener('abort', sair, { once: true });\n    const solto = () => { if (signal) signal.removeEventListener('abort', sair); };\n    d2.rush(ref).invoke(payload).then((v) => {\n      solto();\n      res({ ok: true, status: 200, txt: JSON.stringify(v === undefined ? null : v) });\n    }, (e) => {\n      solto();\n      const msg = String((e && e.message) || e || 'falha ao chamar o listener');\n      const st = Number((e && (e.status || e.statusCode || (e.response && e.response.status)))\n        || (msg.match(/\\b([45][0-9][0-9])\\b/) || [])[1] || 0);\n      res({ ok: false, status: st, txt: msg });\n    });\n  });\n  const call0 = async (payload, signal) => {\n    const r = await invocar(payload, signal);\n    const txt = r.txt;\n    // A policy do PORTAL recusando é um caso distinto do 401 do listener de sessões do Natura, e a\n    // mensagem crua não diz qual dos dois falhou. Só o STATUS do próprio listener aponta a policy do\n    // portal: \"no permissions\" dentro de uma resposta 200 vem do upstream (tratado logo abaixo).\n    if (r.status === 401 || r.status === 403) {\n      throw new Error('o listener do portal recusou a chamada (HTTP ' + r.status + '): a sessão expirou ou a conta '\n        + 'não tem acesso (a policy exige login no Authenticator Strategy com role admin). Recarregue a página e '\n        + 'entre de novo; se a policy acabou de mudar, espere ~1 min antes de concluir.');\n    }\n    // ⚠️ O listener de sessões do Natura NÃO valida sessão: a policy dele compara o header\n    // `authorization` BYTE A BYTE com `auth_key` do KeyVault 4kYx6aU00vaMSQnGP5mOp. Por isso a\n    // mensagem genérica \"Selected session has no permissions\" aparece igual para token ausente,\n    // errado, dessincronizado e para policy sendo editada — re-testar antes de concluir a causa.\n    // O texto do upstream vai junto: o 424 também pode vir de falha que não é de permissão.\n    const sessRecusou = (cru) => new Error('Sessões recusadas (401). Quando a policy do listener do Natura está ativa, ela compara '\n      + 'o header Authorization byte a byte com auth_key do KeyVault 4kYx6aU00vaMSQnGP5mOp; o nosso valor sai '\n      + 'de natura-api-vault/sessions_token. A MESMA mensagem aparece para segredo diferente, policy sendo '\n      + 'alterada naquele instante, ou header ausente — ela não distingue as causas. Re-teste depois de alguns '\n      + 'minutos ANTES de concluir que o segredo dessincronizou. O consumo não é afetado: ele vem da API do Management, por outro caminho.'\n      + (cru && !/^HTTP [0-9]+$/.test(cru) ? ' Resposta do listener: ' + String(cru).slice(0, 140) : ''));\n    let json;\n    try { json = JSON.parse(txt); }\n    catch (e) {\n      // 424 das sessões com corpo vazio ou texto: o mesmo diagnóstico do 424 com JSON\n      if (payload.op === 'sessions' && r.status === 424) throw sessRecusou(txt.trim());\n      throw new Error('resposta não-JSON (HTTP ' + r.status + '): ' + txt.slice(0, 140));\n    }\n    const out = json && json.data ? json.data : json;   // listener responde envelopado\n    // \"no permissions\" vindo do upstream = recusa da chamada que o PROXY fez (gate do Management desde\n    // 02/10/26, ou a fonte de sessões) — não a policy do portal nem o segredo dele (esses dão 401/403 acima)\n    const gate = (extra) => new Error((payload.op === 'sessions'\n      ? 'a fonte de sessões recusou a chamada feita pelo proxy do listener (não é a policy do portal): confira o token em natura-api-vault/sessions_token e a policy da fonte, e re-teste em alguns minutos.'\n      : 'a API de consumo do Management recusou a chamada feita pelo proxy do listener (gate desde 02/10/26) — não é a policy do portal nem o segredo dele. Re-teste em alguns minutos; se persistir, o acesso do system de trabalho à API mudou.')\n      + (extra || ''));\n    // as linhas só saem do teste quando são DADO (lista com product/measure_name): um path ou nome de recurso\n    // com essas palavras não é recusa. A recusa repassada em rows pelo handler (rows: $ores — objeto, texto ou\n    // lista sem linha válida) continua sendo lida: é o formato documentado do gate (TXT, §15 1b)\n    const temLinha = !!(out && typeof out === 'object' && Array.isArray(out.rows)\n      && out.rows.some((x) => x && x.product && x.measure_name));\n    const corpo = (out && typeof out === 'object')\n      ? JSON.stringify(temLinha ? Object.assign({}, out, { rows: undefined }) : out) : String(txt);\n    if (r.ok && out && !out.error && /no permissions/i.test(corpo)) throw gate();\n    if (!r.ok || !out || out.error) {\n      const cru = (out && out.error) || ('HTTP ' + r.status);\n      const txt2 = typeof cru === 'string' ? cru : JSON.stringify(cru);\n      if (payload.op === 'sessions' && (r.status === 424 || /401|424|no permissions/i.test(txt2))) throw sessRecusou(txt2);\n      if (/no permissions/i.test(txt2)) throw gate(' Resposta do listener: ' + txt2.slice(0, 140));\n      throw new Error(txt2);\n    }\n    return out;\n  };\n  return {\n    sessions: (d0, d1, sig) => call({ op: 'sessions', d0, d1 }, sig),\n    overview: (d0, d1, sig) => call({ op: 'overview', d0, d1 }, sig),\n    detailPage: (d0, d1, product, event, skip, sig) =>\n      call({ op: 'detail', d0, d1, product, event, page_size: PAGE, skip }, sig)\n  };\n}\n\n// `stop` é o que faz o cancelar realmente cancelar: sem ele os workers drenavam a fila inteira e as\n// chamadas restantes eram cobradas normalmente — cancelar não economizava um centavo.\nasync function pool(tasks, size, onTick, stop) {\n  const out = new Array(tasks.length);\n  let i = 0, done = 0;\n  await Promise.all(new Array(Math.min(size, tasks.length)).fill(0).map(async () => {\n    for (;;) {\n      if (stop && stop()) return;\n      const k = i++;\n      if (k >= tasks.length) return;\n      try { out[k] = await tasks[k](); }\n      catch (err) { out[k] = { __err: (err && err.message) || String(err) }; }\n      done++;\n      if (onTick) onTick(done, tasks.length);\n    }\n  }));\n  return out;\n}\n\n// nível 3 de um par num período, paginando até acabar OU até o teto — a truncagem é devolvida,\n// nunca escondida, porque é ela que decide se o total pode vir do detalhe.\nasync function fetchDetail(api, per, product, event, maxPages, sig) {\n  const rows = [];\n  let dim = null, truncated = false, pages = 0;\n  for (;;) {\n    const out = await api.detailPage(per.d0, per.d1, product, event, pages * PAGE, sig);\n    // sem lista = resposta defeituosa do upstream (corpo de erro repassado pelo handler): vira erro do\n    // período, nunca \"lista vazia completa\" — senão o detalhe trocaria o nível 1 por 0 sem aviso\n    if (!out || !Array.isArray(out.rows)) throw new Error('detalhe sem lista de linhas (o upstream não trouxe results)');\n    const list = out.rows;\n    if (list.length && !dim) dim = Object.keys(list[0]).find((k) => !BASE_KEYS.has(k)) || null;\n    for (const r of list) {\n      const v = numEstrito(r.sum_measure_value);\n      // valor fora do formato não vira 0 em silêncio: o detalhe completo trocaria o nível 1 por uma soma errada\n      if (!Number.isFinite(v)) throw new Error('detalhe com valor não numérico (' + mostraValor(r.sum_measure_value) + ')');\n      rows.push({ r: dim ? r[dim] : null, v, date: r.date });\n    }\n    pages++;\n    if (list.length < PAGE) break;\n    if (pages >= (maxPages || MAX_PAGES)) { truncated = true; break; }\n  }\n  // ⚠️ A API pagina ao longo do EIXO DATA, ordenada por data: UMA página de 500 linhas de TABLE/read\n  // cobre ~2,2 dias de uma semana (medido: 238 de 10/08 + 224 de 11/08 + 38 de 12/08). Somar isso e\n  // exibir como \"a semana\" mostra ~60% do valor como se fosse o total. Quando truncou, o último dia\n  // veio pela metade: descarta-se esse dia e declara-se quais dias a lista realmente cobre.\n  let cover = null;\n  if (truncated) {\n    const ds = [];\n    for (const x of rows) if (x.date && ds.indexOf(x.date) < 0) ds.push(x.date);\n    ds.sort();\n    // o último dia veio pela metade -> fora. Em período de 1 dia isso zera a cobertura, e é a verdade:\n    // não há nenhum dia íntegro na lista.\n    cover = ds.slice(0, -1);\n  }\n  return { dim: dim || meta(product, event).dim || null, rows, truncated, pages, cover };\n}\n\n// ---------- agregação ----------\n// ⚠️ Regra que separa certo de errado: FLUXO soma entre os dias do período; ESTOQUE (`storage`)\n// tira a MÉDIA — somar estoque de 7 dias daria 7× o valor real.\nfunction aggLevel1(rows) {\n  const acc = {};\n  for (const r of (Array.isArray(rows) ? rows : [])) {\n    if (!r || !r.product || !r.measure_name) continue;   // linha fora do formato não vira par fantasma\n    const k = key(r.product, r.measure_name);\n    if (!acc[k]) acc[k] = { sum: 0, n: 0, datas: new Set() };\n    acc[k].sum += num(r.sum_measure_value); acc[k].n++;\n    if (r.date) acc[k].datas.add(r.date);\n  }\n  const out = {};\n  for (const k in acc) {\n    const i = k.indexOf('|');\n    // estoque = média por DIA presente (datas distintas), não por linha: uma linha de estoque partida em duas\n    // no mesmo dia dividiria o total pela metade\n    const nd = acc[k].datas.size || acc[k].n;\n    out[k] = isStock(k.slice(0, i), k.slice(i + 1)) ? acc[k].sum / nd : acc[k].sum;\n  }\n  return out;\n}\n\nfunction aggDetail(det, p, e, dias) {\n  if (!det || det.__err || det.__loading) return null;\n  const cov = det.cover ? new Set(det.cover) : null;   // só os dias integralmente cobertos\n  const acc = new Map();\n  for (const x of det.rows) {\n    if (cov && x.date && !cov.has(x.date)) continue;\n    const k = (x.r === null || x.r === undefined) ? '' : String(x.r);\n    if (!acc.has(k)) acc.set(k, { sum: 0, n: 0 });\n    const a = acc.get(k); a.sum += x.v; a.n++;\n  }\n  const stock = isStock(p, e);\n  // estoque = média do TOTAL do período: divide pelos dias (cobertos) do período, não pelas linhas\n  // do próprio recurso — senão um recurso criado no meio do período entrava com peso cheio\n  // dias que TÊM dado no detalhe: um dia sem linha nenhuma (buraco de ingestão) não dilui a média — o\n  // nível 1 também só tira a média dos dias presentes. O recurso que só existe no 7º dia segue com 1/7.\n  const datas = new Set();\n  for (const x of det.rows) if (x.date && (!cov || cov.has(x.date))) datas.add(x.date);\n  const nd = det.cover ? det.cover.length : (datas.size || dias || 0);\n  const out = new Map();\n  for (const [k, a] of acc) out.set(k, stock ? a.sum / (nd || a.n) : a.sum);\n  return out;\n}\n\n// O nível 1 é a fonte padrão. O detalhe só substitui quando está COMPLETO — um detalhe truncado\n// vira um total menor que o real, e o erro é invisível. Foi o bug da fase 1.\nfunction valueOf(store, per, p, e) {\n  const a = store.agg[per.k];\n  const carregado = !!(a && Object.keys(a).length);\n  // par AUSENTE de um nível 1 que carregou = não consumiu no período (0), não \"sem dado\": a API não\n  // devolve linha zerada. null fica só para o período cujo nível 1 não veio.\n  const lvl1 = !carregado ? null : (a[key(p, e)] === undefined ? 0 : a[key(p, e)]);\n  const m = meta(p, e);\n  // nodrill: o nível 3 desse par é OUTRA grandeza (média por system) e nunca substitui o nível 1.\n  // Período sem nível 1: o detalhe não vale sozinho (KPI e empilhado não teriam esse período).\n  const det = (m.nodrill || !carregado) ? undefined : store.det[per.k + '|' + p + '|' + e];\n  if (det && !det.__err && !det.__loading) {\n    if (det.truncated) {\n      return { v: lvl1, src: 'nivel1', lvl1, mean: m.agg1 === 'avg', truncated: true,\n               diverges: false, noClose: m.agg1 === 'avg' };\n    }\n    const map = aggDetail(det, p, e, per.days.length);\n    let s = 0; map.forEach((x) => { s += x; });\n    // detalhe \"completo\" que soma 0 com o nível 1 > 0 é lista que veio vazia (falha do upstream, nível 3\n    // atrasado), não consumo zero: vale o nível 1, sinalizado — senão o par mais caro sumia do KPI\n    if (s === 0 && lvl1 > 0) {\n      return { v: lvl1, src: 'nivel1', lvl1, mean: m.agg1 === 'avg', truncated: false, diverges: false,\n               noClose: false, vazio: true };\n    }\n    return { v: s, src: 'detalhe', lvl1, mean: false, truncated: false, noClose: false,\n             diverges: lvl1 !== null && Math.abs(s - lvl1) > Math.max(1, Math.abs(lvl1) * 1e-6) };\n  }\n  return { v: lvl1, src: 'nivel1', lvl1, mean: m.agg1 === 'avg', truncated: false, diverges: false, noClose: false };\n}\n\n// A MESMA decisão de buildRows para pares de MÉDIA, agora também nos totais: se algum período ficou\n// sem detalhe completo, todos usam o nível 1 (antes o KPI somava total num período e média no outro).\nfunction valueOfAll(store, periods, per, p, e) {\n  const vo = valueOf(store, per, p, e);\n  if (meta(p, e).agg1 !== 'avg' || vo.src !== 'detalhe') return vo;\n  const todos = periods.every((q) => valueOf(store, q, p, e).src === 'detalhe');\n  return todos ? vo : Object.assign({}, vo, { v: vo.lvl1, src: 'nivel1', mean: true, diverges: false });\n}\n\nfunction buildRows(store, periods, prog) {\n  const pairs = new Map();\n  for (const per of periods) for (const k in (store.agg[per.k] || {})) {\n    const i = k.indexOf('|');\n    pairs.set(k, { p: k.slice(0, i), e: k.slice(i + 1) });\n  }\n  const base = periods[0];\n  const sBase = nz(store.sess[base.k]);\n  const out = [];\n  pairs.forEach(({ p, e }) => {\n    const m = meta(p, e);\n    // ESTOQUE por sessão em GiB·dia (o nível médio × os dias do período), que é o que a fatura cobra. Sem isso\n    // o mês parcial (ou de 28 dias) dividia uma MÉDIA por sessões SOMADAS de menos dias, e o por-sessão do\n    // storage pulava até 30× sem mudança real: DIVERGENTE falso no mensal padrão (TXT, §13A RO19).\n    const fEst = (per) => (m.k === 'stock' ? per.days.length : 1);\n    const vE = (c) => (c.v === null ? null : c.v * fEst(c.per));\n    const cells = periods.map((per) => {\n      const vo = valueOf(store, per, p, e);\n      const s = store.sess[per.k];\n      const c = vo.v === null ? null : costOf(p, e, vo.v, per.d1, per.days);\n      return Object.assign({}, vo, {\n        per, c,\n        perS: (vo.v !== null && s) ? vo.v * fEst(per) / s : null,\n        cps: (c !== null && s) ? c / s : null\n      });\n    });\n    // ⚠️ Para métricas cujo nível 1 é MÉDIA por recurso (agg1:'avg'; hoje só login), o detalhe completo é a SOMA —\n    // grandezas que diferem por um fator igual ao nº de recursos. Se QUALQUER período ficou sem detalhe,\n    // a linha inteira volta ao nível 1; misturar as duas bases célula a célula produz uma linha em que\n    // 3,3 GiB e 107 GiB convivem como se fossem comparáveis.\n    if (m.agg1 === 'avg' && cells.some((c) => c.src !== 'detalhe')) {\n      cells.forEach((c) => {\n        if (c.src === 'detalhe') {\n          c.v = c.lvl1; c.src = 'nivel1'; c.mean = true; c.diverges = false;\n          c.c = c.v === null ? null : costOf(p, e, c.v, c.per.d1, c.per.days);\n          const s = store.sess[c.per.k];\n          c.perS = (c.v !== null && s) ? c.v * fEst(c.per) / s : null;\n          c.cps = (c.c !== null && s) ? c.c / s : null;\n        }\n      });\n    }\n    const b = cells[0];\n    const cls = cells.map((c, i) => (i === 0 ? null : classify(vE(b), vE(c), sBase, nz(store.sess[c.per.k]), undefined, store.rot)));\n    let worst = null;\n    for (const c of cls) {\n      if (!c) continue;\n      if (!worst) { worst = c; continue; }\n      const rk = (x) => ({ critico: 4, divergente: 4, novo: 3, sumiu: 3, atencao: 2, subutilizado: 1, esperado: 0, estavel: 0, zero: -1 }[x.cls] || 0);\n      if (rk(c) > rk(worst) || (rk(c) === rk(worst) && Math.abs(c.e || 0) > Math.abs(worst.e || 0))) worst = c;\n    }\n    // No semanal a verdade da linha é a PROGRESSÃO inteira, não o pior período isolado: com 4\n    // janelas o \"pior\" pode ser um pico que já voltou ao lugar. Com 2 janelas isto devolve\n    // exatamente o mesmo que o laço acima — a reta por 2 pontos É a reta base->último.\n    if (prog && cells.length > 1) {\n      const last = cells[cells.length - 1];\n      const pr = classify(vE(b), vE(last), sBase, nz(store.sess[last.per.k]), {\n        res: cells.map(vE),\n        sess: cells.map((c) => (store.sess[c.per.k] === undefined ? null : store.sess[c.per.k])),\n        per: cells.map((c) => c.perS)\n      }, store.rot);\n      if (pr.cls !== 'zero') worst = pr;\n    }\n    // estoque: o Δ% do \"recurso\" no porquê é de GiB·dia (nível médio × dias), não do volume da linha — sem o\n    // rótulo, no mensal o tooltip dizia \"recurso −96,7%\" ao lado de um nível que subiu 0,4%. Set: worst pode\n    // ser um dos objetos de cls, e a troca não pode acontecer duas vezes no mesmo objeto.\n    if (m.k === 'stock') new Set([...cls, worst]).forEach((c) => {\n      if (c && c.why) c.why = c.why.replace(/^recurso /, 'recurso em GiB·dia (nível médio × dias) ');\n    });\n    let impact = 0;\n    // períodos de tamanhos diferentes (mês parcial; meses de 30 e 31 dias) entram na escala da base —\n    // senão o mês parcial vira \"economia\" só por faltar dia. FLUXO compara no mesmo ritmo diário.\n    // ESTOQUE (GiB-mês) não: um mês cheio custa o mesmo com 30 ou 31 dias, e dividir por dias inventava\n    // impacto com o storage parado — ele compara o NÍVEL médio, precificado nos dias da base.\n    const nb = b.per.days.length;\n    const estoque = m.k === 'stock';\n    for (let i = 1; i < cells.length; i++) {\n      if (cells[i].c === null || b.c === null) continue;   // sem dado != queda a zero\n      const ni = cells[i].per.days.length;\n      let d = estoque ? costOf(p, e, cells[i].v, b.per.d1, b.per.days) - b.c\n        : (ni === nb || !ni || !nb) ? cells[i].c - b.c : (cells[i].c / ni - b.c / nb) * nb;\n      // resíduo de ponto flutuante (1e-13 onde a conta exata dá 0) não é impacto: sairia colorido\n      if (Math.abs(d) <= 1e-9 * Math.max(Math.abs(b.c), Math.abs(cells[i].c))) d = 0;\n      if (Math.abs(d) > Math.abs(impact)) impact = d;\n    }\n    const maxCost = cells.reduce((a, c) => Math.max(a, c.c || 0), 0);\n    // ⚠️ A tendência plota EXATAMENTE os valores impressos nas células — mesma grandeza, mesma\n    // granularidade, mesma regra de agregação. Plotar valor÷sessões (ou a série diária sob células\n    // semanais) faz o desenho responder a outra pergunta que não a dos números ao lado.\n    out.push({ p, e, m, cells, cls, worst, impact, baseCost: b.c || 0, maxCost,\n               spark: cells.map((c) => c.v),\n               // o volume que o TRÁFEGO sozinho explicaria, ancorado no período-base (estoque: tráfego POR DIA,\n               // porque o nível médio não cresce com o nº de dias do período)\n               refSpark: cells.map((c) => {\n                 const s = store.sess[c.per.k];\n                 return (cells[0].v !== null && sBase && s) ? cells[0].v * (s / sBase) * (fEst(cells[0].per) / fEst(c.per)) : null;\n               }) });\n  });\n  // Pareto: quem forma os primeiros 80% do custo fica marcado TOP CUSTO em QUALQUER ordenação.\n  const tot = out.reduce((a, r) => a + r.maxCost, 0);\n  let cum = 0;\n  out.slice().sort((a, b) => b.maxCost - a.maxCost).forEach((r) => {\n    r.share = tot ? r.maxCost / tot : 0;\n    r.top = cum < PARETO * tot && r.maxCost > 0;\n    cum += r.maxCost;\n  });\n  return out;\n}\n\nfunction sortRows(rows, mode, minCost) {\n  const r = rows.slice();\n  if (mode === 'elasticidade') {\n    const rk = (x) => ({ critico: 4, divergente: 4, novo: 3, sumiu: 3, atencao: 2, subutilizado: 1, esperado: 0, estavel: 0, zero: -1 }[x] || 0);\n    return r.filter((x) => Math.max(Math.abs(x.impact), x.maxCost) >= minCost)\n      .sort((a, b) => {\n        const ra = rk(a.worst && a.worst.cls), rb = rk(b.worst && b.worst.cls);\n        if (ra !== rb) return rb - ra;\n        return Math.abs((b.worst && b.worst.e) || 0) - Math.abs((a.worst && a.worst.e) || 0);\n      });\n  }\n  return r.sort((a, b) => (b.maxCost + Math.abs(b.impact)) - (a.maxCost + Math.abs(a.impact)));\n}\n\nfunction periodTotals(store, periods) {\n  return periods.map((per) => {\n    const a = store.agg[per.k];\n    // ⚠️ PESSOAS entra em `total`, NUNCA em `cost`: `cps` deriva de `cost`, e o custo por sessão tem\n    // de continuar sendo só de infraestrutura — é a métrica-âncora já validada. Somar aqui a\n    // contaminaria de um custo fixo que não tem relação nenhuma com o volume de sessões.\n    const nAloc = store.alloc ? store.alloc(per) : 0;\n    const aloc = nAloc * (store.allocUsd || 0);\n    // sem câmbio digitado (G16) a alocação NÃO tem valor em US$: fica fora do total, com aviso no KPI e no PDF\n    const alocPend = nAloc > 0 && !(store.allocUsd > 0);\n    if (!a || !Object.keys(a).length) {\n      const s0 = store.sess[per.k];\n      // total null: sem a infra, \"pessoas\" sozinho leria como o custo do período\n      return { per, cost: null, sessions: (s0 === undefined ? null : s0), cps: null, semDados: true,\n        sessTot: nz((store.sessTot || {})[per.k]), sessRet: nz((store.sessRet || {})[per.k]),\n        nAloc, aloc, alocPend, total: null };\n    }\n    let cost = 0, unpriced = 0;\n    for (const k in (store.agg[per.k] || {})) {\n      const i = k.indexOf('|'); const p = k.slice(0, i), e = k.slice(i + 1);\n      const vo = valueOfAll(store, periods, per, p, e);\n      if (vo.v === null) continue;\n      const c = costOf(p, e, vo.v, per.d1, per.days);\n      if (c === null) { unpriced++; continue; }\n      cost += c;\n    }\n    const s = store.sess[per.k];\n    return { per, cost, sessions: (s === undefined ? null : s), cps: s ? cost / s : null, unpriced,\n      sessTot: nz((store.sessTot || {})[per.k]), sessRet: nz((store.sessRet || {})[per.k]),\n      nAloc, aloc, alocPend, total: cost + aloc };\n  });\n}\n\nfunction costByProduct(store, per, periods) {\n  const acc = {};\n  // período sem nível 1: nenhuma barra (uma barra só de PESSOAS leria como o custo do período)\n  if (!store.agg[per.k] || !Object.keys(store.agg[per.k]).length) return acc;\n  for (const k in (store.agg[per.k] || {})) {\n    const i = k.indexOf('|'); const p = k.slice(0, i), e = k.slice(i + 1);\n    const vo = valueOfAll(store, periods || [per], per, p, e);\n    if (vo.v === null) continue;\n    const c = costOf(p, e, vo.v, per.d1, per.days);\n    if (c === null) continue;\n    acc[p] = (acc[p] || 0) + c;\n  }\n  // O laço acima é dirigido pelas chaves de store.agg, então um custo que NÃO vem da API só pode\n  // entrar aqui, depois. Sem isto o empilhado deixaria de somar o mesmo que o KPI ao lado.\n  const aloc = (store.alloc ? store.alloc(per) : 0) * (store.allocUsd || 0);\n  if (aloc > 0) acc.PESSOAS = aloc;\n  return acc;\n}\n\n// eager: os pares que mais pesam no custo do período-base, + o evento-irmão de contagem quando um\n// *_duration entra, + pares de MÉDIA com preço (agg1:'avg' — sem detalhe o total do período erra).\nfunction pickEager(store, periods, topN) {\n  // escolhe pelo período-base; se o overview da base falhou, usa o 1º período que tem dado\n  const base = periods.find((q) => store.agg[q.k] && Object.keys(store.agg[q.k]).length) || periods[0];\n  const a = store.agg[base.k] || {};\n  const presente = (k) => periods.some((q) => store.agg[q.k] && store.agg[q.k][k] !== undefined);\n  // par sem quebra por recurso (nodrill: TABLE/storage) não tem detalhe útil — nunca é pré-carregado,\n  // senão a média por system do nível 3 substituiria o total do nível 1 na célula\n  const scored = Object.keys(a).filter((k) => !(PRICES[k] && PRICES[k].nodrill)).map((k) => {\n    const i = k.indexOf('|'); const p = k.slice(0, i), e = k.slice(i + 1);\n    return { p, e, c: costOf(p, e, a[k], base.d1, base.days) || 0 };\n  }).sort((x, y) => y.c - x.c);\n  const seen = new Set(), chosen = [];\n  const add = (p, e, qualquer) => { const k = key(p, e);\n    if (!seen.has(k) && (qualquer ? presente(k) : a[k] !== undefined)) { seen.add(k); chosen.push({ p, e }); } };\n  for (const s of scored.slice(0, (topN === undefined || topN === null) ? TOP_N : topN)) {\n    add(s.p, s.e);\n    if (s.e.endsWith('_duration')) add(s.p, s.e.replace(/_duration$/, ''));\n  }\n  // par de MÉDIA com preço só tem o total pelo detalhe: entra sempre que aparecer em QUALQUER período.\n  // (Desde 02/10/26 DRIVE/TABLE storage vêm totais no nível 1, então hoje nenhum par cai aqui.)\n  for (const k in PRICES) {\n    const m = PRICES[k];\n    if (m.agg1 === 'avg' && m.p && !m.nodrill) { const i = k.indexOf('|'); add(k.slice(0, i), k.slice(i + 1), true); }\n  }\n  return chosen;\n}\n\nconst CSS = `\n:host { display:block; width:100%; height:100%; }\n.lgn { box-sizing:border-box; width:100%; min-height:100%; display:flex; align-items:center; justify-content:center;\n  padding:24px; background:#0b1020; color:#e6ecff; font-family:system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }\n.lgn form { width:100%; max-width:380px; background:#121a30; border:1px solid #233150; border-radius:14px;\n  padding:28px 26px; display:flex; flex-direction:column; gap:14px; }\n.lgn h1 { margin:0; font-size:19px; font-weight:700; }\n.lgn .lgsub { margin:-6px 0 6px; font-size:13px; color:#93a3c8; }\n.lgn label { display:flex; flex-direction:column; gap:6px; font-size:13px; color:#cbd5e1; }\n.lgn input { width:100%; padding:10px 12px; border-radius:8px; border:1px solid #2c3d61; background:#0b1020;\n  color:#f8fafc; font-size:14px; outline:none; }\n.lgn input:focus { border-color:#5b9bff; }\n.lgn button { margin-top:4px; padding:11px 12px; border:0; border-radius:8px; background:#2563eb; color:#fff;\n  font-size:14px; font-weight:600; cursor:pointer; }\n.lgn button[disabled] { opacity:.6; cursor:default; }\n.lgn .lgerr { font-size:13px; color:#fca5a5; background:#3b1219; border:1px solid #7f1d1d; border-radius:8px; padding:9px 11px; }\n*, *::before, *::after { box-sizing:border-box; }\n.wrap {\n  --bg:#0b1020; --panel:#121a30; --panel2:#0f1729; --line:#233150; --txt:#e6ecff; --dim:#93a3c8;\n  --accent:#5b9bff; --ok:#22c55e; --warn:#f59e0b; --bad:#ef4444; --mut:#64748b;\n  background:var(--bg); color:var(--txt); height:100%; width:100%; overflow-y:auto; overflow-x:hidden;\n  font-family:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n  font-size:13px; line-height:1.45; padding:20px 22px 80px;\n}\n.wrap.light { --bg:#f6f8fc; --panel:#ffffff; --panel2:#eef2f9; --line:#dbe3f0; --txt:#0f172a;\n  --dim:#5b6b8c; --accent:#2563eb; --mut:#94a3b8; --ok:#166534; --warn:#92400e; --bad:#b91c1c; }\nh1,h2,h3 { margin:0; font-weight:650; letter-spacing:-.01em; }\nh1 { font-size:19px; } h2 { font-size:14px; margin-bottom:10px; } h3 { font-size:12px; }\n.sub { color:var(--dim); font-size:12px; }\n.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }\n.between { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; flex-wrap:wrap; }\n.card { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }\n.mt { margin-top:16px; } .mt8 { margin-top:8px; }\n.secthead { display:flex; align-items:baseline; gap:10px; margin:30px 0 12px; }\n.secthead .n { width:24px; height:24px; border-radius:7px; background:var(--accent); color:#fff;\n  display:grid; place-items:center; font-size:12px; font-weight:700; flex:none; }\nbutton { font:inherit; color:inherit; cursor:pointer; border-radius:8px; border:1px solid var(--line);\n  background:var(--panel2); padding:6px 11px; transition:.12s; }\nbutton:hover { border-color:var(--accent); }\nbutton.pri { background:var(--accent); border-color:var(--accent); color:#fff; font-weight:600; }\nbutton.pri:disabled { opacity:.45; cursor:not-allowed; }\nbutton.on { background:var(--accent); border-color:var(--accent); color:#fff; }\nbutton.ghost { background:transparent; }\ninput[type=date], input[type=number], input[type=text] { font:inherit; color:inherit; background:var(--panel2);\n  border:1px solid var(--line); border-radius:8px; padding:5px 8px; }\n.chip { display:inline-flex; align-items:center; gap:7px; background:var(--panel2);\n  border:1px solid var(--line); border-radius:999px; padding:4px 6px 4px 11px; }\n.chip.base { border-color:var(--accent); }\n.chip .x { border:0; background:transparent; color:var(--dim); padding:0 5px; line-height:1; font-size:15px; }\n/* seletor do divisor (SESSÕES totais | retidas): dois botões colados no chip, com o mesmo .on do sort */\n.chip.seg { gap:0; padding:3px 3px 3px 11px; }\n.chip.seg b { margin-right:7px; }\n.chip.seg button { padding:3px 10px; border-radius:999px; border-color:transparent; background:transparent; }\n.chip.seg button.on { background:var(--accent); border-color:var(--accent); color:#fff; }\n.chip.seg button:disabled { opacity:.45; cursor:not-allowed; }\n/* câmbio (G16): âmbar enquanto não digitado (o R$ e as alocações esperam por ele); vermelho com valor recusado */\n.chip.fxpend { border-color:var(--warn); }\n.chip.fxerr { border-color:var(--bad); }\n/* o input do câmbio no chip: 2px na vertical (senão o chip fica 6 px mais alto que UNIVERSO/SESSÕES e a página desce),\n   4px na horizontal (o placeholder \"cotação\" cabe inteiro nos 64 px, até com DejaVu) */\n.chip input[data-act=setfx] { padding:2px 4px; }\ninput[data-act=mincost][aria-invalid=true] { border-color:var(--bad); }\n/* a nota do câmbio recusado FLUTUA logo abaixo da barra (altura 0 no fluxo): entrar e sair no blur do campo não empurra\n   a página, e o clique que causou o blur não muda de alvo. Cobre o título do cartão \"Como ler\" (texto, sem botão):\n   a altura mínima esconde o título inteiro, sem deixar uma fatia aparecendo sob a nota de 1 linha */\n.fxnota { position:relative; height:0; z-index:5; }\n.fxnota > .note { position:absolute; top:0; left:0; right:0; box-sizing:border-box; min-height:44px; pointer-events:none;\n  background:linear-gradient(rgba(245,158,11,.09), rgba(245,158,11,.09)), var(--panel); box-shadow:0 6px 18px rgba(0,0,0,.28); }\n.tag { display:inline-block; font-size:10.5px; font-weight:700; letter-spacing:.03em;\n  padding:2px 6px; border-radius:5px; white-space:nowrap; }\n.t-critico,.t-divergente { background:rgba(239,68,68,.16); color:#fca5a5; }\n.t-atencao { background:rgba(245,158,11,.16); color:#fcd34d; }\n.t-esperado,.t-estavel { background:rgba(100,116,139,.16); color:var(--dim); }\n.t-subutilizado { background:rgba(56,189,248,.14); color:#7dd3fc; }\n.t-novo { background:rgba(34,197,94,.16); color:#86efac; }\n.t-sumiu { background:rgba(148,163,184,.18); color:#cbd5e1; }\n.t-zero { background:transparent; color:var(--mut); }\n.t-top { background:rgba(91,155,255,.16); color:#93c5fd; }\n.light .t-critico,.light .t-divergente { color:#b91c1c; } .light .t-atencao { color:#a16207; }\n.light .t-novo { color:#15803d; } .light .t-subutilizado { color:#0369a1; } .light .t-top { color:#1d4ed8; }\n.light .t-sumiu { color:#475569; } .light .err { color:#b91c1c; background:#fff; }\n/* números do HERO: cor por classe, para o tema claro e o papel trocarem (o fill fixo dava ~2:1 no branco) */\n.hS { fill:#38bdf8; } .hC { fill:#f472b6; } .light .hS { fill:#0369a1; opacity:1; } .light .hC { fill:#be185d; opacity:1; }\n.hero { display:grid; grid-template-columns:minmax(340px,1.6fr) minmax(240px,1fr); gap:14px; }\n.heroK { display:flex; flex-direction:column; gap:10px; }\n.kpi { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:11px 14px; }\n.kpi.isbase { border-color:var(--accent); }\n.kpi .lab { color:var(--dim); font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; }\n.kpi .big { font-size:21px; font-weight:680; margin-top:2px; letter-spacing:-.02em; }\n.kpi .foot { font-size:11.5px; color:var(--dim); margin-top:2px; }\n.scroll { overflow-x:auto; }\ntable { border-collapse:collapse; width:100%; font-variant-numeric:tabular-nums; }\nth, td { text-align:right; padding:6px 9px; border-bottom:1px solid var(--line); white-space:nowrap; }\nth { color:var(--dim); font-size:10.5px; font-weight:600; text-transform:uppercase; letter-spacing:.04em;\n  background:var(--panel); }\nth.l, td.l { text-align:left; }\ntbody tr:hover { background:var(--panel2); }\ntr.grp { cursor:pointer; }\ntr.grp.istop td { background:linear-gradient(90deg, rgba(91,155,255,.07), transparent 40%); }\ntr.det td { background:var(--panel2); font-size:11.5px; color:var(--dim); }\ntr.det td.l { padding-left:26px; }\n.nm { display:flex; align-items:center; gap:8px; min-width:250px; }\n.nm .bar { width:52px; height:22px; background:var(--panel2); border-radius:4px; overflow:hidden; flex:none; position:relative; }\n.nm .bar i { position:absolute; left:0; top:0; bottom:0; opacity:.85; }\n.nm .bar b { position:absolute; inset:0; display:grid; place-items:center; font-size:9.5px; font-weight:600; color:var(--txt); }\n.ev { font-weight:600; }\n.stack { display:flex; flex-direction:column; align-items:flex-end; line-height:1.25; }\n.stack .v { font-weight:600; }\n.stack .s { font-size:10.5px; color:var(--dim); }\n.stack .un { font-size:9px; color:var(--dim); font-weight:400; margin-left:1px; }\n.mono { font-family:ui-monospace,'SF Mono',Menlo,monospace; font-size:11px; color:var(--mut); }\n.warnI { color:var(--warn); cursor:help; }\n.bad { color:var(--bad); } .good { color:var(--ok); }\n.note { background:rgba(245,158,11,.09); border:1px solid rgba(245,158,11,.3); color:var(--warn);\n  border-radius:8px; padding:9px 12px; font-size:11.5px; }\n.err { background:rgba(239,68,68,.1); border:1px solid rgba(239,68,68,.35); color:#fca5a5;\n  border-radius:8px; padding:9px 12px; font-size:12px; }\n.bar2 { height:6px; border-radius:3px; background:var(--panel2); overflow:hidden; }\n.bar2 i { display:block; height:100%; background:var(--accent); transition:width .2s; }\n.charts { display:grid; grid-template-columns:repeat(auto-fit,minmax(330px,1fr)); gap:14px; }\nsvg { display:block; width:100%; height:auto; overflow:visible; }\n.lg { display:flex; gap:12px; flex-wrap:wrap; font-size:11px; color:var(--dim); margin-top:8px; }\n.lg span { display:inline-flex; align-items:center; gap:5px; }\n.pdot { display:inline-block; width:8px; height:8px; border-radius:2px; flex:none; }\n.empty { color:var(--dim); padding:26px 0; text-align:center; font-size:12.5px; }\n.foot { color:var(--mut); font-size:11px; margin-top:8px; line-height:1.6; }\n.spin { display:inline-block; width:11px; height:11px; border:2px solid var(--line);\n  border-top-color:var(--accent); border-radius:50%; animation:sp .7s linear infinite; }\n@keyframes sp { to { transform:rotate(360deg); } }\n.explain { display:grid; grid-template-columns:minmax(230px,1fr) minmax(260px,1.3fr) minmax(200px,1fr); gap:18px; }\n.explain code { background:var(--panel2); padding:1px 5px; border-radius:4px; font-size:11px; }\n.explain ul { margin:6px 0 0; padding-left:16px; }\n.explain li { margin:3px 0; font-size:11.5px; color:var(--dim); }\n.bandrow { display:flex; align-items:center; gap:8px; margin:4px 0; font-size:11.5px; color:var(--dim); }\n.bandrow .tag { min-width:96px; text-align:center; }\n.formula { font-size:15px; font-weight:600; text-align:center; padding:12px 0 6px; letter-spacing:-.01em; }\n.formula em { font-style:normal; color:var(--accent); }\n.printhead { display:none; }\n.onlyprint { display:none; }\n.alocrow { margin-top:10px; flex-wrap:wrap; align-items:flex-end; }\n.alocbox { display:inline-flex; flex-direction:column; align-items:center; gap:3px; padding:5px 9px;\n  background:var(--panel2); border:1px solid var(--line); border-radius:10px; }\n.alocmes { font-size:10px; text-transform:uppercase; letter-spacing:.04em; color:var(--dim); }\n.alocctl { display:flex; align-items:center; gap:5px; }\n.alocctl button { width:24px; height:24px; padding:0; font-size:15px; line-height:1; }\n.alocnum { width:54px; text-align:center; font-size:23px; font-weight:700; padding:1px 3px;\n  background:transparent; border:0; color:var(--txt); -moz-appearance:textfield; }\n.alocnum::-webkit-outer-spin-button, .alocnum::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }\n.alocusd { font-size:10px; color:var(--dim); }\n\n/* ---------- impressão / PDF ----------\n   window.print() com folha própria em vez de rasterizar com html2canvas: o texto sai selecionável,\n   os SVG saem vetoriais e não entra dependência de CDN. Três coisas precisam ser desfeitas aqui:\n   a altura fixa com scroll interno (senão só a primeira tela imprime), o tema escuro (queima tinta)\n   e os controles interativos (inúteis no papel — o cabeçalho de impressão diz o escopo em texto). */\n@media print {\n  .onlyprint { display:inline !important; color:#64748b; }\n  .wrap, .wrap.light {\n    height:auto !important; overflow:visible !important; padding:0 !important; font-size:9.5px;\n    --bg:#fff; --panel:#fff; --panel2:#f4f6fa; --line:#c9d3e4;\n    --txt:#0f172a; --dim:#475569; --mut:#64748b; --accent:#1d4ed8;\n  }\n  .noprint, .noprint * { display:none !important; }\n  .printhead { display:block !important; margin-bottom:12px; }\n  .printhead h1 { font-size:15px; }\n  /* margem 1px + entrelinha 1,3: a linha \"Sessão = …\" custava 17,5px e, com Diário + Mensal (alerta de sobreposição,\n     mês parcial, alocações) e a fonte DejaVu, empurrava o cartão explicativo inteiro para a página 2 */\n  .printhead .l { font-size:10px; color:var(--dim); margin-top:1px; line-height:1.3; }\n  .card { break-inside:avoid; page-break-inside:avoid; box-shadow:none; }\n  /* a tabela é mais alta que uma folha: exigir que ela não quebre faria o Chrome empurrá-la inteira\n     para a página seguinte e deixar meia folha em branco. Ela DEVE fluir entre páginas — o cabeçalho\n     se repete (table-header-group) e as linhas seguem indivisíveis. */\n  .cardTabela { break-inside:auto; page-break-inside:auto; }\n  .hero, .charts { break-inside:avoid; }\n  tr, .kpi, .bandrow { break-inside:avoid; page-break-inside:avoid; }\n  thead { display:table-header-group; }\n  .scroll { overflow:visible !important; }\n  table { font-size:8.5px; }\n  th, td { padding:3px 5px; }\n  .secthead { break-after:avoid; }\n  /* as cores dos chips foram escolhidas para fundo escuro; no papel virariam pastel ilegível */\n  .t-critico, .t-divergente { color:#b91c1c; }\n  .t-atencao { color:#a16207; }\n  .t-novo { color:#15803d; }\n  .t-subutilizado { color:#0369a1; }\n  .t-top { color:#1d4ed8; }\n  .t-esperado, .t-estavel, .t-sumiu { color:#475569; }\n  /* rótulos de SVG com opacidade de tela (.45/.5) davam ~3:1 no branco: no papel, no mínimo .8 */\n  svg text { opacity:.8 !important; }\n  /* piso, não teto: o nome TOP CUSTO do gráfico de elasticidade (.95) não pode cair para .8 */\n  svg text[opacity=\".95\"] { opacity:.95 !important; }\n  .good { color:#15803d; }\n  .bad { color:#b91c1c; }\n  .warnI { color:#a16207; }\n  /* a faixa de erro e os avisos imprimem: as cores de fundo escuro dariam ~2:1 de contraste */\n  .err { color:#b91c1c; background:#fff; border-color:#b91c1c; }\n  .note { color:#a16207; background:#fff; }\n  .hS { fill:#0369a1 !important; opacity:1 !important; } .hC { fill:#be185d !important; opacity:1 !important; }\n  /* marcas desenhadas como FUNDO (cores das legendas, barra de Pareto, número da seção): no padrão do Chrome\n     (\"Gráficos de plano de fundo\" desmarcado) elas sumiam do PDF. Só elas imprimem o fundo. */\n  .pdot, .nm .bar i, .secthead .n { -webkit-print-color-adjust:exact; print-color-adjust:exact; }\n  /* o cartão \"Como ler a elasticidade\" (~633px; ~697px com a fonte DejaVu) não cabia sob o cabeçalho e ia\n     inteiro para a página 2. 9,5px é o corpo do papel; a 10px o mensal com DejaVu ainda transbordava. */\n  .explain li, .bandrow, .explain .sub { font-size:9.5px; }\n  /* ⚠️ LARGURA — no A4 paisagem a área útil é ~1062px e a tabela pede 1466px na tela. Como a\n     impressão não rola, o excedente sairia CORTADO: some a coluna de impacto US$. Estas regras\n     comprimem nome, id, sparkline e a mini-barra de elasticidade; os NÚMEROS ficam intactos e\n     seguem sem quebrar linha. */\n  td.l, th.l { white-space:normal; }\n  .nm { min-width:0; gap:5px; }\n  .nm .bar { width:32px; height:15px; }\n  .nm .bar b { font-size:7px; }\n  td:nth-child(3) svg { max-width:76px; }\n  .stack svg { max-width:60px; }\n  .mono { font-size:7px; }\n  td.l .tag { font-size:6.5px; padding:1px 3px; }\n  .stack .s { font-size:7px; }\n  /* modo retidas: \"/sess. ret.\" não quebra linha (nowrap) e alargava cada coluna de período — a tabela passava da folha\n     e o Chrome encolhia o PDF INTEIRO. O rótulo fica; a unidade e o respiro das células encolhem só neste modo. */\n  .wrap.ret th, .wrap.ret td { padding:3px 3px; }\n  .wrap.ret .stack .un { font-size:6.5px; }\n  .wrap.ret td:not(.l) .tag { font-size:8px; padding:1px 4px; }\n  /* reais retidas: a unidade mais longa (\"·dia/sess. real ret.\") vai para a linha de baixo do número — o rótulo fica\n     inteiro e a coluna volta a caber na folha (sem isto o Chrome encolhia o PDF inteiro) */\n  .wrap.realret .stack .v { display:grid; grid-template-columns:1fr repeat(5, auto); align-items:baseline; }\n  .wrap.realret .stack .v .un { grid-row:2; grid-column:1 / -1; text-align:right; margin-left:0; }\n}\n`;\n\n// ---------- gráficos (SVG à mão) ----------\n// HERO: a leitura principal da tela. Barras = sessões (volume de tráfego), linha = US$ por sessão\n// (eficiência). Quando a linha sobe enquanto a barra desce, o custo descolou do tráfego.\nfunction chartHero(tot, rot) {\n  const R = rot || ROT.total;\n  // com sessões retidas carregadas, uma 3ª linha sob o eixo X traz a RETENÇÃO de cada período; o gráfico cresce\n  // só o necessário (a área de plotagem segue com 170, e sem retidas tudo fica onde estava)\n  const temR = tot.some((t) => t.sessRet !== null && t.sessRet !== undefined);\n  const W = 560, PL = 56, PR = 60, PT = 26, PB = temR ? 46 : 34, H = PT + 170 + PB;\n  const withS = tot.filter((t) => t.sessions !== null);\n  if (!withS.length) return '<div class=\"empty\">' + R.sess + ' não carregadas — sem elas não há custo por ' + R.sessao + '</div>';\n  const maxS = Math.max(1, ...tot.map((t) => t.sessions || 0));\n  const cpsV = tot.map((t) => t.cps).filter((x) => x !== null && Number.isFinite(x));\n  // sem nenhum US$/sessão acima de zero (só pares sem preço), eixo direito, linha e pontos não têm o que dizer\n  const temC = cpsV.some((x) => x > 0);\n  const maxC = Math.max(1e-9, ...cpsV), minC = Math.min(...cpsV, 0);\n  const padC = Math.max((maxC - minC) * 0.25, maxC * 0.15);\n  const cHi = maxC + padC, cLo = Math.max(0, minC - padC);\n  const step = (W - PL - PR) / Math.max(1, tot.length);\n  const cx = (i) => PL + step * i + step / 2;\n  const yS = (v) => H - PB - (v / maxS) * (H - PT - PB);\n  const yC = (v) => H - PB - ((v - cLo) / (cHi - cLo || 1)) * (H - PT - PB);\n  const bw = Math.min(74, step * 0.52);\n\n  const grid = [0, .5, 1].map((f) => {\n    const y = H - PB - f * (H - PT - PB);\n    return `<line x1=\"${PL}\" x2=\"${W - PR}\" y1=\"${y.toFixed(1)}\" y2=\"${y.toFixed(1)}\" stroke=\"currentColor\" stroke-opacity=\".1\"/>` +\n      `<text x=\"${PL - 7}\" y=\"${(y + 3.5).toFixed(1)}\" text-anchor=\"end\" font-size=\"9.5\" class=\"hS\" opacity=\".8\">${fmtNum(maxS * f)}</text>` +\n      (temC ? `<text x=\"${W - PR + 7}\" y=\"${(y + 3.5).toFixed(1)}\" font-size=\"9.5\" class=\"hC\" opacity=\".85\">${fmtTick(cLo + (cHi - cLo) * f, cHi)}</text>` : '');\n  }).join('');\n\n  const bars = tot.map((t, i) => {\n    if (t.sessions === null) return '';\n    const y = yS(t.sessions);\n    return `<rect x=\"${(cx(i) - bw / 2).toFixed(1)}\" y=\"${y.toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${(H - PB - y).toFixed(1)}\" rx=\"3\" fill=\"#38bdf8\" opacity=\".28\"/>` +\n      `<text x=\"${cx(i).toFixed(1)}\" y=\"${(y - 6).toFixed(1)}\" text-anchor=\"middle\" font-size=\"10.5\" class=\"hS\" font-weight=\"600\">${t.sessions.toLocaleString('pt-BR')}</text>`;\n  }).join('');\n\n  // um período sem US$/sessão QUEBRA a linha: atravessar o vão inventaria uma tendência não medida\n  const segsC = []; let segC = [];\n  tot.forEach((t, i) => {\n    if (t.cps === null || t.cps === undefined || !Number.isFinite(t.cps)) { if (segC.length) segsC.push(segC); segC = []; return; }\n    segC.push([cx(i), yC(t.cps)]);\n  });\n  if (segC.length) segsC.push(segC);\n  const line = !temC ? '' : segsC.filter((s) => s.length > 1).map((s) => `<path d=\"${s.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ')}\" fill=\"none\" stroke=\"#f472b6\" stroke-width=\"2.5\" stroke-linejoin=\"round\"/>`).join('');\n  const dots = !temC ? '' : tot.map((t, i) => t.cps === null ? '' :\n    `<circle cx=\"${cx(i).toFixed(1)}\" cy=\"${yC(t.cps).toFixed(1)}\" r=\"4\" fill=\"#f472b6\" stroke=\"var(--panel)\" stroke-width=\"2\"/>` +\n    `<text x=\"${cx(i).toFixed(1)}\" y=\"${(yC(t.cps) - 11).toFixed(1)}\" text-anchor=\"middle\" font-size=\"10.5\" class=\"hC\" font-weight=\"700\">${fmtCps(t.cps)}</text>`).join('');\n  const yX = H - PB;   // base do eixo X: os rótulos ficam abaixo dela (sem retidas, nos mesmos 214/226 de antes)\n  const labs = tot.map((t, i) =>\n    `<text x=\"${cx(i).toFixed(1)}\" y=\"${yX + 18}\" text-anchor=\"middle\" font-size=\"10.5\" fill=\"currentColor\" opacity=\".8\">${esc(t.per.lab)}</text>` +\n    `<text x=\"${cx(i).toFixed(1)}\" y=\"${yX + 30}\" text-anchor=\"middle\" font-size=\"9\" fill=\"currentColor\" opacity=\".45\">${esc(t.per.sub)}</text>` +\n    (temR ? `<text x=\"${cx(i).toFixed(1)}\" y=\"${yX + 42}\" text-anchor=\"middle\" font-size=\"9\" class=\"hS\" opacity=\".85\">${R.ra} ${fmtRet(retOf(t.sessTot, t.sessRet))}</text>` : '')).join('');\n\n  // role=\"img\" esconde os <text> de quem usa leitor de tela: a retenção vai no aria-label\n  const aria = R.sess + ' e custo por ' + R.sessao + (MOE.alt ? ' em ' + MOE.sim : '')\n    + (temR ? ' · ' + R.rl + ': ' + tot.map((t) => t.per.lab + ' ' + fmtRet(retOf(t.sessTot, t.sessRet))).join(', ') : '');\n  return `<svg viewBox=\"0 0 ${W} ${H}\" role=\"img\" aria-label=\"${esc(aria)}\">${grid}${bars}${line}${dots}${labs}</svg>\n    <div class=\"lg\"><span><i class=\"pdot\" style=\"background:#38bdf8\"></i>${R.sess} (eixo esq.)</span>\n    ${temC ? '<span><i class=\"pdot\" style=\"background:#f472b6\"></i>' + MOE.sim + ' por ' + R.sessao + ' (eixo dir.)</span>'\n      : '<span>sem ' + MOE.sim + ' por ' + R.sessao + ': ' + (!tot.some((t) => t.cost > 0) ? 'nenhum custo com preço nos períodos'\n        : !tot.some((t) => t.sessions > 0) ? R.sess + ' zeradas ou ausentes nos períodos'\n        : 'nenhum período tem custo com preço e ' + R.sess + ' ao mesmo tempo') + '</span>'}${temR ? '<span>' + R.leg + '</span>' : ''}</div>`;\n}\n\n// Sparkline da coluna \"tendência\". Duas regras inegociáveis, porque foi a violação delas que fez o\n// desenho contradizer os números:\n//  1. plota EXATAMENTE o array impresso nas células da mesma linha — mesma grandeza, mesma agregação;\n//  2. eixo de 0 ao máximo da série, então a altura É o valor e a amplitude desenhada É a variação real.\n//     (Normalizar pelo min–max fazia uma oscilação de 4% ocupar a caixa inteira, igual a um +900%.)\nfunction sparkline(vals, color, opts) {\n  const o = opts || {};\n  const W = o.w || 108, H = 22, P = 2;\n  const ok = vals.map((v) => (v === null || v === undefined || !Number.isFinite(v) ? null : v));\n  const real = ok.filter((v) => v !== null);\n  if (real.length < 2) return `<svg viewBox=\"0 0 ${W} ${H}\"></svg>`;\n  // série de referência: o volume que o TRÁFEGO sozinho explicaria (sessões reescaladas para partir do\n  // mesmo ponto). A distância entre a linha cheia e a pontilhada É o consumo por sessão: acima do\n  // pontilhado subiu por sessão, abaixo caiu. Antes era uma reta no valor inicial, que não dizia nada.\n  const refS = (o.ref || []).map((v) => (v === null || v === undefined || !Number.isFinite(v) ? null : v));\n  const refReal = refS.filter((v) => v !== null);\n  const hi = Math.max.apply(null, real.concat(refReal));\n  if (!(hi > 0)) return `<svg viewBox=\"0 0 ${W} ${H}\"></svg>`;   // série toda zero: nada a desenhar\n  const n = ok.length;\n  const x = (i) => P + (n === 1 ? 0 : i * (W - 2 * P) / (n - 1));\n  const y = (v) => H - P - (v / hi) * (H - 2 * P);\n\n  // segmentos: um buraco NÃO vira reta atravessando o vão (isso inventava uma tendência que não existe)\n  const segs = []; let cur = [];\n  ok.forEach((v, i) => {\n    if (v === null) { if (cur.length) segs.push(cur); cur = []; return; }\n    cur.push([x(i), y(v)]);\n  });\n  if (cur.length) segs.push(cur);\n  const dOf = (s) => s.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');\n  const linhas = segs.filter((s) => s.length > 1);\n  const area = linhas.map((s) => `<path d=\"${dOf(s)}L ${s[s.length - 1][0].toFixed(1)} ${H - P} L ${s[0][0].toFixed(1)} ${H - P} Z\" fill=\"${color}\" opacity=\".14\"/>`).join('');\n  const traco = linhas.map((s) => `<path d=\"${dOf(s)}\" fill=\"none\" stroke=\"${color}\" stroke-width=\"1.6\"/>`).join('');\n  const soltos = segs.filter((s) => s.length === 1).map((s) => `<circle cx=\"${s[0][0].toFixed(1)}\" cy=\"${s[0][1].toFixed(1)}\" r=\"1.6\" fill=\"${color}\" opacity=\".7\"/>`).join('');\n  let ref;\n  if (refReal.length >= 2) {\n    // a pontilhada também quebra no vão, como a linha cheia\n    const segR = []; let cr = [];\n    refS.forEach((v, i) => { if (v === null) { if (cr.length) segR.push(cr); cr = []; return; } cr.push([x(i), y(v)]); });\n    if (cr.length) segR.push(cr);\n    ref = segR.filter((sg) => sg.length > 1).map((sg) => `<path d=\"${dOf(sg)}\" fill=\"none\" stroke=\"currentColor\" stroke-opacity=\".45\" stroke-width=\"1.2\" stroke-dasharray=\"2.5 2\"/>`).join('');\n  } else {\n    // sem sessões carregadas não dá para traçar o tráfego: cai para a marca do valor inicial\n    ref = `<line x1=\"${P}\" x2=\"${W - P}\" y1=\"${y(real[0]).toFixed(1)}\" y2=\"${y(real[0]).toFixed(1)}\" stroke=\"currentColor\" stroke-opacity=\".28\" stroke-dasharray=\"2 2\"/>`;\n  }\n  const ult = segs.length ? segs[segs.length - 1][segs[segs.length - 1].length - 1] : null;\n  const dot = ult ? `<circle cx=\"${ult[0].toFixed(1)}\" cy=\"${ult[1].toFixed(1)}\" r=\"2.2\" fill=\"${color}\"/>` : '';\n  const tip = o.tip ? `<title>${esc(o.tip)}</title>` : '';\n  return `<svg viewBox=\"0 0 ${W} ${H}\" aria-hidden=\"true\">${tip}${area}${ref}${traco}${soltos}${dot}</svg>`;\n}\n\n// barra divergente de elasticidade DENTRO da célula — é daqui que a leitura de elasticidade nasce\nfunction miniElast(cl) {\n  const W = 88, H = 18, mid = W / 2, half = W / 2 - 2;\n  if (!cl) return '';\n  const col = (cl.cls === 'critico' || cl.cls === 'divergente') ? '#ef4444'\n    : cl.cls === 'atencao' ? '#f59e0b' : cl.cls === 'subutilizado' ? '#38bdf8'\n    : cl.cls === 'novo' ? '#22c55e' : '#64748b';\n  if (cl.e === null || !Number.isFinite(cl.e)) {\n    return `<svg viewBox=\"0 0 ${W} ${H}\" aria-hidden=\"true\"><line x1=\"${mid}\" x2=\"${mid}\" y1=\"1\" y2=\"${H - 1}\" stroke=\"currentColor\" stroke-opacity=\".3\"/></svg>`;\n  }\n  const e = Math.max(-4, Math.min(4, cl.e));\n  const w = Math.max(1.5, Math.abs(e) / 4 * half);\n  const x0 = e >= 0 ? mid : mid - w;\n  return `<svg viewBox=\"0 0 ${W} ${H}\" aria-hidden=\"true\">\n    <line x1=\"${mid}\" x2=\"${mid}\" y1=\"1\" y2=\"${H - 1}\" stroke=\"currentColor\" stroke-opacity=\".3\"/>\n    <rect x=\"${x0.toFixed(1)}\" y=\"4\" width=\"${w.toFixed(1)}\" height=\"${H - 8}\" rx=\"2\" fill=\"${col}\" opacity=\".9\"/></svg>`;\n}\n\nfunction chartStacked(store, tot) {\n  const W = 520, H = 210, PL = 54, PR = 12, PT = 14, PB = 32;\n  const pers = tot.map((t) => t.per);\n  const per = tot.map((t) => ({ per: t.per, acc: costByProduct(store, t.per, pers) }));\n  // a escala precisa caber PESSOAS; piso ínfimo (era US$ 1, que achatava projeto pequeno)\n  const max = Math.max(1e-9, ...tot.map((t) => t.total || t.cost || 0));\n  if (max <= 1e-9) return '<div class=\"empty\">sem custo carregado nos períodos (só pares sem preço ou volume zero)</div>';\n  const step = (W - PL - PR) / Math.max(1, per.length);\n  const bw = Math.min(70, step * 0.6);\n  const bars = per.map((d, i) => {\n    let yc = H - PB; const cx = PL + step * i + step / 2;\n    const segs = PROD_ORDER.filter((p) => d.acc[p]).map((p) => {\n      const h = (d.acc[p] / max) * (H - PT - PB); yc -= h;\n      return `<rect x=\"${(cx - bw / 2).toFixed(1)}\" y=\"${yc.toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${Math.max(0.6, h).toFixed(1)}\" fill=\"${PROD_COLOR[p]}\" opacity=\".92\"><title>${p}: ${fmtUsd(d.acc[p])}</title></rect>`;\n    }).join('');\n    return segs + `<text x=\"${cx.toFixed(1)}\" y=\"${H - 10}\" text-anchor=\"middle\" font-size=\"10\" fill=\"currentColor\" opacity=\".7\">${esc(d.per.lab)}</text>`;\n  }).join('');\n  const ax = [0, .5, 1].map((f) => {\n    const y = H - PB - f * (H - PT - PB);\n    return `<line x1=\"${PL}\" x2=\"${W - PR}\" y1=\"${y.toFixed(1)}\" y2=\"${y.toFixed(1)}\" stroke=\"currentColor\" stroke-opacity=\".1\"/>` +\n      `<text x=\"${PL - 6}\" y=\"${(y + 3.5).toFixed(1)}\" text-anchor=\"end\" font-size=\"9.5\" fill=\"currentColor\" opacity=\".5\">${fmtDin(conv(max * f), true)}</text>`;\n  }).join('');\n  const used = PROD_ORDER.filter((p) => per.some((d) => d.acc[p]));\n  return `<svg viewBox=\"0 0 ${W} ${H}\" role=\"img\" aria-label=\"custo por produto${MOE.alt ? ' em ' + MOE.sim : ''}\">${ax}${bars}</svg>\n    <div class=\"lg\">${used.map((p) => `<span><i class=\"pdot\" style=\"background:${PROD_COLOR[p]}\"></i>${p}</span>`).join('')}</div>`;\n}\n\n// Projeção da TABELA: mesma ordenação, mesmo filtro. Deixa de ser um ranking próprio.\nfunction chartElasticity(rows, rot) {\n  const list = rows.filter((r) => r.worst && r.worst.e !== null && Number.isFinite(r.worst.e)).slice(0, 14);\n  if (!list.length) return '<div class=\"empty\">nenhuma linha com elasticidade calculável — ' + (rot || ROT.total).sess + ' estáveis ou só um período</div>';\n  const rowH = 21, W = 520, PL = 178, PR = 50, H = list.length * rowH + 26;\n  const mid = PL + (W - PL - PR) / 2, half = (W - PL - PR) / 2;\n  const maxW = Math.max(1e-9, ...list.map((r) => r.maxCost));\n  const bars = list.map((r, i) => {\n    const yc = 16 + i * rowH;\n    const el = Math.max(-4, Math.min(4, r.worst.e));\n    const w = Math.max(1.5, Math.abs(el) / 4 * half);\n    const th = Math.max(5, 15 * Math.sqrt(r.maxCost / maxW));\n    const col = (r.worst.cls === 'critico' || r.worst.cls === 'divergente') ? '#ef4444'\n      : r.worst.cls === 'atencao' ? '#f59e0b' : r.worst.cls === 'subutilizado' ? '#38bdf8' : '#64748b';\n    return `<text x=\"${PL - 8}\" y=\"${yc + 4}\" text-anchor=\"end\" font-size=\"10\" fill=\"currentColor\" opacity=\"${r.top ? '.95' : '.6'}\" font-weight=\"${r.top ? '600' : '400'}\">${esc((r.p.toLowerCase() + '/' + r.e).slice(0, 30))}</text>` +\n      `<rect x=\"${(el >= 0 ? mid : mid - w).toFixed(1)}\" y=\"${(yc - th / 2).toFixed(1)}\" width=\"${w.toFixed(1)}\" height=\"${th.toFixed(1)}\" rx=\"2\" fill=\"${col}\" opacity=\".85\"><title>E=${fx(r.worst.e, 2)} · custo ${fmtUsd(r.maxCost)} · impacto ${fmtUsd(r.impact)}</title></rect>` +\n      `<text x=\"${W - PR + 5}\" y=\"${yc + 4}\" font-size=\"10\" fill=\"currentColor\" opacity=\".7\">${fx(r.worst.e, 1)}</text>`;\n  }).join('');\n  const ticks = [-4, -2, 0, 2, 4].map((t) => {\n    const xx = mid + t / 4 * half;\n    return `<line x1=\"${xx.toFixed(1)}\" x2=\"${xx.toFixed(1)}\" y1=\"6\" y2=\"${H - 14}\" stroke=\"currentColor\" stroke-opacity=\"${t === 0 ? '.35' : '.1'}\"/>` +\n      `<text x=\"${xx.toFixed(1)}\" y=\"${H - 3}\" text-anchor=\"middle\" font-size=\"9\" fill=\"currentColor\" opacity=\".5\">${t}</text>`;\n  }).join('');\n  return `<svg viewBox=\"0 0 ${W} ${H}\" role=\"img\" aria-label=\"elasticidade por evento\">${ticks}${bars}</svg>\n    <div class=\"foot\">Comprimento = elasticidade (limitada a ±4). <b>Espessura = peso no custo.</b> Segue a ordenação da tabela.</div>`;\n}\n\n// faixa de sessões por dia — só na visão semanal, onde os 14 a 28 dias (7 × nº de janelas) existem\nfunction dailyStrip(days, store) {\n  const daySess = store.daySess || {}, R = store.rot || ROT.total;\n  // com retidas carregadas, a dica de cada dia traz as duas contagens e a retenção (as barras seguem o divisor)\n  const dTot = store.daySessTot || {}, dRet = store.daySessRet || {};\n  const temR = days.some((d) => dRet[d] !== undefined && dRet[d] !== null);\n  const conta = (v) => (v === undefined || v === null ? '—' : v.toLocaleString('pt-BR'));\n  const dica = (d, v) => (!temR ? v.toLocaleString('pt-BR')\n    : conta(dTot[d]) + ' ' + (MOE.alt && R.real ? 'sessões ' : '') + R.ct + ' · ' + conta(dRet[d]) + ' ' + (MOE.alt && R.real ? 'sessões ' : '') + R.cr + ' (' + fmtRet(retOf(dTot[d], dRet[d])) + ')');\n  const vals = days.map((d) => (daySess[d] === undefined ? null : daySess[d]));\n  const real = vals.filter((v) => v !== null);\n  if (real.length < 2) return '';\n  const W = 560, H = 62, PB = 14, max = Math.max(1, ...real);\n  const bw = (W / days.length) * 0.7, step = W / days.length;\n  const bars = days.map((d, i) => {\n    if (vals[i] === null) return '';\n    const h = (vals[i] / max) * (H - PB - 4);\n    const wk = Math.floor(i / WEEK_LEN);\n    return `<rect x=\"${(step * i + (step - bw) / 2).toFixed(1)}\" y=\"${(H - PB - h).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${Math.max(1, h).toFixed(1)}\" rx=\"1.5\" fill=\"#38bdf8\" opacity=\"${0.35 + wk * 0.25}\"><title>${d} (${dowPt(d)}): ${dica(d, vals[i])}</title></rect>`;\n  }).join('');\n  const labs = days.map((d, i) => (i % WEEK_LEN === 0\n    ? `<text x=\"${(step * i).toFixed(1)}\" y=\"${H - 2}\" font-size=\"9\" fill=\"currentColor\" opacity=\".55\">${shortDay(d)}</text>` : '')).join('');\n  return `<svg viewBox=\"0 0 ${W} ${H}\" role=\"img\" aria-label=\"${R.sess} por dia\">${bars}${labs}</svg>\n    <div class=\"foot\">${R.Sess} dia a dia nas ${Math.round(days.length / WEEK_LEN)} semanas — a mais recente é a mais opaca. Revela a sazonalidade que a média semanal esconde.</div>`;\n}\n\n// ---------- render: painel explicativo ----------\nfunction renderExplain() {\n  const bands = BANDS.map((b) => `<div class=\"bandrow\"><span class=\"tag t-${b.cls}\">${esc(b.lab)}</span><span>${esc(b.txt)}</span></div>`).join('');\n  return `<div class=\"card mt\">\n    <h2>Como ler a elasticidade</h2>\n    <div class=\"explain\">\n      <div>\n        <div class=\"formula\"><em>E</em> = Δ% do recurso ÷ Δ% das sessões</div>\n        <div class=\"sub\">Sempre contra o <b>primeiro período</b> da lista. A pergunta que ela responde:\n          <i>o consumo se moveu na mesma proporção do tráfego, ou descolou?</i>\n          <b>|E| = 1</b> é acompanhar o tráfego; <b>|E| = 3</b>, mover-se três vezes mais rápido que ele.<br>\n          <b>|E| dá só a magnitude.</b> A direção vem da seta, que é o sinal da variação do\n          <b>consumo por sessão</b>: ↑ subiu (ficou mais caro por sessão), ↓ caiu (ficou mais eficiente).\n          Isso importa porque E é uma divisão: quando as <b>sessões caem</b>, o divisor é negativo e um\n          E alto passa a significar que o recurso caiu <i>mais</i> que o tráfego — ou seja, melhorou.</div>\n        <h3 style=\"margin-top:9px\">A coluna impacto ${MOE.sim}</h3>\n        <ul>\n          <li>Maior variação de custo contra a base, na escala da base. Com períodos de tamanhos diferentes\n            (mês parcial, meses de 28 a 31 dias), o <b>fluxo</b> compara no mesmo ritmo diário.</li>\n          <li>O <b>storage</b> (preço por GiB-mês) compara sempre o <b>nível médio de GiB</b>, precificado nos dias\n            da base: entre dias ou semanas de meses de tamanhos diferentes (fevereiro, 30 ou 31 dias), o impacto\n            dele pode diferir da diferença das células de custo — e até ter o sinal oposto.</li>\n        </ul>\n        <h3 style=\"margin-top:9px\">Storage por sessão</h3>\n        <ul>\n          <li>No <b>storage</b> o consumo por sessão é em <b>GiB·dia</b> (o nível médio × os dias do período), como a\n            fatura cobra: um mês parcial ou de 28 dias não infla o por-sessão. A elasticidade do storage também\n            compara GiB·dia, e a pontilhada da tendência segue as sessões por dia.</li>\n        </ul>\n      </div>\n      <div>${bands}\n        <div class=\"foot\" style=\"margin-top:9px\">A cor da barra na coluna <b>elasticidade</b> segue estas faixas,\n          e a <b>espessura</b> dela no gráfico é o peso no custo — barra vermelha e grossa é o que dói.</div>\n      </div>\n      <div>\n        <h3>Quando E não vale</h3>\n        <ul>\n          <li><b>Sessões paradas</b> (variação abaixo de 2%): o divisor é quase zero e E explodiria — a linha\n            passa a ser classificada pela variação do <b>consumo por sessão</b>, e o rótulo mostra isso.</li>\n          <li><b>Base zero</b>: não há divisão possível — vira <span class=\"tag t-novo\">NOVO</span> ou\n            <span class=\"tag t-sumiu\">SUMIU</span>.</li>\n          <li><b>Direções opostas</b>: <span class=\"tag t-divergente\">DIVERGENTE</span> — consumo subiu com tráfego\n            caindo (ou o contrário). É o sinal mais forte de que algo mudou no comportamento, não na demanda.</li>\n        </ul>\n        <h3 style=\"margin-top:9px\">A coluna tendência</h3>\n        <ul>\n          <li>A <b>linha cheia</b> é o volume do recurso. A <b>pontilhada</b> é o volume que o\n            <b>tráfego sozinho explicaria</b> — as sessões reescaladas para partir do mesmo ponto.</li>\n          <li>A distância entre as duas <b>é</b> o consumo por sessão: linha cheia <b>acima</b> da pontilhada\n            significa que subiu por sessão; <b>abaixo</b>, que caiu. Coladas = acompanhou o tráfego.</li>\n        </ul>\n        <h3 style=\"margin-top:9px\">Símbolos</h3>\n        <ul>\n          <li><span class=\"warnI\">≈</span> o nível 1 dessa métrica é <b>média por recurso</b>, não total\n            (hoje vale para <code>login</code>; desde 02/10/26 o <code>storage</code> vem total) — só o detalhe fecha.</li>\n          <li><span class=\"warnI\">Δ</span> o detalhe diverge do nível 1; o detalhe é a verdade.</li>\n          <li><span class=\"warnI\">∅</span> o detalhe veio vazio com o nível 1 acima de zero — vale o nível 1 (lista incompleta).</li>\n          <li><span class=\"warnI\">⋯</span> lista de recursos truncada no teto de páginas — o total exibido\n            <b>continua sendo o do nível 1</b>, que é completo.</li>\n          <li><span class=\"warnI\">⚠</span> nem o nível 1 nem o detalhe fecham esse valor.</li>\n        </ul>\n      </div>\n    </div>\n  </div>`;\n}\n\n// ---------- render: tabela produto/evento ----------\nfunction flagsOf(c) {\n  return (c.mean ? '<span class=\"warnI\" title=\"nível 1 é MÉDIA por recurso, não total — abra o detalhe\">≈</span>' : '')\n    + (c.truncated ? '<span class=\"warnI\" title=\"lista de recursos truncada; o total segue vindo do nível 1\">⋯</span>' : '')\n    + (c.diverges ? '<span class=\"warnI\" title=\"detalhe diverge do nível 1 — o detalhe é a verdade\">Δ</span>' : '')\n    + (c.noClose ? '<span class=\"warnI\" title=\"nível 1 é média e o detalhe truncou: este valor não fecha\">⚠</span>' : '')\n    + (c.vazio ? '<span class=\"warnI\" title=\"o detalhe veio vazio, mas o nível 1 tem consumo — vale o nível 1\">∅</span>' : '');\n}\n\n// A tendência desenha os MESMOS números das colunas; o tooltip declara isso para não sobrar dúvida.\nfunction sparkOpts(periods, vals, unidade) {\n  return { tip: periods.map((p, i) => p.lab + ': ' + fmtVal(vals[i], unidade)).join('  →  ') };\n}\n\nfunction renderTable(st, scope, periods, store, rows, nTodas) {\n  const open = st[K(scope, 'Open')] || {};\n  const R = store.rot || ROT.total;\n  const head = `<tr>\n      <th class=\"l\">produto / evento${R.alt ? '<br><span style=\"font-weight:400;text-transform:none\">' + R.un + ' = por ' + R.sessao + '</span>' : ''}</th><th class=\"l\">id</th><th>tendência<br><span style=\"font-weight:400;text-transform:none\">${R.tend}</span></th>\n      ${periods.map((p, i) => `<th>${esc(p.lab)}${i === 0 ? ' · base' : ''}<br><span style=\"font-weight:400;text-transform:none\">${esc(p.sub)}</span></th>`).join('')}\n      <th>elasticidade</th><th title=\"maior variação contra a base, na escala da base. Fluxo: a diferença de custo; com períodos de tamanhos diferentes (mês parcial, meses de 28 a 31 dias), no mesmo ritmo diário. Storage (GiB-mês), em todas as seções: a variação do nível médio de GiB precificada nos dias da base — entre dias ou semanas de meses de tamanhos diferentes (fevereiro, 30 ou 31 dias) ele pode diferir da diferença das células de custo, e até ter o sinal oposto\">impacto ${MOE.sim}</th></tr>`;\n  const body = rows.map((r) => {\n    const k = key(r.p, r.e);\n    const isOpen = !!open[k];\n    const col = PROD_COLOR[r.p] || '#64748b';\n    const shPct = (r.share * 100);\n    // O consumo POR SESSÃO é a leitura principal desta tabela — volume sozinho só acompanha o tráfego.\n    // Por isso ele vem grande; volume e US$ ficam de apoio na linha de baixo.\n    const cells = r.cells.map((c) => `<td><div class=\"stack\">\n        <span class=\"v\">${c.perS === null ? '—' : fmtPerS(c.perS, r.m.u)}<span class=\"un\">${r.m.k === 'stock' ? R.unE : R.un}</span>${flagsOf(c)}</span>\n        <span class=\"s\">${c.v === null ? '—' : fmtVal(c.v, r.m.u)}${c.c === null ? '' : ' · ' + fmtUsd(c.c)}</span>\n      </div></td>`).join('');\n    const w = r.worst;\n    const tag = w ? `<span class=\"tag t-${w.cls}\" title=\"${esc(w.why || '')}\">${esc(w.tag)}</span>` : '<span class=\"tag t-zero\">—</span>';\n    const main = `<tr class=\"grp ${r.top ? 'istop' : ''}\" data-act=\"toggle\" data-scope=\"${scope}\" data-k=\"${esc(k)}\">\n      <td class=\"l\"><div class=\"nm\">\n        <span class=\"bar\" title=\"${fx(shPct, 1)}% da soma dos maiores custos de cada linha (Pareto)\"><i style=\"width:${Math.min(100, shPct).toFixed(1)}%;background:${col}\"></i><b>${shPct >= 0.5 ? fx(shPct, 0) + '%' : ''}</b></span>\n        <span><span class=\"mono\">${isOpen ? '▾' : '▸'}</span> <span class=\"ev\"${r.m.n ? ` title=\"${esc(r.m.n)}\"` : ''}>${esc(r.e)}</span>\n          <span class=\"sub\">${esc(r.p.toLowerCase())} · ${esc(r.m.u || '')}</span>\n          ${r.top ? '<span class=\"tag t-top\" title=\"está entre os que formam 80% do custo estimado\">TOP CUSTO</span>' : ''}\n          ${(r.m.p === null || r.m.p === undefined) ? `<span class=\"tag t-zero\" title=\"${esc(r.m.n || 'sem preço na fatura de ago/26')}\">fora deste total</span>` : ''}\n          ${r.m.neg ? `<span class=\"tag t-top\" title=\"${esc(r.m.n || '')}\">preço acordado</span>` : ''}\n        </span></div></td>\n      <td class=\"l\"><span class=\"mono\" title=\"tipo de identificador dos recursos deste evento — é o que aparece na coluna id ao expandir\">${esc(r.m.dim || '—')}</span></td>\n      <td>${sparkline(r.spark, col, Object.assign(sparkOpts(periods, r.spark, r.m.u), { ref: r.refSpark }))}</td>\n      ${cells}\n      <td><div class=\"stack\">${miniElast(w)}<span class=\"s\">${tag}</span></div></td>\n      <td>${r.impact ? `<span class=\"${r.impact > 0 ? 'bad' : 'good'}\">${fmtUsd(r.impact)}</span>` : '—'}</td></tr>`;\n    return isOpen ? main + renderDetail(st, scope, periods, store, r) : main;\n  }).join('');\n  return `<div class=\"card mt cardTabela\"><div class=\"scroll\"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>\n    ${rows.length ? '' : `<div class=\"empty\">${nTodas ? 'nada para mostrar com o filtro atual' : 'nenhum consumo carregado nos períodos'}</div>`}</div>`;\n}\n\nfunction renderDetail(st, scope, periods, store, r) {\n  const cols = periods.length + 5;   // nome | id | tendência | ...períodos... | elasticidade | impacto\n  // par sem quebra por recurso na API (TABLE/storage: o nível 3 só traz a média por system)\n  if (r.m.nodrill) {\n    return `<tr class=\"det\"><td class=\"l\" colspan=\"${cols}\"><span class=\"warnI\">${esc(r.m.n || 'a API não detalha este par por recurso')}</span></td></tr>`;\n  }\n  const dets = periods.map((per) => store.det[per.k + '|' + r.p + '|' + r.e]);\n  // ⚠️ `|| x.__loading` é o que impede o crash: drill() escreve {__loading:true} em TODOS os períodos\n  // antes de buscar, então a guarda por ausência sozinha deixava passar, aggDetail devolvia null e o\n  // forEach abaixo estourava — derrubando a tela inteira (e o estado do par aberto persistia, então\n  // recarregar reproduzia). Com isso o ramo `busy` finalmente é alcançável.\n  // período sem nível 1 (overview falhou ou ainda não carregado): o detalhe sozinho não fecha com o KPI\n  const semN1 = periods.filter((per) => !(store.agg[per.k] && Object.keys(store.agg[per.k]).length));\n  if (dets.some((x) => !x || x.__loading)) {\n    const busy = dets.some((x) => x && x.__loading);\n    // o drill só busca o que falta, falhou ou veio vazio (∅): o botão conta o mesmo (período completo não paga de novo)\n    const nFalta = periods.filter((per, i) => !dets[i] || dets[i].__err\n      || (!dets[i].__loading && valueOf(store, per, r.p, r.e).vazio)).length;\n    return `<tr class=\"det\"><td class=\"l\" colspan=\"${cols}\">\n      ${busy ? '<span class=\"noprint\"><span class=\"spin\"></span> buscando o detalhe por recurso…</span><span class=\"onlyprint\">detalhe por recurso ainda carregando — não incluído</span>'\n             : semN1.length && SCOPES[scope].drill\n             ? `<span class=\"warnI\">detalhe por recurso indisponível: ${semN1.length === 1 ? 'o período' : 'os períodos'} ${esc(semN1.map((q) => q.lab).join(', '))} ${semN1.length === 1 ? 'está' : 'estão'} sem nível 1 — <span class=\"noprint\">clique em <b>Carregar</b> antes de detalhar</span><span class=\"onlyprint\">não carregado</span>.</span>`\n             : !SCOPES[scope].drill\n             // No mensal o detalhe por recurso não é oferecido: UM evento pode passar de 16 páginas\n             // por mês (~40 s cada). A seção Semanal responde a mesma pergunta por uma fração disso.\n             ? '<span class=\"warnI\">detalhe por recurso não é carregado no mensal — um único evento passa de '\n               + '16 páginas por mês. Para destrinchar por recurso, use a seção <b>Semanal</b>.</span>'\n             : `<button class=\"noprint\" data-act=\"drill\" data-scope=\"${scope}\" data-p=\"${esc(r.p)}\" data-e=\"${esc(r.e)}\">carregar detalhe por recurso (${nFalta < periods.length ? nFalta + ' de ' : ''}${periods.length} período${periods.length > 1 ? 's' : ''} · até ${SCOPES[scope].maxPages} páginas cada · ~5–25 s por página)</button>` +\n             // Agora que só o par mais caro vem pronto, a maioria das linhas expandidas mostra este botão.\n             // Botão no papel não significa nada: no PDF vira uma frase dizendo que o detalhe não foi buscado.\n             '<span class=\"onlyprint\">detalhe por recurso não carregado</span>'}\n    </td></tr>`;\n  }\n  const errs = dets.filter((x) => x.__err);\n  if (errs.length) return `<tr class=\"det\"><td class=\"l\" colspan=\"${cols}\"><span class=\"bad\">falhou: ${esc(errs[0].__err)}</span>\n    ${SCOPES[scope].drill ? ` <button class=\"noprint\" data-act=\"drill\" data-scope=\"${scope}\" data-p=\"${esc(r.p)}\" data-e=\"${esc(r.e)}\">tentar de novo</button>` : ''}</td></tr>`;\n  // detalhe \"completo\" que veio VAZIO com o nível 1 > 0 (∅ na linha-mãe): a lista é incompleta, não zeros —\n  // lida como zeros, cada recurso sairia SUMIU/NOVO com o custo inteiro de impacto, contradizendo a linha-mãe\n  const vaz = periods.filter((per) => valueOf(store, per, r.p, r.e).vazio);\n  if (vaz.length) return `<tr class=\"det\"><td class=\"l\" colspan=\"${cols}\"><span class=\"warnI\">∅ o detalhe por recurso de ${esc(vaz.map((q) => q.lab).join(', '))} veio vazio com o nível 1 acima de zero — lista incompleta: os recursos não são listados e a linha usa o nível 1.</span>\n    ${SCOPES[scope].drill ? ` <button class=\"noprint\" data-act=\"drill\" data-scope=\"${scope}\" data-p=\"${esc(r.p)}\" data-e=\"${esc(r.e)}\">tentar de novo</button>` : ''}</td></tr>`;\n  const dim = dets[0].dim || meta(r.p, r.e).dim || '?';\n  const maps = dets.map((d, i) => aggDetail(d, r.p, r.e, periods[i].days.length));\n  // Cobertura real: quando a paginação trunca, a lista cobre só os primeiros dias do período —\n  // os valores do drill-down são de janela PARCIAL e não podem ser lidos como o período inteiro.\n  const covers = dets.map((d) => (d.cover ? d.cover.length : periods[0].days.length));\n  const coberturaDesigual = new Set(covers).size > 1;\n  // truncou antes de fechar UM dia sequer: não há valor honesto a mostrar aqui\n  if (covers.some((n) => n === 0)) {\n    return `<tr class=\"det\"><td class=\"l\" colspan=\"${cols}\"><span class=\"warnI\">⚠ a lista de recursos truncou\n      antes de completar um único dia do período — qualquer soma aqui seria de janela parcial. O total da\n      linha-pai continua vindo do nível 1, que é completo.</span></td></tr>`;\n  }\n  const refs = new Set(); maps.forEach((m) => m.forEach((_, kk) => refs.add(kk)));\n  // ⚠ O denominador tem de vir do MESMO recorte que o numerador. Quando a lista trunca, os valores\n  // aqui somam só os dias cobertos; dividir isso pelas sessões do período inteiro subestima o consumo\n  // por sessão na proporção dos dias que faltam — medido em TABLE/read semanal: 2 dias de 7 fazia\n  // 38.079/sess aparecer como 13.199/sess. daySess (só existe no semanal) é indexado por data, igual\n  // ao cover; sem a sessão de algum dia coberto, o por-sessão fica SEM DADO (nunca o período inteiro).\n  const estoque = isStock(r.p, r.e);\n  const R = store.rot || ROT.total;\n  const sessOf = (i) => {\n    const d = dets[i], full = nz(store.sess[periods[i].k]);\n    // estoque é MÉDIA dos dias cobertos e não encolhe com a cobertura: divide pelas sessões do período\n    // inteiro (pelas dos dias cobertos inflaria o por-sessão em dias/cobertos)\n    if (estoque || !d || !d.cover) return full;\n    let s = 0;\n    for (const dia of d.cover) {\n      const v = (store.daySess || {})[dia];\n      // dia coberto sem sessão própria: misturar recortes (numerador de N dias ÷ denominador de 7)\n      // subestimava o por-sessão em silêncio\n      if (v === undefined || v === null) return null;\n      s += v;\n    }\n    return s > 0 ? s : null;\n  };\n  // impacto na MESMA base de dias: truncado, cada soma cobre só os dias cobertos — sem escalar para o\n  // período inteiro, o impacto seria quase todo diferença de cobertura (só fluxo; estoque já é média)\n  const escala = (i) => ((!estoque && dets[i].cover && dets[i].cover.length) ? periods[i].days.length / dets[i].cover.length : 1);\n  const sBase = sessOf(0);\n  const sLast = sessOf(periods.length - 1);\n  const fE = (i) => (estoque ? periods[i].days.length : 1);   // estoque por sessão em GiB·dia\n  const list = [...refs].map((ref) => {\n    const vals = maps.map((m) => (m.get(ref) === undefined ? 0 : m.get(ref)));\n    // estoque em GiB·dia, como na linha-mãe (nível médio dos dias cobertos × dias do período)\n    const vEf = vals.map((v, i) => v * fE(i));\n    const cl = classify(vEf[0], vEf[vEf.length - 1], sBase, sLast, SCOPES[scope].prog ? {\n      res: vEf,\n      sess: periods.map((_, i) => sessOf(i)),\n      per: vEf.map((v, i) => { const s = sessOf(i); return s ? v / s : null; })\n    } : undefined, store.rot);\n    const il = vals.length - 1;\n    const cb = costOf(r.p, r.e, vals[0] * escala(0), periods[0].d1, periods[0].days) || 0;\n    // estoque: o nível do último período precificado nos dias da base (a mesma regra da linha-mãe)\n    const pl = estoque ? periods[0] : periods[il];\n    const cl2 = costOf(r.p, r.e, vals[il] * escala(il), pl.d1, pl.days) || 0;\n    let imp = cl2 - cb;\n    if (Math.abs(imp) <= 1e-9 * Math.max(Math.abs(cb), Math.abs(cl2))) imp = 0;   // resíduo, não impacto\n    // mesma referência de tráfego da linha-mãe, ancorada no período-base deste recurso\n    const refTraf = vals.map((_, i) => {\n      const s = sessOf(i);\n      return (sBase && s) ? vals[0] * (s / sBase) * (fE(0) / fE(i)) : null;\n    });\n    return { ref, vals, cl, imp, cb, spark: vals, refSerie: refTraf };\n  }).sort((a, b) => (st.sort === 'elasticidade'\n      ? Math.abs(b.cl.e || 0) - Math.abs(a.cl.e || 0)\n      : (Math.abs(b.imp) + b.cb) - (Math.abs(a.imp) + a.cb))).slice(0, 25);\n  const trunc = dets.some((x) => x.truncated);\n  const semSessCob = periods.some((_, i) => sessOf(i) === null);\n  const head = `<tr class=\"det\"><td class=\"l\" colspan=\"${cols}\">\n    <b>${fx(refs.size, 0)}</b> recurso${refs.size === 1 ? '' : 's'} em <span class=\"mono\">${esc(dim)}</span> · mostrando ${fx(Math.min(25, refs.size), 0)} por ${st.sort === 'elasticidade' ? 'discrepância' : 'impacto'}\n    ${trunc ? `<span class=\"warnI\"> · ⋯ lista truncada em ${fx(Math.max.apply(null, dets.map((x) => x.pages || 0)) * PAGE, 0)} linhas por período — o total da linha continua vindo do nível 1, que é completo. Os volumes abaixo cobrem só os dias cobertos (${covers.join(' / ')} de ${periods[0].days.length}); ${estoque\n      ? 'o estoque é a média desses dias, e o por-sessão divide pelas ' + R.sess + ' do período inteiro'\n      : 'o por-sessão divide só pelas ' + R.sess + ' desses dias, e o impacto ≈ é estimado para o período inteiro'\n        + (semSessCob ? '. Onde faltam as ' + R.sess + ', o por-sessão sai “—” e aquele período fica fora do veredito' : '')}</span>` : ''}\n    ${dim === 'file.reference' ? '<span class=\"sub\"> · arquivos individuais não têm nome no dicionário</span>' : ''}\n  </td></tr>`;\n  const body = list.map((x) => {\n    const nm = labelOf(x.ref, dim);\n    // o id do recurso ganha coluna própria: é ele que serve para procurar o recurso no builder.\n    // Em CONNECT/request a dimensão JÁ é o path, então nome e id seriam a mesma string — não duplica.\n    const idTxt = (dim === 'request.path' && nm === x.ref) ? '—' : (x.ref || '(nulo)');\n    return `<tr class=\"det\"><td class=\"l\">${nm ? esc(nm) : '<span class=\"tag t-zero\">sem nome</span>'}</td>\n      <td class=\"l\"><span class=\"mono\" title=\"${esc(dim)}\">${esc(idTxt)}</span></td>\n      <td>${sparkline(x.spark, PROD_COLOR[r.p] || '#64748b', Object.assign(sparkOpts(periods, x.spark, r.m.u), { ref: x.refSerie }))}</td>\n      ${x.vals.map((v, i) => { const s = sessOf(i);\n        return `<td><div class=\"stack\"><span class=\"v\">${s ? fmtPerS(v * fE(i) / s, r.m.u) : '—'}<span class=\"un\">${estoque ? R.unE : R.un}</span></span>\n          <span class=\"s\">${fmtVal(v, r.m.u)}</span></div></td>`; }).join('')}\n      <td><span class=\"tag t-${x.cl.cls}\" title=\"${esc(x.cl.why || '')}${coberturaDesigual ? ' · ⋯ os períodos cobrem quantidades diferentes de dias (truncagem da paginação): leia com ressalva' : ''}\">${esc(x.cl.tag)}${coberturaDesigual ? ' ⋯' : ''}</span></td>\n      <td>${x.imp ? `<span class=\"${x.imp > 0 ? 'bad' : 'good'}\"${trunc && !estoque ? ' title=\"estimado: volume dos dias cobertos escalado para o período inteiro\"' : ''}>${trunc && !estoque ? '≈ ' : ''}${fmtUsd(x.imp)}</span>` : '—'}</td></tr>`;\n  }).join('');\n  return head + body;\n}\n\nfunction renderKpis(tot, rot) {\n  const R = rot || ROT.total;\n  const base = tot[0];\n  // retenção (retidas ÷ totais) na linha de contagem, nos dois modos, quando a seção trouxe retidas. Sem Δ em p.p.:\n  // não cabe no cartão impresso, e ela não ganha cor (vermelho e verde ficam só para o US$/sessão)\n  const temR = tot.some((t) => t.sessRet !== null && t.sessRet !== undefined);\n  // O US$/sessão é o número mais visível da página e o mais fácil de confundir com \"custo de um\n  // atendimento\". Ele inclui parcelas que não variam com volume (BUILDER/sessions, storage rateado),\n  // então cai quando o tráfego sobe sem que nada tenha ficado mais barato.\n  const nota = R.real ? `<div class=\"foot\" style=\"grid-column:1/-1\">${MOE.sim}/${R.sessao} = o <b>mesmo</b> custo modelado ÷ só as\n    ${R.ret ? 'sessões <b>reais retidas</b> (das filas que podem falar com a IA, resolvidas sem atendimento humano), ou seja, ' + MOE.sim + '/sessão real ÷ retenção real'\n      : 'sessões <b>reais</b> (as das filas que podem falar com a IA; fora as que vão direto ao humano)'}. <b>Não é o custo de um\n    atendimento nem custo marginal</b>: inclui parcelas que existiriam com zero atendimento. Valores em ${MOE.sim} são\n    <b>modelados a preço acordado</b> e não reconciliam com a fatura — ver a nota ao pé da página.</div>`\n    : R.ret ? `<div class=\"foot\" style=\"grid-column:1/-1\">${MOE.sim}/sessão retida = o <b>mesmo</b> custo modelado ÷ só as\n    sessões <b>retidas</b> (resolvidas sem atendimento humano), ou seja, ${MOE.sim}/sessão ÷ retenção. <b>Não é o custo de um\n    atendimento nem custo marginal</b>: inclui parcelas que existiriam com zero atendimento. Valores em ${MOE.sim} são\n    <b>modelados a preço acordado</b> e não reconciliam com a fatura — ver a nota ao pé da página.</div>`\n    : `<div class=\"foot\" style=\"grid-column:1/-1\">${MOE.sim}/sessão = custo modelado do período ÷ sessões\n    da plataforma no período. <b>Não é o custo de um atendimento nem custo marginal</b>: inclui parcelas\n    que existiriam com zero atendimento, então dobrar o volume não dobra o custo. Valores em ${MOE.sim} são\n    <b>modelados a preço acordado</b> e não reconciliam com a fatura — ver a nota ao pé da página.</div>`;\n  return nota + tot.map((t, i) => {\n    // período sem sessão ou sem dado: Δ \"—\" (antes (null − base)/base = −100% em VERDE)\n    const dS = i && base.sessions && t.sessions !== null && t.sessions !== undefined\n      ? (t.sessions - base.sessions) / base.sessions : null;\n    const dC = i && base.cps && t.cps !== null && t.cps !== undefined ? (t.cps - base.cps) / base.cps : null;\n    const cl = (x) => (x === null ? '' : x > 0.02 ? 'bad' : x < -0.02 ? 'good' : '');\n    return `<div class=\"kpi ${i === 0 ? 'isbase' : ''}\">\n      <div class=\"lab\">${esc(t.per.lab)} · ${esc(t.per.sub)}${i === 0 ? ' · BASE' : ''}</div>\n      <div class=\"big\">${t.cps === null ? '—' : fmtCps(t.cps)}<span class=\"sub\" style=\"font-size:11px\"> ${t.semDados ? 'sem dado carregado' : 'por ' + R.sessao}</span></div>\n      <div class=\"foot\">${i ? `<span class=\"${cl(dC)}\">${fmtPct(dC)}</span> vs base · ` : 'referência · '}${fmtUsd(t.cost)} de custo modelado</div>\n      ${t.aloc ? `<div class=\"foot\">+ ${fmtUsd(t.aloc)} de pessoas (${t.nAloc} aloc.) ·\n        <b>${t.total === null ? 'total incompleto (infra sem dado)' : fmtUsd(t.total) + ' total'}</b></div>` : ''}\n      ${t.alocPend ? `<div class=\"foot\"><span class=\"warnI\">+ pessoas (${t.nAloc} aloc.): sem câmbio digitado, fora do total</span></div>` : ''}\n      <div class=\"foot\">${t.sessions === null ? (R.alt && (R.ret ? t.sessRet : t.sessTot) === 0 ? '0' : '—') : t.sessions.toLocaleString('pt-BR')} ${MOE.alt && R.real ? 'sessões ' + R.cont : R.cont} ${i ? `<span>${fmtPct(dS)}</span>` : ''}${temR\n        ? ' · ' + R.rl + ' ' + fmtRet(retOf(t.sessTot, t.sessRet)) + (R.ret && t.sessTot !== null ? ' de' + String.fromCharCode(160) + t.sessTot.toLocaleString('pt-BR') : '') : ''}</div>\n    </div>`;\n  }).join('');\n}\n\n// ---------- estado ----------\nfunction defaultDays(d2) {\n  const n = Math.max(MIN_DAYS, Math.min(MAX_DAYS, Number(d2.param('defaultDays', 3)) || 3));\n  const end = yesterday(), out = [];\n  for (let i = n - 1; i >= 0; i--) out.push(addDays(end, -i));\n  return out;\n}\nconst EMPTY_PROG = { done: 0, total: 0, label: '' };\n// render() nunca depende de setState ter rodado: lê o estado com defaults aplicados.\nfunction viewState(d2) {\n  const s = d2.state || {};\n  // o modo \"retidas\" só vale com retidas carregadas em alguma seção; o pedido fica guardado em s.den e volta sozinho\n  // quando elas chegam\n  const temRet = ['d', 'w', 'm'].some((x) => {\n    const r = s[x + 'SessRet'] || {}, t = s[x + 'Sess'] || {};\n    return Object.keys(r).some((k) => t[k] === undefined || t[k] === null || !(r[k] > t[k]));   // incoerente não conta\n  });\n  // universo \"só filas c/ IA\" (sessões reais): vale com reais carregadas; dentro dele, \"retidas\" vale com reais retidas\n  // coerentes. Os pedidos ficam guardados em s.uni / s.den e voltam sozinhos quando os dados chegam\n  const temReal = ['d', 'w', 'm'].some((x) => Object.keys(s[x + 'SessReal'] || {}).length > 0);\n  const temRealRet = ['d', 'w', 'm'].some((x) => {\n    const r = s[x + 'SessRealRet'] || {}, t = s[x + 'SessReal'] || {};\n    return Object.keys(r).some((k) => t[k] === undefined || t[k] === null || !(r[k] > t[k]));\n  });\n  const uni = (s.uni === 'reais' && temReal) ? 'reais' : 'todas';\n  return {\n    theme: s.theme || 'dark', sort: s.sort || 'impacto',\n    uni, temReal, temRealRet,\n    den: (s.den === 'retidas' && (uni === 'reais' ? temRealRet : temRet)) ? 'retidas' : 'total', temRet,\n    minCost: (s.minCost === undefined || s.minCost === null) ? 1 : s.minCost,\n    days: normDays((s.days && s.days.length) ? s.days : defaultDays(d2)),\n    dAgg: s.dAgg || {}, dSess: s.dSess || {}, dDet: s.dDet || {}, dOpen: s.dOpen || {},\n    dLoading: !!s.dLoading, dProg: s.dProg || EMPTY_PROG, dErr: s.dErr || null, dRun: s.dRun || 0,\n    dSessErr: s.dSessErr || [], wSessErr: s.wSessErr || [], mSessErr: s.mSessErr || [],\n    dSessRet: s.dSessRet || {}, wSessRet: s.wSessRet || {}, wDaySessRet: s.wDaySessRet || {}, mSessRet: s.mSessRet || {},\n    dRetErr: s.dRetErr || [], wRetErr: s.wRetErr || [], mRetErr: s.mRetErr || [],\n    dSessReal: s.dSessReal || {}, wSessReal: s.wSessReal || {}, wDaySessReal: s.wDaySessReal || {}, mSessReal: s.mSessReal || {},\n    dSessRealRet: s.dSessRealRet || {}, wSessRealRet: s.wSessRealRet || {}, wDaySessRealRet: s.wDaySessRealRet || {},\n    mSessRealRet: s.mSessRealRet || {},\n    dRealErr: s.dRealErr || [], wRealErr: s.wRealErr || [], mRealErr: s.mRealErr || [],\n    dRealRetErr: s.dRealRetErr || [], wRealRetErr: s.wRealRetErr || [], mRealRetErr: s.mRealRetErr || [],\n    wEnd: s.wEnd || yesterday(),\n    wAgg: s.wAgg || {}, wSess: s.wSess || {}, wDaySess: s.wDaySess || {}, wDet: s.wDet || {},\n    wOpen: s.wOpen || {}, wLoading: !!s.wLoading, wProg: s.wProg || EMPTY_PROG,\n    wErr: s.wErr || null, wRun: s.wRun || 0, wWarn: s.wWarn || '',\n    wCount: Math.max(WEEKS_MIN, Math.min(WEEKS_MAX, s.wCount || WEEKS_MAX)),\n    mEnd: s.mEnd || yesterday(),\n    mAgg: s.mAgg || {}, mSess: s.mSess || {}, mDet: s.mDet || {}, mOpen: s.mOpen || {},\n    mLoading: !!s.mLoading, mProg: s.mProg || EMPTY_PROG, mErr: s.mErr || null, mRun: s.mRun || 0,\n    mWarn: s.mWarn || '',\n    mCount: Math.max(MONTHS_MIN, Math.min(MONTHS_MAX, s.mCount || MONTHS_MAX)),\n    mAlloc: s.mAlloc || {}, mRef: s.mRef || '',\n    // câmbio digitado NESTA execução (o estado não sobrevive a recarregar a página: começa vazio, decisão do dono).\n    // R$ só com câmbio; o pedido fica em s.moeda e volta sozinho, como uni/den\n    fx: fxOk(Number(s.fx)) ? Number(s.fx) : null, fxSet: fxOk(Number(s.fx)), fxErr: s.fxErr || '', pisoPend: s.pisoPend || null,\n    moeda: (s.moeda === 'brl' && fxOk(Number(s.fx))) ? 'brl' : 'usd',\n    printedAt: s.printedAt || '', pdfWarn: s.pdfWarn || ''\n  };\n}\n// ---------- escopo ----------\n// O escopo era BINÁRIO em 24 pontos (`sc === 'w' ? … : …`), e 16 escritas de estado fixavam o\n// prefixo `d`/`w` no nome da chave. Com três seções cada um viraria ternário de três vias. As chaves\n// já seguem o padrão <letra><Nome> (dAgg/wAgg/mAgg), então basta montar o nome — mesmo truque que\n// `alive` já usava com `d2.state[runKey]`. Isso torna as ESCRITAS genéricas; um 4º escopo ainda exige\n// os defaults em viewState (inclusive <x>SessErr), um ctrl<Nome>, o bloco em render() e as listas do\n// printhead (escopo e carr), as ações load/cancel no handler, o congelamento da carga em loadScope e o\n// emVooDe do PDF (TXT, §10.5).\nconst K = (sc, nome) => sc + nome;\nconst setScope = (sc, o) => { const out = {}; for (const n in o) out[K(sc, n)] = o[n]; return out; };\nconst SCOPES = {\n  d: { ord: 1, periods: (st) => dayPeriods(st.days),\n       maxPages: MAX_PAGES,   topN: TOP_N,   prog: false, strip: false, drill: true },\n  w: { ord: 2, periods: (st) => weekPeriods(st.wEnd, st.wCount),\n       maxPages: MAX_PAGES_W, topN: TOP_N_W, prog: true,  strip: true,  drill: true },\n  // `strip:false` — o mensal não busca sessões por dia: o endpoint devolve a soma do intervalo, então\n  // um mês custa 1 chamada em vez de 30. `drill:false` — detalhe por recurso num mês são ~16 páginas.\n  m: { ord: 3, periods: (st) => monthPeriods(st.mEnd, st.mCount, st.mRef),\n       maxPages: MAX_PAGES_M, topN: TOP_N_M, prog: true,  strip: false, drill: false, alloc: true }\n};\n// `periodTotals` e `costByProduct` recebem só `store`, nunca `st` — então a alocação viaja por aqui,\n// o que evita mexer na lista de parâmetros das duas e nos seus chamadores.\n// O DIVISOR mora aqui: no modo \"retidas\", sess/daySess passam a ser as sessões RETIDAS — e todo consumidor (buildRows,\n// classify, renderDetail, periodTotals, HERO, KPIs, faixa diária, avisos) troca junto. sess e daySess trocam JUNTOS:\n// o drill truncado do semanal soma daySess dos dias cobertos. Retida maior que o total é dado incoerente da fonte:\n// sai como ausente (e um aviso conta), nunca como retenção acima de 100%.\nconst storeOf = (st, sc) => {\n  const real = st.uni === 'reais';\n  const R = real ? (st.den === 'retidas' ? ROT.reaisRet : ROT.reais) : (st.den === 'retidas' ? ROT.retidas : ROT.total);\n  // o PAR do universo: (sessões, retidas) ou (reais, reais retidas); `todas` é o total de sessões, \"pai\" das reais\n  const todas = st[K(sc, 'Sess')] || {};\n  const tot = real ? (st[K(sc, 'SessReal')] || {}) : todas;\n  const dTot = st[K(sc, real ? 'DaySessReal' : 'DaySess')] || {};\n  const ok = (t, v) => t === undefined || t === null || !(v > t);\n  const coer = (r, t) => { const o = {}; for (const k in r) if (ok(t[k], r[k])) o[k] = r[k]; return o; };\n  const rCru = st[K(sc, real ? 'SessRealRet' : 'SessRet')] || {}, dCru = st[K(sc, real ? 'DaySessRealRet' : 'DaySessRet')] || {};\n  const ret = coer(rCru, tot), dRet = coer(dCru, dTot);\n  // divisor 0 NÃO é divisor (retidas, reais ou reais retidas): o período sai de sess como ausente (senão o classify lia\n  // Δ −100% e a reta da progressão passava pelo zero); sessTot/sessRet guardam o 0 para a retenção e para o aviso. O\n  // total de sessões do universo \"todas\" segue como sempre foi.\n  const sem0 = (o) => { const r = {}; for (const k in o) if (o[k] !== 0) r[k] = o[k]; return r; };\n  return { agg: st[K(sc, 'Agg')] || {}, sess: R.ret ? sem0(ret) : (real ? sem0(tot) : tot),\n    det: st[K(sc, 'Det')] || {}, daySess: R.ret ? dRet : dTot,\n    sessTot: tot, sessRet: ret, daySessTot: dTot, daySessRet: dRet, rot: R,\n    sessPai: R.ret ? tot : todas,   // o que a fonte respondeu \"sem o campo do divisor\" (diagnóstico do aviso)\n    sessTodas: todas,               // reais retidas sem nem as reais: o diagnóstico sobe até o total de sessões\n    retIncoer: Object.keys(rCru).filter((k) => !(k in ret)),\n    retIncoerDias: Object.keys(dCru).filter((k) => !(k in dRet)),\n    alloc: SCOPES[sc].alloc ? (per) => allocOf(st, per) : null,\n    allocUsd: st.fxSet ? ALLOC_BRL / st.fx : 0 };   // sem câmbio digitado: 0 = fora do total (periodTotals avisa)\n};\nconst periodsOf = (st, sc) => SCOPES[sc].periods(st);\n// há busca de detalhe em voo? (o drill sob demanda não liga <sc>Loading, então o cancelar e o bloqueio do\n// PDF olham também as marcas __loading)\nconst temBusca = (det) => Object.keys(det || {}).some((k) => det[k] && det[k].__loading);\nconst emVooDe = (st) => st.dLoading || st.wLoading || st.mLoading\n  || temBusca(st.dDet) || temBusca(st.wDet) || temBusca(st.mDet);\n\n// ---------- render: controles ----------\nfunction progBar(prog) {\n  if (!prog.total) return '';\n  return `<div class=\"mt8\"><div class=\"bar2\"><i style=\"width:${(prog.done / prog.total * 100).toFixed(1)}%\"></i></div>\n    <div class=\"sub\" style=\"margin-top:5px\">${prog.done}/${prog.total} tarefas · ${esc(prog.label || '')} · cada tarefa de detalhe pode levar várias páginas</div></div>`;\n}\n\nfunction ctrlDaily(st) {\n  const chips = st.days.map((d, i) => `\n    <span class=\"chip ${i === 0 ? 'base' : ''}\" title=\"${i === 0 ? 'período-base: todos comparam com ele' : ''}\">\n      ${i === 0 ? '<b>BASE</b>' : ''}\n      <input type=\"date\" data-act=\"setday\" data-i=\"${i}\" value=\"${d}\" max=\"${yesterday()}\">\n      <span class=\"mono\">${dowPt(d)}</span>\n      ${st.days.length > MIN_DAYS ? `<button class=\"x\" data-act=\"rmday\" data-i=\"${i}\" title=\"remover\">×</button>` : ''}\n    </span>`).join('');\n  return `<div class=\"card\"><div class=\"row\">\n      ${chips}\n      ${st.days.length < MAX_DAYS ? '<button data-act=\"addday\" class=\"ghost\">+ dia</button>' : `<span class=\"sub\">teto de ${MAX_DAYS} dias</span>`}\n      ${st.days.length <= MIN_DAYS ? `<span class=\"sub\">mínimo de ${MIN_DAYS} dias — o primeiro é a base de comparação</span>` : ''}\n      <button class=\"pri\" data-act=\"loadD\" ${st.dLoading || st.days.length < MIN_DAYS ? 'disabled' : ''}>${st.dLoading ? '<span class=\"spin\"></span> carregando…' : 'Carregar diário'}</button>\n      ${st.dLoading || temBusca(st.dDet) ? '<button data-act=\"cancelD\" class=\"ghost\">cancelar</button>' : ''}\n    </div>${progBar(st.dProg)}</div>`;\n}\n\nfunction ctrlWeekly(st) {\n  const pers = weekPeriods(st.wEnd, st.wCount);\n  return `<div class=\"card\"><div class=\"row\">\n      <span class=\"chip\"><b>ATÉ</b><input type=\"date\" data-act=\"setwend\" value=\"${st.wEnd}\" max=\"${yesterday()}\"><span class=\"mono\">${dowPt(st.wEnd)}</span></span>\n      <span class=\"sub\">${pers.map((p, i) => (i === 0 ? '<b>' + esc(p.lab) + '</b> (base)' : esc(p.lab))).join(' · ')}</span>\n      <button data-act=\"rmweek\" class=\"ghost\" title=\"mínimo ${WEEKS_MIN} semanas\">− semana</button>\n      <span class=\"mono\">${st.wCount} sem.</span>\n      <button data-act=\"addweek\" class=\"ghost\" title=\"máximo ${WEEKS_MAX} semanas\">+ semana</button>\n      <button class=\"pri\" data-act=\"loadW\" ${st.wLoading ? 'disabled' : ''}>${st.wLoading ? '<span class=\"spin\"></span> carregando…' : 'Carregar semanal'}</button>\n      ${st.wLoading || temBusca(st.wDet) ? '<button data-act=\"cancelW\" class=\"ghost\">cancelar</button>' : ''}\n    </div>\n    ${st.wWarn === 'max' ? `<div class=\"note mt8\"><b>Máximo de ${WEEKS_MAX} semanas.</b>\n      Com ${WEEKS_MAX} janelas a leitura já é de progressão — a reta sobre os ${WEEKS_MAX} pontos, que é o que separa\n      \"subiu e voltou\" de \"alta consistente\". Mais uma só somaria carga (${2 + WEEK_LEN} chamadas de sessões e\n      visão geral por janela, mais as páginas do detalhe) sem mudar essa leitura. Para olhar mais longe, mova a data final.</div>` : ''}\n    ${st.wWarn === 'min' ? `<div class=\"note mt8\"><b>Mínimo de ${WEEKS_MIN} semanas.</b>\n      Com uma semana só, o período-base seria o único período — não sobraria nada contra o que comparar,\n      e elasticidade e impacto ficariam indefinidos. Para ver uma semana isolada, use a seção diária.</div>` : ''}\n    ${progBar(st.wProg)}</div>`;\n}\n\nfunction ctrlMonthly(st) {\n  const pers = monthPeriods(st.mEnd, st.mCount, st.mRef);\n  const parc = pers.filter((p) => p.parcial);\n  const usdPorAloc = st.fxSet ? ALLOC_BRL / st.fx : 0;\n  return `<div class=\"card\"><div class=\"row\">\n      <span class=\"chip\"><b>ATÉ O MÊS DE</b><input type=\"date\" data-act=\"setmend\" value=\"${st.mEnd}\" max=\"${yesterday()}\"><span class=\"mono\">${monthLab(st.mEnd)}</span></span>\n      <span class=\"sub\">${pers.map((p, i) => (i === 0 ? '<b>' + esc(p.lab) + '</b> (base)' : esc(p.lab))).join(' · ')}</span>\n      <button data-act=\"rmmonth\" class=\"ghost\" title=\"mínimo ${MONTHS_MIN} meses\">− mês</button>\n      <span class=\"mono\">${st.mCount} meses</span>\n      <button data-act=\"addmonth\" class=\"ghost\" title=\"máximo ${MONTHS_MAX} meses\">+ mês</button>\n      <button class=\"pri\" data-act=\"loadM\" ${st.mLoading ? 'disabled' : ''}>${st.mLoading ? '<span class=\"spin\"></span> carregando…' : 'Carregar mensal'}</button>\n      ${st.mLoading || temBusca(st.mDet) ? '<button data-act=\"cancelM\" class=\"ghost\">cancelar</button>' : ''}\n    </div>\n    ${SCOPES.m.alloc ? `<div class=\"row alocrow\">\n      <span class=\"chip\"><b>ALLOCATIONS</b><span class=\"sub\">devs contratados · R$ ${ALLOC_BRL.toLocaleString('pt-BR')} cada</span></span>\n      ${pers.map((p) => { const n = allocOf(st, p); return `<span class=\"alocbox\">\n        <span class=\"alocmes\">${esc(p.lab)}</span>\n        <span class=\"alocctl\">\n          <button data-act=\"allocdec\" data-k=\"${esc(p.k)}\" ${n <= 0 ? 'disabled' : ''}>−</button>\n          <input type=\"number\" class=\"alocnum\" data-act=\"allocset\" data-k=\"${esc(p.k)}\" value=\"${n}\" min=\"0\" max=\"${ALLOC_MAX}\" step=\"1\">\n          <button data-act=\"allocinc\" data-k=\"${esc(p.k)}\" ${n >= ALLOC_MAX ? 'disabled' : ''}>+</button>\n        </span>\n        <span class=\"alocusd\" title=\"${st.fxSet ? n + ' × R$ ' + ALLOC_BRL.toLocaleString('pt-BR') + (MOE.alt ? '' : ' ÷ R$ ' + fmtTaxa(st.fx) + '/US$ (câmbio do topo)')\n          : 'sem câmbio: digite US$ 1 = R$ … no topo para somar as alocações'}\">${st.fxSet ? fmtUsd(n * usdPorAloc) : '—'}</span>\n      </span>`; }).join('')}\n    </div>` : ''}\n    ${SCOPES.m.alloc && !st.fxSet && pers.some((p) => allocOf(st, p) > 0) ? `<div class=\"note mt8\"><b>Custo de pessoas fora dos totais.</b>\n      As alocações nascem em R$ e o painel soma em US$: digite o câmbio do dia no topo (US$ 1 = R$ …) para somá-las.\n      O campo começa vazio a cada abertura.</div>` : ''}\n    ${parc.length ? `<div class=\"note mt8\"><b>${esc(parc[0].lab)} é um mês PARCIAL</b> (${esc(parc[0].sub)}).\n      O custo <b>por sessão</b> continua comparável, porque numerador e denominador encolhem juntos — mas o\n      custo <b>total</b> desse mês aparece menor só porque ainda faltam dias. Não leia essa queda como economia —\n      por isso o <b>impacto ${MOE.sim}</b> compara os meses no mesmo ritmo diário (o storage, pelo nível médio de GiB).\n      ${SCOPES.m.alloc && st.fxSet && allocOf(st, parc[0]) > 0 ? `<br>A <b>alocação entra cheia</b> nesse mês — salário não se rateia por dia decorrido —, então o total\n      mistura pessoas de mês inteiro com infra de mês parcial.` : ''}</div>` : ''}\n    ${st.mWarn === 'max' ? `<div class=\"note mt8\"><b>Máximo de ${MONTHS_MAX} meses.</b>\n      Com ${MONTHS_MAX} pontos a leitura já é de progressão — a reta sobre os ${MONTHS_MAX} meses. Para olhar mais para trás,\n      mova o mês final.</div>` : ''}\n    ${st.mWarn === 'min' ? `<div class=\"note mt8\"><b>Mínimo de ${MONTHS_MIN} meses.</b>\n      Com um mês só, o período-base seria o único período — não sobraria nada contra o que comparar.</div>` : ''}\n    ${progBar(st.mProg)}</div>`;\n}\n\n// ---------- render: uma seção completa (serve as três seções) ----------\nfunction renderSection(st, sc) {\n  const periods = periodsOf(st, sc);\n  const store = storeOf(st, sc);\n  const has = periods.some((p) => store.agg[p.k]);\n  if (!has) return '';\n  const tot = periodTotals(store, periods);\n  const todas = buildRows(store, periods, SCOPES[sc].prog);\n  const rows = sortRows(todas, st.sort, Number(st.minCost) || 0);\n  const strip = SCOPES[sc].strip ? dailyStrip(periods.reduce((a, p) => a.concat(p.days), []), store) : '';\n  // consumo carregou e sessões não: a tela continua útil, mas metade das perguntas fica sem resposta\n  // avisa quando QUALQUER período carregado ficou sem sessões (antes só quando TODOS ficavam)\n  const semSessP = periods.filter((p) => store.agg[p.k] && (store.sess[p.k] === undefined || store.sess[p.k] === null));\n  const semSess = semSessP.length > 0 && semSessP.length === periods.filter((p) => store.agg[p.k]).length;\n  // a faixa vermelha só cobre os períodos cuja chamada de sessões FALHOU; sessão null sem erro é outro\n  // diagnóstico (TXT, §4) — numa seção mista, cada grupo recebe o seu\n  const sErr = st[K(sc, 'SessErr')] || [];\n  const R = store.rot;\n  // modo retidas: período com o TOTAL e sem a retida não é falha da chamada — a fonte respondeu sem o campo. Retida\n  // incoerente (maior que o total) tem aviso próprio. No modo totais as duas listas ficam vazias.\n  const incoerP = periods.filter((p) => store.agg[p.k] && store.retIncoer.includes(p.k));\n  // retida 0 (aviso próprio) e retida fora do formato (a faixa vermelha mostra o valor) têm diagnóstico próprio\n  const zeroP = R.alt ? periods.filter((p) => store.agg[p.k] && (R.ret ? store.sessRet[p.k] : store.sessTot[p.k]) === 0) : [];\n  const rErr = R.alt ? (st[K(sc, R.errKey)] || []) : [];\n  const ruimP = R.alt ? semSessP.filter((p) => rErr.includes(p.k)) : [];\n  // modo reais retidas: sem as REAIS também, o diagnóstico é do par inteiro (fonte sem as reais, ou reais fora do formato)\n  const reErr = R.real && R.ret ? (st[K(sc, 'RealErr')] || []) : [];\n  const ruimReal = semSessP.filter((p) => !incoerP.includes(p) && !zeroP.includes(p) && !ruimP.includes(p) && reErr.includes(p.k));\n  const fora = (p) => incoerP.includes(p) || zeroP.includes(p) || ruimP.includes(p) || ruimReal.includes(p);\n  const soRet = R.alt ? semSessP.filter((p) => !fora(p) && store.sessPai[p.k] !== undefined && store.sessPai[p.k] !== null) : [];\n  const semReal = R.real && R.ret ? semSessP.filter((p) => !fora(p) && !soRet.includes(p)\n    && store.sessTodas[p.k] !== undefined && store.sessTodas[p.k] !== null) : [];\n  const resto = semSessP.filter((p) => !soRet.includes(p) && !fora(p) && !semReal.includes(p));\n  const comErr = resto.filter((p) => sErr.includes(p.k)), semErr = resto.filter((p) => !sErr.includes(p.k));\n  const labsDe = (ps) => esc(ps.map((p) => p.lab).join(', '));\n  const misto = comErr.length > 0 && semErr.length > 0;\n  const diag = (comErr.length ? ' A faixa vermelha acima traz o diagnóstico' + (misto ? ' de ' + labsDe(comErr) : '')\n      + '; a causa costuma ser o token da fonte de sessões (natura-api-vault) ou a policy do listener de sessões da Natura.' : '')\n    + (semErr.length ? ' ' + (misto ? 'Em ' + labsDe(semErr) + ' não' : 'Não') + ' houve erro: a fonte de sessões respondeu sem número'\n      + (misto ? '' : ' para esses períodos') + ' — confira o caminho do valor na resposta do listener ($sres.data.sessions) ou se a fonte devolveu null.' : '')\n    + (soRet.length ? ' Em ' + labsDe(soRet) + ' a fonte respondeu ' + R.semCampo + ' — confira se o listener repassa ' + R.campo\n      + ' ($sres.data.' + R.campo + ') ou se a fonte devolveu null; ' + R.valido + ' desses períodos segue válido.' : '')\n    + (semReal.length ? ' Em ' + labsDe(semReal) + ' a fonte respondeu o total sem as sessões reais — confira se o listener repassa'\n      + ' sessoes_reais e sessoes_reais_retenidas ($sres.data.sessoes_reais) ou se a fonte devolveu null; o universo todas desses períodos segue válido.' : '')\n    + (ruimP.length ? ' Em ' + labsDe(ruimP) + ' as ' + R.sess + ' vieram fora do formato — a faixa vermelha acima mostra o valor; '\n      + R.valido + ' desses períodos segue válido.' : '')\n    + (ruimReal.length ? ' Em ' + labsDe(ruimReal) + ' as sessões reais vieram fora do formato — a faixa vermelha acima mostra o valor;'\n      + ' o universo todas desses períodos segue válido.' : '');\n  // o veredito só \"vem dos demais períodos\" se sobrar comparação: no diário toda comparação é contra a\n  // base (sem a sessão dela nenhuma vale); na progressão bastam 2 períodos com sessão e nível 1, desde que\n  // base e último tenham nível 1\n  const comS = periods.filter((p) => store.agg[p.k] && store.sess[p.k] !== undefined && store.sess[p.k] !== null);\n  const pU = periods[periods.length - 1];\n  const vale = !!store.agg[periods[0].k] && comS.length >= 2\n    && (comS.includes(periods[0]) || (SCOPES[sc].prog && !!store.agg[pU.k]));\n  const aviso = semSess ? `<div class=\"note mt\">Consumo carregado, <b>${R.sess} não</b>. Sem elas não há\n    custo por ${R.sessao} nem elasticidade — volume e custo absoluto seguem válidos.${diag}</div>`\n    : semSessP.length ? `<div class=\"note mt\"><b>${R.Sess} ausentes em ${esc(semSessP.map((p) => p.lab).join(', '))}</b>:\n    nesses períodos não há custo por ${R.sessao} nem elasticidade — as comparações com eles saem como \"sem dado\", ${vale\n      ? 'e o veredito da linha vem dos demais períodos'\n      : 'e nenhuma comparação de elasticidade sobra nesta seção (NOVO e SUMIU continuam valendo)'}.${diag}</div>` : '';\n  // Períodos selecionados mas ainda não buscados — acontece ao mover a data final ou ao passar de 2 para\n  // 3 semanas, que acrescenta uma janela mais antiga E a torna a nova base. Sem este aviso a tela\n  // mostraria “—” em toda a coluna e pareceria defeito, quando só falta carregar.\n  // retida maior que o total (nos dois modos) e retida zero (só no modo retidas, onde ela é o divisor)\n  // dias da faixa diária (só o semanal busca sessões por dia) com retida maior que o total: somem da faixa e do drill\n  const incoerD = SCOPES[sc].strip ? store.retIncoerDias.filter((d) => periods.some((p) => p.days.includes(d))).sort() : [];\n  const ondeIncoer = (incoerP.length ? 'em ' + labsDe(incoerP) : '')\n    + (incoerD.length ? (incoerP.length ? ' e ' : '') + (incoerD.length === 1 ? 'no dia ' : 'nos dias ') + esc(incoerD.map(shortDay).join(', ')) : '');\n  const avisoIncoer = (incoerP.length || incoerD.length) ? `<div class=\"note mt\"><b>${R.inc} ${ondeIncoer}</b>:\n    dado incoerente da fonte de sessões, descartado — a ${R.rl} ${incoerP.length ? 'desses períodos' : 'desses dias'} sai “—”${R.ret\n      ? ', e o custo por ' + R.sessao + ' também' : ''}.</div>` : '';\n  const avisoZero = zeroP.length ? `<div class=\"note mt\"><b>Nenhuma ${R.sessao} em ${labsDe(zeroP)}</b>: sem divisor, o custo\n    e o consumo por ${R.sessao} desse período saem “—”, e ele fica fora dos vereditos e da elasticidade. Se ${R.zq} não foi\n    mesmo zero, a fonte pode estar devolvendo 0 no lugar de “sem dado”.</div>` : '';\n  // universo reais: a regra da AR muda em 19/10/26 (os gerentes da Argentina passam a ir direto ao humano e saem das\n  // reais) — comparar períodos dos dois lados da data mistura duas definições de sessão real\n  const corteAR = '2026-10-19';\n  const avisoAR = R.real && periods.some((p) => store.agg[p.k] && p.d0 < corteAR) && periods.some((p) => store.agg[p.k] && p.d1 >= corteAR)\n    ? `<div class=\"note mt\"><b>A regra das sessões reais muda em 19/10/26</b>: a partir dessa data os gerentes da Argentina vão\n    direto ao atendimento humano e saem das reais — comparações que cruzam a data misturam duas definições.</div>` : '';\n  const faltam = periods.filter((p) => !store.agg[p.k]);\n  const avisoFalta = faltam.length ? `<div class=\"note mt\"><b>${faltam.length} de ${periods.length} períodos\n    ainda sem dado</b> (${esc(faltam.map((p) => p.lab).join(', '))}) — as colunas saem como “—”.\n    ${!store.agg[periods[0].k] ? '<b>O período-base está entre eles</b>, então nenhuma comparação vale. ' : ''}\n    <span class=\"noprint\">Clique em <b>Carregar</b> para buscá-los.</span><span class=\"onlyprint\">Esses períodos não foram carregados.</span></div>` : '';\n  return aviso + avisoIncoer + avisoZero + avisoAR + avisoFalta + `<div class=\"hero mt\">\n      <div class=\"card\"><h2>${R.Sess} × custo por ${R.sessao}</h2>${chartHero(tot, R)}${strip ? '<div class=\"mt8\">' + strip + '</div>' : ''}</div>\n      <div class=\"heroK\">${renderKpis(tot, R)}</div>\n    </div>\n    <div class=\"charts mt\">\n      <div class=\"card\"><h2>Custo estimado por produto${MOE.alt ? ' (' + MOE.sim + ')' : ''}</h2>${chartStacked(store, tot)}</div>\n      <div class=\"card\"><h2>Elasticidade por evento</h2>${chartElasticity(rows, R)}</div>\n    </div>\n    ${renderTable(st, sc, periods, store, rows, todas.length)}`;\n}\n\n// ---------- acesso (login dentro do artboard) ----------\n// Desde 23/10/26 o portal não tem app nem router: é só o artboard, aberto pelo builder. Quem protege os dados é\n// a policy do listener (sessão admin do Authenticator Strategy, builder ou rush interno; anônimo = 401). No\n// builder (canvas, ou o preview em *.data2.qa, que já exige login de builder) a perna de builder autoriza as\n// chamadas (medido 23/10: sessions de 1 dia OK), então o portal abre direto. Em qualquer outro lugar (um app\n// publicado, se voltar a existir) o artboard pede o login antes de mostrar qualquer coisa. Isto é só a tela:\n// a garantia continua sendo a policy. Sem o param `auth` na instância, o portal renderiza direto.\nfunction authDe(d2) {\n  const ref = d2.param('auth');\n  return ref && typeof d2.auth === 'function' ? d2.auth(ref) : null;\n}\nfunction noBuilder(d2) {\n  const c = d2.context || {};\n  const host = typeof location !== 'undefined' ? String(location.host || '') : '';\n  return !!c.isBuilder || /\\.data2\\.qa$/.test(host);\n}\nfunction acessoDe(d2) {\n  const a = authDe(d2);\n  if (!a || noBuilder(d2)) return { ok: true };\n  const u = a.user;\n  if (u && u.email && String(u.role || '') === 'admin') return { ok: true };\n  return { ok: false, logado: !!(u && u.email) };\n}\nfunction telaLogin(d2, acesso) {\n  const s = d2.state || {};\n  const msg = s.lgErr || (acesso.logado ? 'Conta sem permissão de acesso (role diferente de admin).' : '');\n  return '<div class=\"lgn\"><form data-login novalidate>'\n    + '<h1>Portal de custos — acesso restrito</h1>'\n    + '<div class=\"lgsub\">Entre com sua conta do Authenticator Strategy (role admin).</div>'\n    + '<label>E-mail<input name=\"email\" type=\"email\" autocomplete=\"username\"></label>'\n    + '<label>Senha<input name=\"password\" type=\"password\" autocomplete=\"current-password\"></label>'\n    + '<button type=\"submit\"' + (s.lgBusy ? ' disabled' : '') + '>' + (s.lgBusy ? 'Entrando…' : 'Entrar') + '</button>'\n    + (msg ? '<div class=\"lgerr\">' + esc(msg) + '</div>' : '')\n    + '</form></div>';\n}\n\nexport default {\n  css: CSS,\n\n  render(d2) {\n    const acesso = acessoDe(d2);\n    if (!acesso.ok) return telaLogin(d2, acesso);\n    const st = viewState(d2);\n    MOE = moedaOf(st);   // antes de qualquer HTML: seções, ctrlMonthly e printhead formatam dinheiro\n    // Cabeçalho que SÓ existe no papel: os controles somem na impressão, então o escopo do relatório\n    // precisa estar escrito em texto, senão o PDF sai sem dizer de que dias ele fala.\n    // ⚠️ weekPeriods sem o wCount usava o teto e podia LISTAR mais semanas do que a seção mostra.\n    const cD = dayPeriods(st.days).filter((p) => st.dAgg[p.k]).map((p) => p.lab);\n    const cW = weekPeriods(st.wEnd, st.wCount).filter((p) => st.wAgg[p.k]).map((p) => p.lab);\n    const mPer = monthPeriods(st.mEnd, st.mCount, st.mRef).filter((p) => st.mAgg[p.k]);\n    const cM = mPer.map((p) => p.lab);\n    const temAloc = !!SCOPES.m.alloc && mPer.some((p) => allocOf(st, p) > 0);\n    // o aviso de mês parcial fica no card de controles, que NÃO imprime: o papel precisa dele em texto\n    const parcImp = mPer.filter((p) => p.parcial);\n    const escopo = [cD.length ? 'Diário: ' + cD.join(', ') : '', cW.length ? 'Semanal: ' + cW.join(', ') : '',\n      cM.length ? 'Mensal: ' + cM.join(', ') : ''].filter(Boolean).join('   ·   ') || 'nenhum período carregado';\n    // As duas seções podem cobrir os MESMOS dias (um dia solto costuma cair dentro de uma das semanas).\n    // Somar as duas contaria o mesmo custo duas vezes — e no papel nada impediria esse erro.\n    // Com três seções a sobreposição virou a regra (dia ⊂ semana ⊂ mês), então o aviso precisa dizer\n    // QUAIS pares se cruzam. O teste também passa a ser de interseção de intervalos: o antigo só\n    // comparava o INÍCIO de um contra o outro e perdia um período que começa antes e invade.\n    const carr = [['Diário', dayPeriods(st.days).filter((p) => st.dAgg[p.k])],\n      ['Semanal', weekPeriods(st.wEnd, st.wCount).filter((p) => st.wAgg[p.k])],\n      ['Mensal', monthPeriods(st.mEnd, st.mCount, st.mRef).filter((p) => st.mAgg[p.k])]];\n    const pares = [];\n    for (let i = 0; i < carr.length; i++) for (let j = i + 1; j < carr.length; j++) {\n      if (carr[i][1].some((a) => carr[j][1].some((b) => a.d0 <= b.d1 && b.d0 <= a.d1))) {\n        pares.push(carr[i][0] + ' × ' + carr[j][0]);\n      }\n    }\n    const alerta = pares.length\n      ? 'Seções que cobrem os MESMOS dias: ' + pares.join(', ') + '. Não some uma com a outra — '\n        + 'seria o mesmo custo contado duas vezes.'\n      : '';\n    const secD = renderSection(st, 'd'), secW = renderSection(st, 'w'), secM = renderSection(st, 'm');\n    // seção sem nada carregado e sem erro: o título não vai ao papel (a dica embaixo já é .noprint)\n    const shC = (sec, err) => 'secthead' + (sec || err ? '' : ' noprint');\n    return `<div class=\"wrap ${st.theme === 'light' ? 'light' : ''}${(st.den === 'retidas' || st.uni === 'reais') ? ' ret' : ''}${(st.den === 'retidas' && st.uni === 'reais') ? ' realret' : ''}\">\n      <div class=\"printhead\">\n        <h1>Consumo × sessões — Natura AVI${MOE.alt ? ` <span style=\"font-weight:400\">· valores em ${MOE.sim} · US$ 1 = R$ ${fmtTaxa(MOE.taxa)}</span>` : ''}</h1>\n        <div class=\"l\">${esc(escopo)}</div>\n        <div class=\"l\">${st.uni === 'reais' ? (st.den === 'retidas'\n          ? '<b>Sessão = sessão REAL RETIDA (filas que podem falar com a IA, resolvida sem humano).</b> O custo é o mesmo; só o divisor muda: ' + MOE.sim + '/sessão real retida = ' + MOE.sim + '/sessão real ÷ retenção real.'\n          : '<b>Sessão = sessão REAL</b> (só as filas que podem falar com a IA; fora gerentes de vendas, gerentes de CO/PE/EC e, desde 19/10/26, os da AR). Retenção real = reais retidas ÷ reais.')\n          : st.den === 'retidas'\n          ? '<b>Sessão = sessão RETIDA (resolvida sem atendimento humano).</b> O custo é o mesmo do modo totais; só o divisor muda: ' + MOE.sim + '/sessão retida = ' + MOE.sim + '/sessão ÷ retenção (retidas ÷ totais).'\n          : st.temRet ? '<b>Sessão = toda sessão da plataforma no período (modo totais).</b> Retenção = sessões retidas (resolvidas sem atendimento humano) ÷ totais.'\n            : '<b>Sessão = toda sessão da plataforma no período.</b>'}</div>\n        ${alerta ? `<div class=\"l\"><b>${esc(alerta)}</b></div>` : ''}\n        ${parcImp.length ? `<div class=\"l\"><b>${esc(parcImp.map((p) => p.lab + ' (' + p.sub + ')').join(', '))}: mês PARCIAL</b> — o custo\n          por sessão segue comparável, mas o custo total desse mês aparece menor só porque ainda faltam dias; o impacto ${MOE.sim} compara os meses no mesmo ritmo diário (o storage, pelo nível médio de GiB).${temAloc && st.fxSet && parcImp.some((p) => allocOf(st, p) > 0) ? ' A alocação entra cheia nesse mês.' : ''}</div>` : ''}\n        ${temAloc ? (st.fxSet ? `<div class=\"l\">Alocações a R$ ${ALLOC_BRL.toLocaleString('pt-BR')}/mês por desenvolvedor,\n          ${MOE.alt ? 'em R$, sem conversão' : 'convertidas a R$ ' + fmtTaxa(st.fx) + '/US$'}. Elas somam ao total do mês e NÃO entram no\n          custo por sessão, que segue sendo só de infraestrutura.</div>`\n          : `<div class=\"l\"><b>Alocações a R$ ${ALLOC_BRL.toLocaleString('pt-BR')}/mês por desenvolvedor FORA dos totais</b>: o câmbio do\n          dólar não foi informado. Elas nunca entram no custo por sessão, que é só de infraestrutura.</div>`) : ''}\n        <div class=\"l\">${st.printedAt ? 'Gerado em ' + esc(st.printedAt) + ' · ' : ''}preços da fatura de ago/26 (TABLE/storage a US$ 25/GiB-mês), exceto DOMAIN/ai_tokens\n          ao preço acordado de US$ 0,000008/token — por isso o total NÃO reconcilia com o valor faturado.</div>\n      </div>\n      <div class=\"between\">\n        <div>\n          <h1 class=\"noprint\">Consumo × sessões — Natura AVI</h1>\n          <div class=\"sub\">A métrica-âncora é <b>produto/evento</b>: quanto cada recurso custa por sessão e\n            se ele cresceu na proporção do tráfego. O escopo vai de dias avulsos a janelas de 7 dias e meses de calendário.</div>\n        </div>\n        <div class=\"row noprint\">\n          <button data-act=\"pdf\" title=\"abre o diálogo de impressão do navegador; escolha 'Salvar como PDF'\">⬇ PDF</button>\n          <span class=\"chip seg\" role=\"group\" aria-label=\"universo de sessões\" title=\"${st.temReal\n            ? 'Todas: toda sessão da plataforma. Só filas c/ IA: as sessões reais — fora as filas que vão direto ao humano (gerentes de vendas, gerentes de CO/PE/EC e, desde 19/10/26, os da AR). A retenção mostrada é a do universo escolhido.'\n            : 'Nenhuma seção carregada trouxe sessões reais: só o universo todas está disponível.'}\"><b>UNIVERSO</b>\n            <button data-act=\"uni\" data-v=\"todas\" class=\"${st.uni === 'todas' ? 'on' : ''}\" aria-pressed=\"${st.uni === 'todas'}\">todas</button>\n            <button data-act=\"uni\" data-v=\"reais\" class=\"${st.uni === 'reais' ? 'on' : ''}\" aria-pressed=\"${st.uni === 'reais'}\"${st.temReal ? '' : ' disabled'}>só filas c/ IA</button>\n          </span>\n          <span class=\"chip seg\" role=\"group\" aria-label=\"sessões usadas como divisor do custo por sessão\" title=\"${(st.uni === 'reais' ? st.temRealRet : st.temRet)\n            ? (st.uni === 'reais'\n              ? 'Divisor de todo número “por sessão” (' + MOE.sim + '/sessão, consumo por sessão, elasticidade) no universo só filas c/ IA. Totais: as sessões reais. Retidas: só as reais resolvidas sem atendimento humano. O custo não muda, e o PDF sai no modo escolhido.'\n              : 'Divisor de todo número “por sessão” (' + MOE.sim + '/sessão, consumo por sessão, elasticidade). Totais: todas as sessões da plataforma. Retidas: só as resolvidas sem atendimento humano. O custo não muda, e o PDF sai no modo escolhido.')\n            : st.uni === 'reais' ? 'Nenhuma seção carregada trouxe sessões reais retidas: no universo só filas c/ IA, só o modo totais está disponível.'\n            : 'Nenhuma seção carregada trouxe sessões retidas: só o modo totais está disponível.'}\"><b>SESSÕES</b>\n            <button data-act=\"den\" data-v=\"total\" class=\"${st.den === 'total' ? 'on' : ''}\" aria-pressed=\"${st.den === 'total'}\">totais</button>\n            <button data-act=\"den\" data-v=\"retidas\" class=\"${st.den === 'retidas' ? 'on' : ''}\" aria-pressed=\"${st.den === 'retidas'}\"${(st.uni === 'reais' ? st.temRealRet : st.temRet) ? '' : ' disabled'}>retidas</button>\n          </span>\n          <span class=\"chip seg\" role=\"group\" aria-label=\"moeda dos valores monetários\" title=\"${st.fxSet\n            ? 'US$: a moeda da fatura. R$: todo valor monetário × o câmbio ao lado (US$ 1 = R$ ' + fmtTaxa(st.fx) + '). Só a exibição muda: contas, ordenação, vereditos e piso seguem os mesmos; os preços unitários da fatura e do acordo continuam em US$. O PDF sai na moeda escolhida, com o câmbio no cabeçalho.'\n            : 'Digite o câmbio do dia ao lado (US$ 1 = R$ …) para liberar o R$.'}\"><b>MOEDA</b>\n            <button data-act=\"moeda\" data-v=\"usd\" class=\"${MOE.id === 'usd' ? 'on' : ''}\" aria-pressed=\"${MOE.id === 'usd'}\">US$</button>\n            <button data-act=\"moeda\" data-v=\"brl\" class=\"${MOE.id === 'brl' ? 'on' : ''}\" aria-pressed=\"${MOE.id === 'brl'}\"${st.fxSet ? '' : ' disabled'}>R$</button>\n          </span>\n          <span class=\"chip${st.fxErr ? ' fxerr' : st.fxSet ? '' : ' fxpend'}\" title=\"${st.fxErr\n            ? esc('Valor não aceito (' + st.fxErr + '): use um número de ' + FX_MIN + ' a ' + FX_MAX + ', com vírgula ou ponto. ' + (st.fxSet ? 'Segue valendo R$ ' + fmtTaxa(st.fx) + '.' : 'Sem câmbio, só US$.'))\n            : st.fxSet ? 'Quantos R$ vale US$ 1 nesta execução (recarregar a página zera). Converte todo valor no modo R$' + (SCOPES.m.alloc ? '; no modo US$, converte as alocações (R$ ' + ALLOC_BRL.toLocaleString('pt-BR') + '/dev) para US$' : '') + '.'\n            : 'Digite a cotação do dia: quantos R$ vale US$ 1. O campo começa vazio a cada abertura; sem ele, o R$ fica desabilitado' + (SCOPES.m.alloc ? ' e as alocações do Mensal ficam fora dos totais' : '') + '.'}\"><b>CÂMBIO</b>\n            <span class=\"mono\">US$ 1 = R$</span>\n            <input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-act=\"setfx\" value=\"${st.fxErr ? esc(st.fxErr) : st.fxSet ? fmtTaxa(st.fx) : ''}\" placeholder=\"cotação\" aria-label=\"câmbio: reais por 1 dólar\"${st.fxErr ? ' aria-invalid=\"true\"' : ''} style=\"width:64px\">\n          </span>\n          <span class=\"row\" style=\"flex-wrap:nowrap\"><button data-act=\"sort\" data-v=\"impacto\" class=\"${st.sort === 'impacto' ? 'on' : ''}\">Impacto ${MOE.sim}</button>\n          <button data-act=\"sort\" data-v=\"elasticidade\" class=\"${st.sort === 'elasticidade' ? 'on' : ''}\">Discrepância</button>\n          ${st.sort === 'elasticidade' ? `<span class=\"sub\">piso <input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-act=\"mincost\" value=\"${st.pisoPend ? esc(st.pisoPend.txt) : fmtPiso(conv(st.minCost))}\"${st.pisoPend ? ' aria-invalid=\"true\" title=\"' + esc('Piso em R$ sem câmbio: passa a valer quando o câmbio for digitado no topo (US$ 1 = R$ …). Até lá, segue o piso de US$ ' + fmtPiso(st.minCost) + '.') + '\"' : ''} style=\"width:70px\"> ${MOE.sim}</span>` : ''}\n          <button data-act=\"theme\" class=\"ghost\">${st.theme === 'dark' ? '☀' : '☾'}</button></span>\n        </div>\n      </div>\n      ${st.pdfWarn && emVooDe(st) ? `<div class=\"note mt8 noprint\"><b>Há busca em andamento.</b>\n        Imprimir agora levaria ao papel a frase “buscando o detalhe por recurso…” no lugar dos\n        números, porque o estado de carregamento fica dentro da tabela. Espere a carga terminar —\n        ou clique em <b>cancelar</b> — e gere o PDF de novo.</div>` : ''}\n      ${st.fxErr ? `<div class=\"fxnota noprint\"><div class=\"note mt8 noprint\"><b>Câmbio não aceito: “${esc(st.fxErr)}”.</b> Use um número de ${FX_MIN} a ${FX_MAX},\n        com vírgula ou ponto e até 4 casas. ${st.fxSet ? 'Segue valendo US$ 1 = R$ ' + fmtTaxa(st.fx) + ' — é a taxa que a tela e o PDF usam.'\n          : 'Sem câmbio: só US$' + (SCOPES.m.alloc ? ', e as alocações do Mensal ficam fora dos totais' : '') + '.'}</div></div>` : ''}\n\n      ${renderExplain()}\n\n      <div class=\"${shC(secD, st.dErr)}\"><span class=\"n\">1</span><h2 style=\"margin:0\">Diário — dias avulsos, até ${MAX_DAYS}</h2>\n        <span class=\"sub\">o primeiro dia é a base de comparação</span></div>\n      <div class=\"noprint\">${ctrlDaily(st)}</div>\n      ${st.dErr ? `<div class=\"err mt\">${esc(st.dErr)}</div>` : ''}\n      ${secD || '<div class=\"card mt noprint\"><div class=\"empty\">escolha os dias e clique em <b>Carregar diário</b>.</div></div>'}\n\n      <div class=\"${shC(secW, st.wErr)}\"><span class=\"n\">2</span><h2 style=\"margin:0\">Semanal — ${st.wCount} janelas de ${WEEK_LEN} dias</h2>\n        <span class=\"sub\">de ${WEEKS_MIN} a ${WEEKS_MAX} janelas; a mais antiga é a base · com 3+ a\n          elasticidade lê a <b>progressão inteira</b>, e o <b>n/n</b> diz quantos degraus seguiram a direção</span></div>\n      <div class=\"noprint\">${ctrlWeekly(st)}</div>\n      ${st.wErr ? `<div class=\"err mt\">${esc(st.wErr)}</div>` : ''}\n      ${secW || `<div class=\"card mt noprint\"><div class=\"empty\">escolha a data final e clique em <b>Carregar semanal</b>.<br><span class=\"sub\">cada janela custa ${2 + WEEK_LEN} chamadas na fase 1 (visão geral, sessões do intervalo e ${WEEK_LEN} sessões por dia), mais as páginas do detalhe do par de maior custo — o custo da API é por chamada, não por dia.</span></div></div>`}\n\n      <div class=\"${shC(secM, st.mErr)}\"><span class=\"n\">3</span><h2 style=\"margin:0\">Mensal — ${st.mCount} meses de calendário</h2>\n        <span class=\"sub\">de ${MONTHS_MIN} a ${MONTHS_MAX} meses; o mais antigo é a base · recorte da <b>fatura</b> ·\n          o detalhe por recurso fica na seção Semanal</span></div>\n      <div class=\"noprint\">${ctrlMonthly(st)}</div>\n      ${st.mErr ? `<div class=\"err mt\">${esc(st.mErr)}</div>` : ''}\n      ${secM || `<div class=\"card mt noprint\"><div class=\"empty\">escolha o mês final e clique em <b>Carregar mensal</b>.<br><span class=\"sub\">um mês custa 2 chamadas (sessões + visão geral) e nenhuma de detalhe — ${MONTHS_MAX} meses saem bem mais baratos que ${WEEKS_MAX} semanas.</span></div></div>`}\n\n      <div class=\"foot\">\n        <b>Preços</b> da fatura de ago/26 (iguais aos de jul/26), <b>exceto <code>DOMAIN/ai_tokens</code>, que usa o preço acordado de US$ 0,000008 por token</b>\n        — as faturas registram US$ 0,0001, o preço de tabela, 12,5× maior. O custo mostrado aqui é o efetivo; ele\n        <b>não</b> reconcilia com o valor faturado, e a diferença é quase toda essa linha.\n        Os demais preços continuam vindo da fatura e podem ter o mesmo tipo de desconto sem que eu saiba.\n        <code>ID/login</code>, <code>BUILDER/login</code>, <code>ID/sessions</code> e <code>BUILDER/sessions</code> não são faturados\n        por dia (a fatura cobra <code>logged_users</code> por mês). Desde 02/10/26 o nível 1 traz o <b>total</b> de\n        <code>TABLE/storage</code> e de <code>DRIVE/storage</code> (antes era média por recurso): o storage de tabelas entra\n        no custo a US$ 25/GiB-mês, rateado pelos dias do período; ele não tem quebra por tabela na API.\n        Métricas de <b>estoque</b> (<code>storage</code>) são a média dos dias do período, nunca a soma; no consumo\n        por sessão elas entram em GiB·dia (média × dias), como a fatura cobra.\n        <b>Sessões</b>: <b>totais</b> = todas as sessões da plataforma no período; <b>retidas</b> = as resolvidas sem\n        atendimento humano (<code>sessiones_retenidas</code> da fonte de sessões); <b>retenção</b> = retidas ÷ totais;\n        <b>reais</b> = só as das filas que podem falar com a IA, sem as que vão direto ao humano (gerentes de vendas, gerentes de\n        CO/PE/EC e, desde 19/10/26, os da AR; <code>sessoes_reais</code>), e <b>retenção real</b> = reais retidas ÷ reais. No modo\n        retidas, todo número “por sessão” (${MOE.sim}/sessão, consumo por sessão, elasticidade) divide pelas retidas; no universo\n        <b>só filas c/ IA</b>, pelas <b>reais</b> (no modo retidas, pelas <b>reais retidas</b>). O custo é o mesmo em todos os\n        modos de sessão, e o cabeçalho do PDF diz qual modo foi impresso.\n        <b>Moeda</b>: ${MOE.alt ? `valores em <b>R$</b> = o custo modelado em US$ × R$ ${fmtTaxa(MOE.taxa)}/US$, o câmbio digitado nesta\n        execução; ${SCOPES.m.alloc ? 'as alocações de pessoas já nascem em R$ e não são convertidas; ' : ''}os preços unitários acima seguem em US$, a moeda\n        da fatura.` : `valores em <b>US$</b>, a moeda da fatura.<span class=\"noprint\"> Com o câmbio do dia digitado no topo (o campo começa vazio a cada\n        abertura), o seletor <b>MOEDA</b> mostra tudo em R$${SCOPES.m.alloc ? ', e as alocações de pessoas (em R$) entram nos totais; sem ele, elas ficam de fora' : ''}.</span>`}\n      </div>\n    </div>`;\n  },\n\n  mounted(d2) {\n    const api = makeApi(d2);\n    const aborts = {};   // um AbortController por seção, para o cancelar interromper de fato\n    let drillSeq = 0;\n    // UM registro de saída para tudo: abortar as buscas (senão seguiam pagando chamadas e chamando\n    // setState num componente desmontado), soltar o afterprint e remover o CSS de impressão\n    let morto = false;\n    const aoSair = [() => { morto = true; for (const sc in aborts) if (aborts[sc]) aborts[sc].abort(); }];\n    if (typeof d2.onUnmount === 'function') d2.onUnmount(() => aoSair.forEach((f) => { try { f(); } catch (err) { /* segue */ } }));\n    // depois de imprimir, a hora carimbada sai: um Ctrl+P posterior não leva ao papel uma data velha\n    if (typeof window !== 'undefined' && window.addEventListener) {\n      const aposImprimir = () => { restauraTitulo(); if (!morto && d2.state.printedAt) d2.setState({ printedAt: '' }); };\n      window.addEventListener('afterprint', aposImprimir);\n      aoSair.push(() => { window.removeEventListener('afterprint', aposImprimir); restauraTitulo(); });\n    }\n    // o aviso do PDF bloqueado vale para a busca que o bloqueou: sem nada mais em voo, ele sai (senão\n    // voltava em toda carga ou drill seguinte, sem ninguém ter pedido PDF de novo)\n    const fimDeVoo = () => { if (!morto && d2.state.pdfWarn && !emVooDe(viewState(d2))) d2.setState({ pdfWarn: '' }); };\n    // remove as marcas __loading de UMA busca (drill ou fase 2) que foi cancelada ou recarregada\n    function limpaMarcas(sc, tok) {\n      if (morto) return;\n      const cur = d2.state[K(sc, 'Det')] || {};\n      const next = Object.assign({}, cur); let mudou = false;\n      for (const k in next) if (next[k] && next[k].__tok === tok) { delete next[k]; mudou = true; }\n      if (mudou) d2.setState(setScope(sc, { Det: next }));\n      fimDeVoo();\n    }\n    // Cancelar = abort() das chamadas em voo + controller NOVO (senão todo drill seguinte nascia com o\n    // signal abortado) + Run+1 (é o Run+1 que faz os pools pararem de puxar tarefas). As marcas __loading\n    // da seção saem junto: o abort derruba TODAS as buscas dela e as guardas alive/vivo deixam de gravar —\n    // marca que sobrasse (p.ex. de uma desmontagem no meio da carga) prenderia o spinner e o PDF.\n    function cancelar(sc) {\n      if (aborts[sc]) aborts[sc].abort();\n      aborts[sc] = (typeof AbortController !== 'undefined') ? new AbortController() : null;\n      const st = viewState(d2);\n      const det = Object.assign({}, d2.state[K(sc, 'Det')] || {});\n      for (const k in det) if (det[k] && det[k].__loading) delete det[k];\n      // mensal cancelado antes de gravar qualquer mês: o corte congelado na partida (mRef) não vale para nada\n      const nada = sc === 'm' && !Object.keys(st.mAgg).length && !Object.keys(st.mSess).length;\n      d2.setState(setScope(sc, Object.assign({ Run: (st[K(sc, 'Run')] || 0) + 1, Loading: false, Prog: EMPTY_PROG, Det: det },\n        nada ? { Ref: '' } : {})));\n      fimDeVoo();\n    }\n\n    // Regras de impressão que precisam viver no DOCUMENTO, não no shadow root: a altura fixa e o\n    // scroll que cortariam o relatório na primeira página estão nos ANCESTRAIS do componente.\n    // guarda de ambiente: o dry_render roda num DOM simulado onde document.head não existe\n    if (typeof document !== 'undefined' && document.head && document.getElementById\n        && !document.getElementById('pc-print-css')) {\n      const s = document.createElement('style');\n      s.id = 'pc-print-css';\n      s.textContent = '@page{size:A4 landscape;margin:8mm}'\n        + '@media print{html,body{height:auto!important;overflow:visible!important;background:#fff!important}'\n        + '.data2-artboard,.data2-player-block{height:auto!important;max-height:none!important;'\n        + 'overflow:visible!important;position:static!important}}';\n      document.head.appendChild(s);\n      aoSair.push(() => { const el = document.getElementById('pc-print-css'); if (el) el.remove(); });\n    }\n\n    async function loadScope(sc) {\n      const st = viewState(d2);\n      if (st[K(sc, 'Loading')]) return;\n      const runKey = K(sc, 'Run');\n      const run = (st[runKey] || 0) + 1;\n      const P = (o) => {\n        d2.setState(setScope(sc, {\n          Loading: o.loading, Prog: o.prog || EMPTY_PROG,\n          Err: o.err === undefined ? d2.state[K(sc, 'Err')] : o.err\n        }));\n        if (!o.loading) fimDeVoo();\n      };\n      // congela o que esta carga usou: os dias/datas padrão andam com o relógio (00h UTC = 21h em\n      // Brasília) e, sem isso, os períodos mudavam por baixo dos dados já carregados\n      const ref = yesterday();\n      const stC = sc === 'm' ? Object.assign({}, st, { mRef: ref }) : st;\n      const periods = periodsOf(stC, sc);\n      if (sc === 'd') d2.setState({ days: st.days });\n      else if (sc === 'w') d2.setState(setScope('w', { End: st.wEnd }));\n      // mensal: congela o corte da carga (d1 do último mês). Ao fim da fase 1 ele vira o d1 do último mês que\n      // GRAVOU dado, e um cancelar sem nada gravado o apaga: mês nunca carregado segue no ontem de agora\n      else if (sc === 'm') d2.setState(setScope('m', { End: st.mEnd, Ref: periods[periods.length - 1].d1 }));\n      const alive = () => !morto && d2.state[runKey] === run;\n      if (aborts[sc]) aborts[sc].abort();\n      const ac = (typeof AbortController !== 'undefined') ? new AbortController() : null;\n      aborts[sc] = ac;\n      const sig = ac ? ac.signal : undefined;\n\n      d2.setState(setScope(sc, { Run: run, Loading: true, Err: null,\n        Agg: {}, Sess: {}, DaySess: {}, SessRet: {}, DaySessRet: {}, RetErr: [], SessReal: {}, DaySessReal: {}, RealErr: [],\n        SessRealRet: {}, DaySessRealRet: {}, RealRetErr: [], Det: {}, SessErr: [], Prog: { done: 0, total: 0, label: 'fase 1' } }));\n\n      try {\n        // ---- fase 1: sessões + nível 1 por período (+ sessões por dia no semanal) ----\n        const jobs = [];\n        periods.forEach((per) => {\n          jobs.push({ kind: 's', per });\n          jobs.push({ kind: 'o', per });\n        });\n        const allDays = SCOPES[sc].strip ? periods.reduce((a, p) => a.concat(p.days), []) : [];\n        allDays.forEach((day) => jobs.push({ kind: 'ds', day }));\n        P({ loading: true, prog: { done: 0, total: jobs.length, label: 'fase 1: sessões e visão geral' } });\n\n        const res = await pool(jobs.map((j) => () => (\n          j.kind === 's' ? api.sessions(j.per.d0, j.per.d1, sig)\n            : j.kind === 'o' ? api.overview(j.per.d0, j.per.d1, sig)\n              : api.sessions(j.day, j.day, sig)\n        )), POOL, (done) => { if (alive()) P({ loading: true, prog: { done, total: jobs.length, label: 'fase 1: sessões e visão geral' } }); }, () => !alive());\n        if (!alive()) return;\n\n        const sess = {}, agg = {}, daySess = {}, errs = [], sessErr = [], sessRet = {}, daySessRet = {}, retErr = [];\n        const sessReal = {}, daySessReal = {}, realErr = [], sessRealRet = {}, daySessRealRet = {}, realRetErr = [];\n        // campos OPCIONAIS da fonte de sessões, todos pelo mesmo leitor estrito e independente\n        const opcionais = [\n          { campo: 'sessiones_retenidas', nome: 'sessões retidas', porPer: sessRet, porDia: daySessRet, err: retErr },\n          { campo: 'sessoes_reais', nome: 'sessões reais', porPer: sessReal, porDia: daySessReal, err: realErr },\n          { campo: 'sessoes_reais_retenidas', nome: 'sessões reais retidas', porPer: sessRealRet, porDia: daySessRealRet, err: realRetErr }];\n        jobs.forEach((j, i) => {\n          const r = res[i];\n          if (!r) return;\n          const lab = j.per ? j.per.lab : shortDay(j.day);\n          // sessões do PERÍODO que falharam: o aviso da seção aponta a faixa vermelha só para esses\n          if (r.__err) { errs.push({ lab, msg: String(r.__err) }); if (j.kind === 's') sessErr.push(j.per.k); return; }\n          if (j.kind === 'o') {\n            // nível 1 fora do formato (corpo de erro do upstream repassado como rows), vazio ou sem nenhuma\n            // linha válida: erro do período, nunca um período \"carregado\" com custo zero\n            if (!Array.isArray(r.rows)) { errs.push({ lab, msg: 'visão geral sem lista de linhas (resposta do upstream fora do formato)' }); return; }\n            if (!r.rows.length) {\n              errs.push({ lab, msg: j.per.days.length === 1 ? 'visão geral vazia — o dia ainda não foi processado no warehouse?'\n                : 'visão geral vazia — nenhum consumo registrado no período (projeto sem histórico até aqui, ou dados ainda não processados)' });\n              return;\n            }\n            // valor não numérico numa linha válida: erro do período (o num() o transformaria em 0 em silêncio)\n            const ruins = r.rows.filter((x) => x && x.product && x.measure_name && !Number.isFinite(numEstrito(x.sum_measure_value)));\n            if (ruins.length) {\n              errs.push({ lab, msg: 'visão geral com valor não numérico (' + fx(ruins.length, 0) + ' linha' + (ruins.length > 1 ? 's' : '')\n                + ', ex.: ' + ruins[0].product + '|' + ruins[0].measure_name + ' = ' + mostraValor(ruins[0].sum_measure_value) + ')' });\n              return;\n            }\n            const a1 = aggLevel1(r.rows);\n            if (!Object.keys(a1).length) {\n              errs.push({ lab, msg: 'visão geral sem linhas válidas (' + fx(r.rows.length, 0) + ' linha' + (r.rows.length > 1 ? 's' : '') + ' sem product/measure_name)' });\n              return;\n            }\n            agg[j.per.k] = a1;\n            return;\n          }\n          // RETIDAS, REAIS e REAIS RETIDAS: lidas ANTES do total e cada uma por conta própria — valor ruim vai à faixa de\n          // erro mas nunca derruba o total (o modo totais segue válido); ausente fica ausente, e o modo que o usa avisa onde\n          for (const o of opcionais) {\n            const rr = r[o.campo];\n            if (rr === null || rr === undefined) continue;\n            const rn = typeof rr === 'number' ? rr : (typeof rr === 'string' && rr.trim() !== '' ? Number(rr) : NaN);\n            if (Number.isFinite(rn) && rn >= 0) {\n              const v = rn === 0 ? 0 : rn;   // -0 do JSON vira 0 (senão imprimia \"-0\")\n              if (j.kind === 's') o.porPer[j.per.k] = v; else o.porDia[j.day] = v;\n            } else {\n              errs.push({ lab, msg: o.nome + ' não numéricas na resposta: ' + mostraValor(rr)\n                + (Number.isFinite(numEstrito(r.sessions)) ? ' (o total segue valendo)' : '') });\n              if (j.kind === 's') o.err.push(j.per.k);\n            }\n          }\n          // sessão ausente fica AUSENTE (undefined), nunca 0: com 0 o classify inventava −100%. E só\n          // número conta: texto, objeto ou '' viravam 0 em silêncio pelo num()\n          const raw = r.sessions;\n          const nn = typeof raw === 'number' ? raw : (typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : NaN);\n          const sv = (raw === null || raw === undefined) ? undefined : (Number.isFinite(nn) ? nn : undefined);\n          if (raw !== null && raw !== undefined && sv === undefined) {\n            errs.push({ lab, msg: 'sessões não numéricas na resposta: ' + String(JSON.stringify(raw)).slice(0, 60) });\n            if (j.kind === 's') sessErr.push(j.per.k);\n            return;\n          }\n          if (j.kind === 's') { if (sv !== undefined) sess[j.per.k] = sv; }\n          else if (sv !== undefined) daySess[j.day] = sv;\n        });\n        // 27 chamadas falhando pelo mesmo motivo são UM problema, não 27 — agrupa por mensagem para\n        // a faixa de erro dizer a causa uma vez e listar quais períodos ela atingiu. O texto do upstream\n        // (\" Resposta do listener: …\") pode mudar a cada chamada (id, hora): agrupa SEM ele e mostra as\n        // respostas distintas uma vez só, no máximo 3.\n        const SUF = ' Resposta do listener: ';\n        const porMsg = {};\n        errs.forEach((e) => {\n          const i = e.msg.indexOf(SUF);\n          const m = i < 0 ? e.msg : e.msg.slice(0, i), resp = i < 0 ? '' : e.msg.slice(i + SUF.length);\n          const g = porMsg[m] = porMsg[m] || { labs: [], resps: [] };\n          g.labs.push(e.lab);\n          if (resp && g.resps.indexOf(resp) < 0) g.resps.push(resp);\n        });\n        const resumoErr = Object.keys(porMsg).map((m) => {\n          const ls = porMsg[m].labs, rs = porMsg[m].resps;\n          // o mesmo período aparece uma vez só (sessões e visão geral do mesmo dia falham juntas); o \"N×\" segue\n          // contando as chamadas\n          const us = ls.filter((x, j) => ls.indexOf(x) === j);\n          const quem = us.length > 4 ? us.slice(0, 4).join(', ') + ' e mais ' + (us.length - 4) : us.join(', ');\n          const mais = rs.length - 3;\n          const resp = !rs.length ? '' : (rs.length > 1 ? ' Respostas do listener: ' : SUF) + rs.slice(0, 3).join(' / ')\n            + (mais > 0 ? ' (e mais ' + mais + (mais === 1 ? ' resposta distinta)' : ' respostas distintas)') : '');\n          return (ls.length > 1 ? ls.length + '× (' + quem + ') — ' : quem + ' — ') + m + resp;\n        }).join('  |  ');\n        const errFinal = errs.length ? resumoErr : null;\n        // mensal: o corte congelado passa a ser o d1 do ÚLTIMO MÊS QUE GRAVOU dado (o da partida valeria para um\n        // mês cujas chamadas falharam); sem nenhum mês gravado, nenhum corte\n        const okM = sc === 'm' ? periods.filter((q) => agg[q.k] || sess[q.k] !== undefined) : null;\n        d2.setState(setScope(sc, Object.assign({ Sess: sess, Agg: agg, DaySess: daySess, SessRet: sessRet, DaySessRet: daySessRet, RetErr: retErr,\n          SessReal: sessReal, DaySessReal: daySessReal, RealErr: realErr, SessRealRet: sessRealRet, DaySessRealRet: daySessRealRet,\n          RealRetErr: realRetErr, Err: errFinal, SessErr: sessErr },\n          okM ? { Ref: okM.length ? okM[okM.length - 1].d1 : '' } : {})));\n\n        // ---- fase 2: detalhe antecipado dos pares que mais pesam ----\n        const eager = pickEager({ agg, sess, det: {} }, periods, SCOPES[sc].topN);\n        if (!eager.length) { P({ loading: false, prog: EMPTY_PROG }); return; }\n        const t2 = [];\n        // só períodos com nível 1: o detalhe de um período sem nível 1 não fecha com o KPI e seria pago à toa\n        eager.forEach(({ p, e }) => periods.forEach((per) => { if (agg[per.k]) t2.push({ per, p, e }); }));\n        if (!t2.length) { P({ loading: false, prog: EMPTY_PROG }); return; }\n        // marca __loading: sem isso a linha do par pré-carregado mostrava o botão de drill durante a fase 2\n        // e o clique buscava de novo as mesmas páginas\n        const tok2 = 'e' + run;\n        const marcas = {};\n        t2.forEach((j) => { marcas[j.per.k + '|' + j.p + '|' + j.e] = { __loading: true, __tok: tok2 }; });\n        d2.setState(setScope(sc, { Det: Object.assign({}, d2.state[K(sc, 'Det')] || {}, marcas) }));\n        let done = 0;\n        const lab2 = 'fase 2: detalhe de ' + eager.length + ' par' + (eager.length > 1 ? 'es' : '')\n          + ' — os demais são sob demanda';\n        P({ loading: true, prog: { done: 0, total: t2.length, label: lab2 } });\n        await pool(t2.map((j) => async () => {\n          const dk = j.per.k + '|' + j.p + '|' + j.e;\n          let out;\n          try { out = await fetchDetail(api, j.per, j.p, j.e, SCOPES[sc].maxPages, sig); }\n          catch (err) { out = { __err: (err && err.message) || String(err) }; }\n          if (!alive()) return;\n          const cur = d2.state[K(sc, 'Det')] || {};\n          d2.setState(setScope(sc, { Det: Object.assign({}, cur, { [dk]: out }) }));\n        }), POOL, () => { done++; if (alive()) P({ loading: true, prog: { done, total: t2.length, label: lab2 } }); }, () => !alive());\n        if (!alive()) { limpaMarcas(sc, tok2); return; }\n        P({ loading: false, prog: EMPTY_PROG });\n      } catch (err) {\n        limpaMarcas(sc, 'e' + run);\n        if (alive()) P({ loading: false, prog: EMPTY_PROG, err: (err && err.message) || String(err) });\n      }\n    }\n\n    async function drill(sc, p, e) {\n      const st = viewState(d2);\n      const periods = periodsOf(st, sc);\n      const runKey = K(sc, 'Run');\n      const run = d2.state[runKey] || 0;\n      // o signal é lido UMA vez: relê-lo por tarefa fazia a tarefa que esperava na fila sair com o\n      // controller novo depois de um cancelar\n      if (!aborts[sc] && typeof AbortController !== 'undefined') aborts[sc] = new AbortController();\n      const sig = aborts[sc] ? aborts[sc].signal : undefined;\n      const tok = 'dr' + (++drillSeq);   // marca as entradas __loading DESTE drill\n      const vivo = () => !morto && (d2.state[runKey] || 0) === run;\n      const aggSc = st[K(sc, 'Agg')] || {};\n      // período sem nível 1: o detalhe não fecha com o KPI — a linha pede para carregar antes\n      if (periods.some((per) => !aggSc[per.k] || !Object.keys(aggSc[per.k]).length)) return;\n      // só busca o que falta, falhou ou veio vazio com nível 1 > 0 (∅): nunca refaz período completo nem duplica\n      // uma busca em voo (a fase 2 marca __loading), e o \"tentar de novo\" repete só esses\n      const base = d2.state[K(sc, 'Det')] || {};\n      const vazio = (per) => !!valueOf({ agg: aggSc, det: base }, per, p, e).vazio;\n      const todo = periods.filter((per) => { const x = base[per.k + '|' + p + '|' + e];\n        return !x || x.__err || (!x.__loading && vazio(per)); });\n      if (!todo.length) return;\n      const mark = {};\n      todo.forEach((per) => { mark[per.k + '|' + p + '|' + e] = { __loading: true, __tok: tok }; });\n      d2.setState(setScope(sc, { Det: Object.assign({}, base, mark) }));\n      await pool(todo.map((per) => async () => {\n        let out;\n        try { out = await fetchDetail(api, per, p, e, SCOPES[sc].maxPages, sig); }\n        catch (err) { out = { __err: (err && err.message) || String(err) }; }\n        if (!vivo()) return;   // cancelado ou recarregado: não grava (limpeza logo abaixo)\n        const cur = d2.state[K(sc, 'Det')] || {};\n        d2.setState(setScope(sc, { Det: Object.assign({}, cur, { [per.k + '|' + p + '|' + e]: out }) }));\n      }), POOL, null, () => !vivo());\n      // cancelado ou recarregado: a guarda vivo() não grava nada, então este drill apaga as próprias marcas —\n      // sem isso o spinner ficaria preso e o PDF bloqueado\n      if (!vivo()) limpaMarcas(sc, tok);\n      else fimDeVoo();\n    }\n\n    // login do artboard (ver acessoDe): a instância crua da auth, porque o login do runtime espera { credentials }\n    d2.on('form[data-login]', 'submit', async (ev, el) => {\n      if (ev && ev.preventDefault) ev.preventDefault();\n      const f = el || (ev && ev.target && ev.target.closest ? ev.target.closest('form') : null);\n      const a = authDe(d2);\n      if (!f || !a || d2.state.lgBusy) return;\n      const email = String(f.querySelector('[name=email]').value || '').trim();\n      const password = String(f.querySelector('[name=password]').value || '');\n      if (!email || !password) return void d2.setState({ lgErr: 'Preencha e-mail e senha.' });\n      d2.setState({ lgBusy: true, lgErr: '' });\n      let falhou = false;\n      try {\n        const alvo = a.raw && typeof a.raw.login === 'function' ? a.raw : a;\n        const r = await alvo.login({ credentials: { email, password } });\n        if (r && (r.isData2Error || r.error)) falhou = true;\n      } catch (e) { falhou = true; }\n      const u = a.user;\n      d2.setState({ lgBusy: false, lgErr: (falhou || !u || !u.email) ? 'E-mail ou senha inválidos.'\n        : (String(u.role || '') !== 'admin' ? 'Conta sem permissão de acesso (role diferente de admin).' : '') });\n    });\n    d2.on('[data-act]', 'click', (ev, el) => {\n      const t = el || (ev.target && ev.target.closest && ev.target.closest('[data-act]'));\n      if (!t) return;\n      const act = t.dataset.act, st = viewState(d2);\n      if (act === 'loadD') { if (st.days.length < MIN_DAYS) return; return void loadScope('d'); }\n      if (act === 'loadW') return void loadScope('w');\n      if (act === 'loadM') return void loadScope('m');\n      if (act === 'cancelD') return void cancelar('d');\n      if (act === 'cancelW') return void cancelar('w');\n      if (act === 'cancelM') return void cancelar('m');\n      if (act === 'pdf') {\n        // Nunca imprimir com busca em voo: o estado de carregamento de um drill é renderizado DENTRO\n        // da tabela (fora do .noprint), então o PDF sairia com 'buscando o detalhe por recurso…'\n        // impresso no lugar dos números — e este relatório vai para fora.\n        if (emVooDe(st)) return void d2.setState({ pdfWarn: 1 });\n        // carimba a hora, deixa o render sair, e só então abre o diálogo — senão o cabeçalho de\n        // impressão iria ao papel sem a data, porque setState re-renderiza em lote.\n        d2.setState({ printedAt: new Date().toLocaleString('pt-BR'), pdfWarn: '' });\n        // o nome sugerido do arquivo sai do título da página (Chrome): troca aqui e volta no afterprint\n        const nome = nomePdf(st);\n        setTimeout(() => {\n          trocaTitulo(nome);\n          try { window.print(); } catch (err) { restauraTitulo(); d2.log('print falhou', err); }\n        }, 150);\n        return;\n      }\n      if (act === 'theme') return void d2.setState({ theme: st.theme === 'dark' ? 'light' : 'dark' });\n      if (act === 'sort') return void d2.setState({ sort: t.dataset.v });\n      // troca só o divisor: as duas contagens já vieram na mesma resposta, então nenhuma chamada sai daqui\n      if (act === 'den') return void d2.setState({ den: t.dataset.v === 'retidas' ? 'retidas' : 'total' });\n      if (act === 'uni') return void d2.setState({ uni: t.dataset.v === 'reais' ? 'reais' : 'todas' });\n      // troca só a moeda de EXIBIÇÃO: a conta é sempre em US$, então nenhuma chamada sai daqui\n      if (act === 'moeda') return void d2.setState({ moeda: t.dataset.v === 'brl' ? 'brl' : 'usd' });\n      if (act === 'addweek') {\n        return void (st.wCount >= WEEKS_MAX\n          ? d2.setState({ wWarn: 'max' })\n          : d2.setState({ wCount: st.wCount + 1, wWarn: '' }));\n      }\n      if (act === 'rmweek') {\n        return void (st.wCount <= WEEKS_MIN\n          ? d2.setState({ wWarn: 'min' })\n          : d2.setState({ wCount: st.wCount - 1, wWarn: '' }));\n      }\n      if (act === 'addmonth') {\n        return void (st.mCount >= MONTHS_MAX\n          ? d2.setState({ mWarn: 'max' })\n          : d2.setState({ mCount: st.mCount + 1, mWarn: '' }));\n      }\n      if (act === 'rmmonth') {\n        return void (st.mCount <= MONTHS_MIN\n          ? d2.setState({ mWarn: 'min' })\n          : d2.setState({ mCount: st.mCount - 1, mWarn: '' }));\n      }\n      if (act === 'allocinc' || act === 'allocdec') {\n        const kk = t.dataset.k;\n        // a chave do período mensal é 'M' + d0, então slice(1) devolve a data do início do mês\n        const atual = st.mAlloc[kk] === undefined ? allocDefault(kk.slice(1)) : st.mAlloc[kk];\n        const novo = Math.max(0, Math.min(ALLOC_MAX, atual + (act === 'allocinc' ? 1 : -1)));\n        return void d2.setState({ mAlloc: Object.assign({}, st.mAlloc, { [kk]: novo }) });\n      }\n      if (act === 'addday') {\n        if (st.days.length >= MAX_DAYS) return;\n        // o dia livre mais recente <= ontem (antes duplicava ontem no estado padrão)\n        let nd = yesterday();\n        while (st.days.includes(nd)) nd = addDays(nd, -1);\n        return void d2.setState({ days: normDays(st.days.concat([nd])) });\n      }\n      if (act === 'rmday') return void d2.setState({ days: st.days.filter((_, k) => k !== Number(t.dataset.i)) });\n      if (act === 'toggle') {\n        const sc = t.dataset.scope, k = t.dataset.k;\n        const cur = st[K(sc, 'Open')] || {};\n        const next = Object.assign({}, cur); next[k] = !cur[k];\n        return void d2.setState(setScope(sc, { Open: next }));\n      }\n      if (act === 'drill') return void drill(t.dataset.scope, t.dataset.p, t.dataset.e);\n    });\n\n    d2.on('[data-act]', 'change', (ev, el) => {\n      const t = el || (ev.target && ev.target.closest && ev.target.closest('[data-act]'));\n      if (!t) return;\n      const st = viewState(d2);\n      if (t.dataset.act === 'setday') {\n        if (!t.value || t.value > yesterday()) return;\n        const days = st.days.slice(); days[Number(t.dataset.i)] = t.value;\n        d2.setState({ days: normDays(days) });   // ordenado e sem repetição: o mais antigo é a base\n      }\n      // data digitada além do max do input é presa a ontem (o max só limita o seletor, não o que se digita)\n      if (t.dataset.act === 'setwend' && t.value) d2.setState({ wEnd: t.value > yesterday() ? yesterday() : t.value, wWarn: '' });\n      if (t.dataset.act === 'setmend' && t.value) d2.setState({ mEnd: t.value > yesterday() ? yesterday() : t.value, mWarn: '' });\n      if (t.dataset.act === 'allocset') {\n        const n = Math.max(0, Math.min(ALLOC_MAX, Math.round(Number(t.value) || 0)));\n        d2.setState({ mAlloc: Object.assign({}, st.mAlloc, { [t.dataset.k]: n }) });\n      }\n      if (t.dataset.act === 'setfx') {\n        // vírgula ou ponto, de FX_MIN a FX_MAX, até 4 casas. Apagar = sem câmbio (volta a só US$ e as alocações saem dos\n        // totais); valor inválido NÃO troca a taxa: marca o erro (borda + nota abaixo da barra) e mantém a anterior\n        const bruto = String(t.value === null || t.value === undefined ? '' : t.value).trim();\n        const v = numBr(bruto.replace(/^R\\$/i, ''));   // a cotação colada de um site vem com o símbolo: \"R$ 5,43\"\n        if (bruto === '') d2.setState({ fx: null, fxErr: '' });\n        else if (fxOk(v)) {\n          const taxa = Math.round(v * 1e4) / 1e4;\n          // piso pedido em R$ antes do câmbio: vale agora, convertido\n          d2.setState(Object.assign({ fx: taxa, fxErr: '' }, st.pisoPend ? { minCost: st.pisoPend.v / taxa, pisoPend: null } : {}));\n        }\n        else d2.setState({ fxErr: bruto.slice(0, 40) });\n      }\n      // o piso mora em US$ (a unidade de sortRows): digitado em R$, divide pelo câmbio\n      if (t.dataset.act === 'mincost') {\n        const cru = String(t.value === null || t.value === undefined ? '' : t.value).replace(/\\s/g, '');\n        const sim = (cru.match(/^(?:R\\$|US\\$|\\$)/i) || [''])[0].toUpperCase(), bruto = cru.slice(sim.length);\n        // \"1.000\" é mil (a própria tela escreve R$ 26.000); \"0.005\" segue 0,005\n        const v = Math.max(0, numBr(/^[1-9]\\d{0,2}(\\.\\d{3})+$/.test(bruto) ? bruto.replace(/\\./g, '') : bruto) || 0);\n        // o símbolo digitado manda (\"R$ 500\" com a tela em US$ = 500 ÷ câmbio); sem símbolo, vale a moeda da tela.\n        // Valor em R$ sem câmbio digitado não tem conversão: o piso fica como estava, mas o pedido não some calado —\n        // fica no campo (borda vermelha + title) e passa a valer sozinho quando o câmbio for aceito\n        const emReais = sim === 'R$' || (sim !== 'US$' && st.moeda === 'brl');\n        const pend = emReais && v > 0 && !st.fxSet;\n        d2.setState({ minCost: !emReais || v === 0 ? v : st.fxSet ? v / st.fx : st.minCost,\n          pisoPend: pend ? { txt: String(t.value).trim().slice(0, 40), v } : null });\n      }\n    });\n  }\n};\n/*C1*/\n\n\n\n\n\n"}},"listenerRef":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"listenerRef"}}}},"defaultDays":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"defaultDays"}}}},"auth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"auth"}}}}}}}}}},{"_id":"6a8c5b7905e12b46422da308","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"ZZProbe","package":"lucas-playground","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: `.wrap{display:flex;flex-direction:column;gap:12px;align-items:flex-start;justify-content:center;height:100%;padding:32px;font:16px/1.5 system-ui,sans-serif;background:#0f172a;color:#e2e8f0}\n        .t{font-size:28px;font-weight:700;color:#38bdf8}\n        .b{padding:8px 16px;border:0;border-radius:8px;background:#38bdf8;color:#0f172a;font-weight:600;cursor:pointer}`,\n  render(d2){\n    const n = d2.state.n || 0\n    return `<div class=\"wrap\" data-loaded>\n      <div class=\"t\">probe ok</div>\n      <div>etiqueta: ${String(d2.param('etiqueta','(sem valor)'))}</div>\n      <div>contador: <b class=\"n\">${n}</b></div>\n      <button class=\"b\">somar</button>\n    </div>`\n  },\n  mounted(d2){\n    d2.on('.b','click',()=>{ d2.state.n = (d2.state.n||0)+1 })\n  },\n}\n"}},"etiqueta":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"etiqueta"}}}}}}}}}},{"_id":"6a8c5c542fc601cd4a2c374c","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"FaqOperadorV2","package":"lucas-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// FaqOperadorV2 — NASVI operator FAQ copilot, single code component.\n// demo:true  -> embedded mock data (playground validation, no backend, no AI cost)\n// demo:false -> real path: d2.data('faqs').filter+refresh, then d2.assistant().ask({context})\n\nconst MOCK_USER = { name: 'Ana Gomez', email: 'ana.gomez@natura.net', countries: ['AR', 'MX'], user_type: ['Standard'], role: 'agent' }\nconst MOCK_REGIONS = ['AR', 'MX', 'CO', 'PE', 'CL', 'EC']\nconst MOCK_FAQS = [\n  { _id: 'm1', title: 'Puntos de la consultora no actualizados', article: '10231', d2_search_score: 0.87, category: 'MODELO_COMERCIAL', content: 'Si la consultora tiene apenas 1 punto, debe actualizar su registro. Esto ocurre por alguna inconsistencia en su dirección; después de actualizar sus datos sus puntos deberían restablecerse en hasta 24 horas.' },\n  { _id: 'm2', title: 'Pedido retenido por límite de crédito', article: '10884', d2_search_score: 0.74, category: 'PEDIDOS', content: 'Si el pedido queda retenido por exceder el límite de crédito, la consultora deberá ajustar el puntaje del pedido según el crédito disponible o pagar pedidos pendientes para liberar crédito.' },\n  { _id: 'm3', title: 'Unificación ELO Natura y Avon', article: '11002', d2_search_score: 0.66, category: 'MODELO_COMERCIAL', content: 'Con el ELO México, Natura y Avon se unificaron. Los ciclos, el catálogo y la tabla de puntos pasan a ser únicos. Ante conflicto de información, siempre prevalece el contenido que habla específicamente del ELO.' },\n  { _id: 'm4', title: 'Dejar de vender / baja de consultora', article: '10077', d2_search_score: 0.52, category: 'MODELO_COMERCIAL', content: 'Para darse de baja como Consultora de Belleza Natura debe enviar un correo a atencion_consultor@natura.net adjuntando identificación oficial vigente por ambos lados y una carta con el motivo.' },\n  { _id: 'm5', title: 'Plazos de entrega por región', article: '10455', d2_search_score: 0.41, category: 'PEDIDOS', content: 'Los plazos de entrega varían por región. Capital y GBA: 3 a 5 días hábiles. Interior: 5 a 8 días hábiles contados desde la facturación del pedido.' }\n]\nconst MOCK_HISTORY = [\n  { _id: 'h1', pergunta: '¿Cómo actualizo la dirección de una consultora?', resposta: 'Querido Asesor, para actualizar la dirección ingresá en el panel de registro, sección *Datos personales*, y editá el campo Domicilio. El cambio impacta el próximo ciclo.', createdAt: Date.now() - 1000 * 60 * 18 },\n  { _id: 'h2', pergunta: '¿Cuántos días demora un reembolso?', resposta: 'Estimado asesor, el reembolso se procesa en hasta **10 días hábiles** después de aprobada la solicitud.', createdAt: Date.now() - 1000 * 60 * 95 },\n  { _id: 'h3', pergunta: '¿Qué pasa si el pedido llega incompleto?', resposta: 'Querido Asesor, se debe abrir un reclamo de falta de producto adjuntando el número de pedido y la foto del remito.', createdAt: Date.now() - 1000 * 60 * 60 * 26 }\n]\nconst MOCK_ANSWER = 'Querido Asesor,\\n\\nSi la consultora visualiza **apenas 1 punto**, casi siempre se debe a una **inconsistencia en su dirección**.\\n\\nPasos a seguir:\\n\\n1. Pedíle que ingrese a *Mi registro* y revise el domicilio completo.\\n2. Que corrija calle, número y código postal.\\n3. Guardar los cambios.\\n\\nDespués de actualizar los datos, **sus puntos deberían restablecerse** automáticamente.\\n\\n> Si pasadas 24 horas el puntaje sigue igual, abrí un ticket para el área comercial.'\n\nconst HELP_MD = 'Soy **NASVI**\\n\\nTu nuevo **copiloto virtual**, listo para acompañarte en cada contacto con nuestras consultoras.\\n\\nEstoy aquí para hacer tu trabajo **más ágil, más seguro y más claro**.\\n\\n¿Cómo lo hago? Muy fácil:\\n\\n- Estoy entrenado con los procesos, protocolos y wikis actualizados de Natura y Avon.\\n- Cuando tengas una duda, yo te doy la respuesta en segundos, sin detener tu atención.\\n- Además, te llevo directo a la fuente oficial en Beedoo, para que siempre tengas información validada.\\n\\nMi misión es que puedas resolver más consultas en el primer contacto, sin errores, y con total confianza.'\n\nconst VOTE = {\n  1: ['No encontré nada útil', 'Sin respuesta o sin relevancia para la consulta'],\n  2: ['Encontré muy poca información', 'Respuesta incompleta o poco relevante'],\n  3: ['Encontré parte de la información', 'Resolvió parcialmente, se requiere buscar más'],\n  4: ['Encontré casi todo lo que buscaba', 'Útil en gran parte, pero con pequeños vacíos'],\n  5: ['Encontré exactamente lo que necesitaba', 'Respuesta clara, completa y satisfactoria']\n}\nconst ORIGENES = [['chat', 'Chat'], ['voz', 'Voz'], ['video', 'Video']]\nconst BEEDOO = { MX: 'https://naturamexico.beedoo.io/wiki/' }\nconst BEEDOO_DEFAULT = 'https://naturalatam.beedoo.io/wiki/'\n\nfunction esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') }\nfunction attr(s) { return esc(s).replace(/\"/g, '&quot;') }\nfunction isDemo(d2) { return d2.param('demo') === true }\n\nfunction user(d2) {\n  if (isDemo(d2)) return MOCK_USER\n  const ds = d2.data('usuarios')\n  return (ds && ds.selected()) || {}\n}\nfunction isEcom(u) { return [].concat(u.user_type || []).indexOf('E-commerce') >= 0 }\nfunction solo(u) { return (u.countries || []).length === 1 }\nfunction regionCodes(d2) {\n  if (isDemo(d2)) return MOCK_REGIONS\n  const ds = d2.data('regiones')\n  return ds ? ds.records().map(function (r) { return r.country_code }).filter(Boolean) : []\n}\nfunction marketOptions(d2, u) { return isEcom(u) ? regionCodes(d2) : (u.countries || []) }\nfunction showSelector(d2, u) { return isEcom(u) || !solo(u) }\nfunction market(d2, u) {\n  if (!isEcom(u) && solo(u)) return u.countries[0]\n  return d2.state.region || marketOptions(d2, u)[0] || ''\n}\nfunction hits(d2) {\n  if (isDemo(d2)) return d2.state.hits || []\n  const ds = d2.data('faqs')\n  return ds ? ds.records() : []\n}\nfunction answerText(d2) {\n  if (isDemo(d2)) return d2.state.answer || ''\n  const a = d2.assistant(d2.params.assistant)\n  return (a && a.answer) || ''\n}\nfunction busy(d2) {\n  if (isDemo(d2)) return !!d2.state.busy\n  const a = d2.assistant(d2.params.assistant)\n  return !!(a && a.busy)\n}\nfunction history(d2) {\n  if (isDemo(d2)) return MOCK_HISTORY\n  const ds = d2.data('historial')\n  return ds ? ds.records() : []\n}\nfunction beedooUrl(mk, article) { return (BEEDOO[mk] || BEEDOO_DEFAULT) + String(article || '') + '/lll' }\n\nfunction card(d2, r, i) {\n  const score = typeof r.d2_search_score === 'number' ? (r.d2_search_score * 10).toFixed(1) : ''\n  const mk = market(d2, user(d2))\n  return '<article class=\"src\" data-key=\"' + attr(r._id || i) + '\">'\n    + '<header class=\"src-h\"><h3 class=\"src-t\">' + esc(r.title) + '</h3>'\n    + (score ? '<span class=\"src-s\">' + esc(score) + '</span>' : '') + '</header>'\n    + (r.category ? '<span class=\"src-c\">' + esc(r.category) + '</span>' : '')\n    + '<p class=\"src-b\">' + esc(r.content) + '</p>'\n    + (r.article ? '<a class=\"src-l\" href=\"' + attr(beedooUrl(mk, r.article)) + '\" target=\"_blank\" rel=\"noopener\" data-article=\"' + attr(r.article) + '\">contenido en Beedoo</a>' : '')\n    + '</article>'\n}\n\nexport default {\n  css: '*,*::before,*::after{box-sizing:border-box}'\n    + ':host{all:initial}'\n    + '.app{--cream:rgb(254,251,244);--ink:#162b37;--ink2:rgba(22,43,55,.62);--line:#e3ddd2;--olive:rgb(230,238,189);--ok:rgb(125,230,28);--off:rgb(200,46,0);--accent:var(--acc,#FF6C0E);display:grid;grid-template-rows:auto 1fr auto;height:100%;width:100%;background:var(--cream);color:var(--ink);font:14px/1.55 \"Open Sans\",system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif;position:relative;overflow:hidden}'\n    + '.hd{display:flex;align-items:center;gap:18px;padding:14px 22px;border-bottom:1px solid var(--line);background:#fff}'\n    + '.brand{display:flex;flex-direction:column;line-height:1.15;margin-right:6px}'\n    + '.brand b{font-size:22px;font-weight:700;letter-spacing:.5px}'\n    + '.brand span{font-size:12px;color:var(--ink2)}'\n    + '.chips{display:flex;gap:6px}'\n    + '.chip{flex:none;border:0;border-radius:16px;padding:6px 14px;font:600 12px/1 inherit;color:#fff;background:var(--off);cursor:pointer;opacity:.55;transition:opacity .15s}'\n    + '.chip[aria-pressed=\"true\"]{background:var(--ok);color:var(--ink);opacity:1}'\n    + '.spacer{flex:1}'\n    + '.mk{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ink2)}'\n    + '.mk select{font:inherit;padding:5px 8px;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--ink)}'\n    + '.lock{flex:none;border:1px solid var(--line);border-radius:7px;padding:5px 10px;font-weight:700;background:#faf6ef}'\n    + '.who{font-size:12px;color:var(--ink2);text-align:right;line-height:1.3}'\n    + '.who b{display:block;color:var(--ink);font-size:13px}'\n    + '.ico{flex:none;width:34px;height:34px;border-radius:50%;border:1px solid var(--line);background:#fff;color:var(--ink);cursor:pointer;font:700 15px/1 inherit;display:grid;place-items:center}'\n    + '.ico:hover{border-color:var(--accent);color:var(--accent)}'\n    + '.main{display:grid;grid-template-columns:1fr 372px;min-height:0}'\n    + '.left{display:flex;flex-direction:column;min-height:0;padding:20px 22px;gap:14px}'\n    + '.ask label{display:block;font-weight:600;margin-bottom:6px}'\n    + '.q{width:100%;height:96px;resize:none;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#fff;color:var(--ink);font:inherit}'\n    + '.q:focus{outline:2px solid var(--accent);outline-offset:-1px}'\n    + '.ask-r{display:flex;align-items:center;gap:12px;margin-top:8px}'\n    + '.hint{font-size:12px;color:var(--ink2)}'\n    + '.send{flex:none;margin-left:auto;border:0;border-radius:8px;padding:10px 22px;background:var(--ink);color:#fff;font:600 14px/1 inherit;cursor:pointer}'\n    + '.send:disabled{opacity:.4;cursor:default}'\n    + '.log{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:12px;padding-right:4px}'\n    + '.you{align-self:flex-end;max-width:82%;background:#fff;border:1px solid var(--line);border-radius:12px 12px 2px 12px;padding:10px 14px}'\n    + '.bot{display:flex;gap:10px;max-width:94%}'\n    + '.av{width:34px;height:34px;flex:none;border-radius:50%;background:var(--ink);color:#fff;display:grid;place-items:center;font:700 13px/1 inherit}'\n    + '.bub{background:var(--olive);border-radius:2px 16px 16px 16px;padding:12px 16px;min-width:0}'\n    + '.bub p{margin:0 0 8px}.bub p:last-child{margin:0}.bub ul,.bub ol{margin:6px 0;padding-left:20px}'\n    + '.bub h1,.bub h2,.bub h3{font-size:15px;margin:8px 0 4px}.bub code{background:rgba(0,0,0,.07);padding:1px 5px;border-radius:4px}'\n    + '.bub blockquote{margin:8px 0;padding-left:10px;border-left:3px solid rgba(22,43,55,.25);color:var(--ink2)}'\n    + '.cur{display:inline-block;width:7px;height:15px;background:var(--ink);vertical-align:-2px;animation:bl .9s steps(2) infinite}'\n    + '@keyframes bl{50%{opacity:0}}'\n    + '.acts{display:flex;gap:8px;margin-top:10px}'\n    + '.act{border:1px solid rgba(22,43,55,.22);background:rgba(255,255,255,.6);border-radius:7px;padding:5px 11px;font:600 12px/1.4 inherit;color:var(--ink);cursor:pointer}'\n    + '.act:hover{border-color:var(--ink)}'\n    + '.empty{margin:auto;text-align:center;color:var(--ink2);max-width:300px;font-size:13px}'\n    + '.empty b{display:block;margin-bottom:8px;opacity:.3}.empty svg{width:34px;height:34px;stroke:currentColor;fill:none;stroke-width:1.6}'\n    + '.err{background:#fff0ef;border:1px solid rgba(255,60,51,.45);color:#b3261e;border-radius:9px;padding:9px 12px;font-size:13px}'\n    + '.right{border-left:1px solid var(--line);background:#fffdf9;display:flex;flex-direction:column;min-height:0}'\n    + '.right h2{margin:0;padding:16px 18px 10px;font-size:11px;letter-spacing:1.3px;text-transform:uppercase;color:var(--ink2)}'\n    + '.srcs{flex:1;min-height:0;overflow:auto;padding:0 16px 16px;display:flex;flex-direction:column;gap:10px}'\n    + '.src{background:#fff;border:1px solid var(--line);border-radius:14px;padding:12px 14px}'\n    + '.src-h{display:flex;gap:10px;align-items:flex-start}'\n    + '.src-t{flex:1;margin:0;font-size:13.5px;font-weight:700;line-height:1.35}'\n    + '.src-s{font-size:19px;font-weight:700;color:var(--accent);line-height:1}'\n    + '.src-c{display:inline-block;margin-top:6px;font-size:10px;letter-spacing:.6px;background:#f2ede4;border-radius:5px;padding:2px 7px;color:var(--ink2)}'\n    + '.src-b{margin:8px 0 0;font-size:12.5px;color:var(--ink2);max-height:76px;overflow:hidden}'\n    + '.src-l{display:inline-block;margin-top:9px;font-size:12px;font-weight:600;color:var(--accent);text-decoration:underline}'\n    + '.ft{display:flex;align-items:center;gap:16px;padding:10px 22px;border-top:1px solid var(--line);background:#fff;font-size:12px;color:var(--ink2)}'\n    + '.hist{flex:none;margin-left:auto;border:1px solid var(--line);background:#fff;border-radius:8px;padding:7px 14px;font:600 12px/1 inherit;color:var(--ink);cursor:pointer}'\n    + '.hist:hover{border-color:var(--accent);color:var(--accent)}'\n    + '.scrim{position:absolute;inset:0;background:rgba(0,0,0,.44);backdrop-filter:blur(2px);display:grid;place-items:center;z-index:9}'\n    + '.dlg{background:var(--cream);border-radius:20px;box-shadow:0 18px 50px rgba(0,0,0,.28);position:relative;max-width:92%;max-height:88%;display:flex;flex-direction:column}'\n    + '.dlg-h{display:flex;align-items:center;padding:22px 24px 12px}'\n    + '.dlg-h h2{margin:0;font-size:21px;font-weight:700}'\n    + '.x{margin-left:auto;border:0;background:transparent;font:700 20px/1 inherit;color:var(--ink);cursor:pointer;padding:2px 4px}'\n    + '.dlg-b{padding:0 24px 24px;overflow:auto;min-height:0}'\n    + '.help{width:660px}'\n    + '.hist-w{width:1000px;height:78%;display:grid;grid-template-columns:290px 1fr;gap:18px;overflow:hidden}'\n    + '.hlist{overflow:auto;background:#fff;border:1px solid var(--line);border-radius:12px}'\n    + '.hitem{display:block;width:100%;text-align:left;border:0;border-bottom:1px solid var(--line);background:transparent;padding:11px 13px;cursor:pointer;font:inherit;color:var(--ink)}'\n    + '.hitem:hover{background:#faf6ef}'\n    + '.hitem[aria-selected=\"true\"]{background:var(--olive)}'\n    + '.hitem b{display:block;font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}'\n    + '.hitem span{font-size:11px;color:var(--ink2)}'\n    + '.hdet{overflow:auto;padding-right:6px;display:flex;flex-direction:column;gap:12px}'\n    + '.vote{width:700px}'\n    + '.scale{display:flex;gap:11px;margin:4px 0 12px}'\n    + '.dot{flex:none;width:42px;height:42px;border-radius:50%;border:1px solid rgba(22,43,55,.35);background:#fff;color:var(--ink);font:700 17px/1 inherit;cursor:pointer}'\n    + '.dot[aria-pressed=\"true\"]{background:var(--ink);color:#fff}'\n    + '.vdesc{min-height:38px;font-size:13px;color:var(--ink2);white-space:pre-line}'\n    + '.vin{width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:9px;font:inherit;background:#fff;color:var(--ink)}'\n    + '.vrow{display:flex;align-items:center;gap:12px;margin-top:14px}'\n    + '.vrow .send{margin-left:auto}'\n    + '.tag{font-size:11px;border:1px dashed rgba(22,43,55,.35);border-radius:6px;padding:2px 7px;color:var(--ink2)}',\n\n  render(d2) {\n    const u = user(d2)\n    const mk = market(d2, u)\n    const opts = marketOptions(d2, u)\n    const hs = hits(d2)\n    const ans = answerText(d2)\n    const bz = busy(d2)\n    const marked = d2.lib('marked@12.0.0')\n    const md = function (t) {\n      if (!t) return ''\n      if (!marked) return '<p>' + esc(t).replace(/\\n/g, '<br>') + '</p>'\n      try { return marked.parse(t) } catch (e) { return '<p>' + esc(t) + '</p>' }\n    }\n    const acc = d2.param('accent', '#FF6C0E')\n\n    const selector = showSelector(d2, u)\n      ? '<select class=\"sel\">' + opts.map(function (c) { return '<option value=\"' + attr(c) + '\"' + (c === mk ? ' selected' : '') + '>' + esc(c) + '</option>' }).join('') + '</select>'\n      : '<span class=\"lock\" title=\"Tu usuario está asignado a un solo mercado\">' + esc(mk || '--') + '</span>'\n\n    const conversa = (!d2.state.pregunta && !ans)\n      ? '<div class=\"empty\"><b><svg viewBox=\"0 0 24 24\"><path d=\"M21 12a8 8 0 0 1-8 8H7l-4 3v-5a8 8 0 0 1 8-11h2a8 8 0 0 1 8 5z\" stroke-linejoin=\"round\"/></svg></b>Escribí la duda del cliente y NASVI busca en las wikis por vos.</div>'\n      : (d2.state.pregunta ? '<div class=\"you\">' + esc(d2.state.pregunta) + '</div>' : '')\n        + ((ans || bz)\n          ? '<div class=\"bot\"><div class=\"av\">N</div><div><div class=\"bub\">' + md(ans) + (bz ? '<span class=\"cur\"></span>' : '') + '</div>'\n            + (!bz && ans ? '<div class=\"acts\"><button class=\"act copy\">Copiar</button><button class=\"act vote\">Votar</button></div>' : '')\n            + '</div></div>'\n          : '')\n\n    const fuentes = hs.length\n      ? hs.map(function (r, i) { return card(d2, r, i) }).join('')\n      : '<div class=\"empty\"><b><svg viewBox=\"0 0 24 24\"><path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\" stroke-linejoin=\"round\"/><path d=\"M14 3v5h5M9 13h6M9 17h4\" stroke-linecap=\"round\"/></svg></b>Realiza tu consulta para ver las wikis relacionadas aquí.</div>'\n\n    let overlay = ''\n    if (d2.state.panel === 'help') {\n      overlay = '<div class=\"scrim\" data-close><div class=\"dlg help\"><div class=\"dlg-h\"><h2>NASVI: El Agente del Agente</h2><button class=\"x\">&#215;</button></div><div class=\"dlg-b bub\" style=\"background:transparent;padding:0 24px 24px\">' + md(HELP_MD) + '</div></div></div>'\n    } else if (d2.state.panel === 'hist') {\n      const rows = history(d2)\n      const sel = d2.state.hsel || (rows[0] && (rows[0]._id || 0))\n      const cur = rows.filter(function (r, i) { return (r._id || i) === sel })[0] || rows[0]\n      overlay = '<div class=\"scrim\" data-close><div class=\"dlg hist-w\"><div class=\"dlg-h\" style=\"grid-column:1/-1\"><h2>Historial</h2><button class=\"x\">&#215;</button></div>'\n        + '<div class=\"hlist\">' + (rows.length ? rows.map(function (r, i) {\n          const id = r._id || i\n          return '<button class=\"hitem\" data-key=\"' + attr(id) + '\" data-h=\"' + attr(id) + '\" aria-selected=\"' + (id === sel ? 'true' : 'false') + '\"><b>' + esc(r.pergunta) + '</b><span>' + esc(r.createdAt ? d2.fmt.relative(r.createdAt) : '') + '</span></button>'\n        }).join('') : '<div class=\"empty\" style=\"padding:20px\">Sin consultas todavía.</div>') + '</div>'\n        + '<div class=\"hdet\">' + (cur ? '<div class=\"you\" style=\"align-self:flex-start;max-width:100%\">' + esc(cur.pergunta) + '</div><div class=\"bot\"><div class=\"av\">N</div><div class=\"bub\">' + md(cur.resposta) + '</div></div>' : '') + '</div>'\n        + '</div></div>'\n    } else if (d2.state.panel === 'vote') {\n      const g = d2.state.grade || 0\n      const lab = VOTE[g]\n      overlay = '<div class=\"scrim\" data-close><div class=\"dlg vote\"><div class=\"dlg-h\"><h2>¿Qué tan útil fue la respuesta?</h2><button class=\"x\">&#215;</button></div><div class=\"dlg-b\">'\n        + '<div class=\"scale\">' + [1, 2, 3, 4, 5].map(function (n) { return '<button class=\"dot\" data-key=\"' + n + '\" data-g=\"' + n + '\" aria-pressed=\"' + (n === g ? 'true' : 'false') + '\">' + n + '</button>' }).join('') + '</div>'\n        + '<div class=\"vdesc\">' + (lab ? esc(lab[0]) + '\\n' + esc(lab[1]) : '') + '</div>'\n        + '<input class=\"vin\" placeholder=\"Comentarios (opcional)\" value=\"' + attr(d2.state.vcomment || '') + '\"/>'\n        + '<div class=\"vrow\">' + (d2.param('telemetry') ? '' : '<span class=\"tag\">telemetría desactivada — no se guarda</span>') + '<button class=\"send vsend\"' + (g ? '' : ' disabled') + '>Enviar</button></div>'\n        + '</div></div></div>'\n    }\n\n    return '<div class=\"app\" data-ready style=\"--acc:' + attr(acc) + '\">'\n      + '<header class=\"hd\">'\n      + '<div class=\"brand\"><b>NASVI</b><span>Copiloto activo</span></div>'\n      + '<div class=\"chips\">' + ORIGENES.map(function (o) { return '<button class=\"chip\" data-o=\"' + o[0] + '\" aria-pressed=\"' + ((d2.state.origen || 'chat') === o[0] ? 'true' : 'false') + '\">' + o[1] + '</button>' }).join('') + '</div>'\n      + '<div class=\"spacer\"></div>'\n      + '<div class=\"mk\"><span>Mercado</span>' + selector + '</div>'\n      + '<div class=\"who\"><b>' + esc(u.name || 'Asesor') + '</b>' + esc(u.email || '') + '</div>'\n      + '<button class=\"ico help\" title=\"Ayuda\">?</button>'\n      + '</header>'\n      + '<div class=\"main\">'\n      + '<section class=\"left\">'\n      + '<div class=\"ask\"><label for=\"q\">¿Cuál es la pregunta del cliente?</label>'\n      + '<textarea class=\"q\" id=\"q\" placeholder=\"Escribí la duda tal como te la hizo la consultora…\"' + (bz ? ' disabled' : '') + '></textarea>'\n      + '<div class=\"ask-r\"><span class=\"hint\">Enter envía &middot; Shift+Enter salta línea</span>'\n      + '<button class=\"send go\"' + (bz ? ' disabled' : '') + '>' + (bz ? 'Buscando…' : 'Enviar pregunta') + '</button></div></div>'\n      + (d2.state.err ? '<div class=\"err\">' + esc(d2.state.err) + '</div>' : '')\n      + '<div class=\"log\">' + conversa + '</div>'\n      + '</section>'\n      + '<aside class=\"right\"><h2>Fuentes</h2><div class=\"srcs\">' + fuentes + '</div></aside>'\n      + '</div>'\n      + '<footer class=\"ft\">'\n      + '<span>' + (d2.state.duracion ? (d2.state.duracion / 1000).toFixed(1) + ' s' : '--') + '</span>'\n      + '<span>' + hs.length + ' fuentes</span>'\n      + '<span>Mercado ' + esc(mk || '--') + '</span>'\n      + (isDemo(d2) ? '<span class=\"tag\">demo</span>' : '')\n      + (d2.param('telemetry') ? '' : '<span class=\"tag\">telemetría off</span>')\n      + '<button class=\"hist\">Historial</button>'\n      + '</footer>'\n      + overlay\n      + '</div>'\n  },\n\n  mounted(d2) {\n    const ask = async function () {\n      const el = d2.$('.q')\n      if (!el) return\n      const q = (el.value || '').trim()\n      if (!q) return\n      const t0 = Date.now()\n      el.value = ''\n      d2.setState({ pregunta: q, err: '', duracion: 0, grade: 0, vcomment: '' })\n      if (isDemo(d2)) { await demo(d2, q, t0); return }\n      try {\n        const u = user(d2)\n        const ds = d2.data('faqs')\n        if (!ds) throw new Error('Parámetro \"faqs\" no está conectado a un dataset.')\n        ds.filter({ ecommerce: isEcom(u) ? true : { nin: [true] }, busca: q, country: market(d2, u) })\n        await ds.refresh()\n        const rows = ds.records()\n        d2.setState({ hits: rows })\n        await d2.assistant(d2.params.assistant).ask('Operador Pergunta :  \\n\\n\"' + q + '\"\\n\\nContesta en espanol', { context: JSON.stringify(rows) })\n        d2.state.duracion = Date.now() - t0\n        if (d2.param('telemetry')) await persist(d2, q)\n      } catch (e) {\n        d2.state.err = 'No se pudo completar la búsqueda: ' + String((e && e.message) || e)\n      }\n    }\n\n    const demo = async function (d2, q, t0) {\n      d2.setState({ busy: true, answer: '', hits: [] })\n      await new Promise(function (r) { setTimeout(r, 450) })\n      d2.setState({ hits: MOCK_FAQS })\n      let i = 0\n      await new Promise(function (done) {\n        const id = setInterval(function () {\n          i += 4\n          d2.state.answer = MOCK_ANSWER.slice(0, i)\n          if (i >= MOCK_ANSWER.length) { clearInterval(id); done() }\n        }, 16)\n        d2.onUnmount(function () { clearInterval(id) })\n      })\n      d2.setState({ busy: false, duracion: Date.now() - t0 })\n    }\n\n    const persist = async function (d2, q) {\n      const u = user(d2)\n      const mon = d2.data('historial')\n      if (mon) await mon.create({\n        email: u.email, name: u.name, pergunta: q, resposta: answerText(d2),\n        country: market(d2, u), origen: d2.state.origen || 'chat',\n        duracion: d2.state.duracion, user_type: isEcom(u) ? 'ecommerce' : undefined\n      })\n      const ev = d2.data('eventos')\n      if (ev) await ev.create({ event: 'assistant_ask', email: u.email, nome: u.name, country: market(d2, u), payload: { article: (hits(d2)[0] || {}).article } })\n    }\n\n    d2.on('.go', 'click', ask)\n    d2.on('.q', 'keydown', function (e) {\n      if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); ask() }\n    })\n    d2.on('.chip', 'click', function (e, el) { d2.state.origen = el.dataset.o })\n    d2.on('.sel', 'change', function (e, el) { d2.state.region = el.value })\n    d2.on('.help', 'click', function () { d2.state.panel = 'help' })\n    d2.on('.hist', 'click', function () { d2.state.panel = 'hist' })\n    d2.on('.vote', 'click', function () { d2.setState({ panel: 'vote', grade: 0, vcomment: '' }) })\n    d2.on('.x', 'click', function () { d2.state.panel = null })\n    d2.on('.scrim', 'click', function (e, el) { if (e.target === el) d2.state.panel = null })\n    d2.on('.hitem', 'click', function (e, el) { d2.state.hsel = el.dataset.h })\n    d2.on('.dot', 'click', function (e, el) { d2.state.grade = Number(el.dataset.g) })\n    d2.on('.vin', 'input', function (e, el) { d2.state.vcomment = el.value })\n    d2.on('.src-l', 'click', function (e, el) {\n      if (!d2.param('telemetry')) return\n      const ev = d2.data('eventos')\n      const u = user(d2)\n      if (ev) ev.create({ event: 'wiki_view', email: u.email, nome: u.name, country: market(d2, u), payload: { article: el.dataset.article } })\n    })\n    d2.on('.vsend', 'click', async function () {\n      const g = d2.state.grade\n      if (!g) return\n      if (d2.param('telemetry')) {\n        const u = user(d2)\n        const ev = d2.data('eventos')\n        const payload = { message: d2.state.pregunta, answer: answerText(d2), grade: g, comment: d2.state.vcomment || '' }\n        if (ev) {\n          await ev.create({ event: 'voting', email: u.email, nome: u.name, country: market(d2, u), payload: payload })\n          if (g === 1) await ev.create({ event: 'answer_not_found', email: u.email, nome: u.name, country: market(d2, u), payload: payload })\n        }\n        const enc = d2.data('encuestas')\n        if (enc) await enc.create({ country: market(d2, u), email: u.email, reaction: g >= 4 ? 'like' : 'dislike', comments: d2.state.vcomment || '' })\n      }\n      d2.setState({ panel: null, voted: g })\n    })\n    d2.on('.copy', 'click', function (e, el) {\n      const t = answerText(d2)\n      if (!t) return\n      try { navigator.clipboard.writeText(t) } catch (err) { }\n      el.textContent = 'Copiado'\n      setTimeout(function () { try { el.textContent = 'Copiar' } catch (e2) { } }, 1400)\n    })\n    d2.on(document, 'keydown', function (e) { if (e.key === 'Escape' && d2.state.panel) d2.state.panel = null })\n  },\n\n  updated(d2) {\n    const log = d2.$('.log')\n    if (log) log.scrollTop = log.scrollHeight\n  }\n}\n"}},"demo":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"demo"}}}},"telemetry":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"telemetry"}}}},"assistant":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"assistant"}}}},"faqs":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"faqs"}}}},"usuarios":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"usuarios"}}}},"regiones":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"regiones"}}}},"historial":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"historial"}}}},"eventos":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"eventos"}}}},"encuestas":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"encuestas"}}}},"estado":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"estado"}}}},"accent":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"accent"}}}}}}}}}},{"_id":"6ab029c576d35d65732ff420","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"bellaSim","package":"lucas-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"/* Bella Sim — réplica de WhatsApp Web (80%) + móvil espejo (20%) */\n\nconst T = {\n  searchPh: 'Buscar un chat',\n  tabs: ['Todos', 'No leídos', 'Favoritos', 'Grupos'],\n  online: 'en línea',\n  typing: 'escribiendo…',\n  composerPh: 'Escribe un mensaje',\n  encrypted: 'Los mensajes están cifrados de extremo a extremo. Nadie fuera de este chat puede leerlos, ni siquiera nosotros.',\n  today: 'HOY',\n  emptyT: 'Aún no hay mensajes',\n  reply: 'Responder',\n  you: 'Tú',\n  recHint: 'Grabando…',\n  attPhotos: 'Fotos y vídeos',\n  attDoc: 'Documento',\n  dbgEmpty: 'Todavía no hay turnos. Envía un mensaje para ver la petición y la respuesta.',\n  noReply: 'No se pudo contactar a Bella.'\n}\nconst KL = { image: 'Foto', video: 'Vídeo', audio: 'Mensaje de voz', document: 'Documento', sticker: 'Sticker' }\n\nconst EMO = ['😀','😃','😄','😁','😆','😅','😂','🤣','😊','😇','🙂','🙃','😉','😌','😍','🥰','😘','😋','😎','🤓','🥳','🤔','😐','😴','😒','😞','😔','😟','🙁','😢','😭','😤','😠','🥺','😳','🤗','🤝','👍','👎','👏','🙏','💪','👋','❤️','🧡','💛','💚','💙','💜','🔥','✨','🎉','✅','❌','💯','🚀','👀','💬','📦','🛍️','💄','🌸','☀️','🌙']\nconst QUICK = ['👍','❤️','😂','😮','😢','🙏']\n\nconst sv = (p, w) => '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"' + (w || 1.9) + '\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' + p + '</svg>'\nconst IC = {\n  search: sv('<circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20.5 20.5-3.7-3.7\"/>'),\n  menu: '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"12\" cy=\"5\" r=\"1.8\"/><circle cx=\"12\" cy=\"12\" r=\"1.8\"/><circle cx=\"12\" cy=\"19\" r=\"1.8\"/></svg>',\n  newchat: sv('<path d=\"M21 11.5a8.4 8.4 0 0 1-9 8.4 8.6 8.6 0 0 1-3.8-.9L3 20.5l1.6-4.9A8.4 8.4 0 1 1 21 11.5Z\"/><path d=\"M12 8.5v5M9.5 11h5\"/>'),\n  emoji: sv('<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8.5 14.5a4.5 4.5 0 0 0 7 0\"/><path d=\"M9 9.5h.01M15 9.5h.01\" stroke-width=\"2.6\"/>'),\n  clip: sv('<path d=\"M20 11.5 12.2 19.3a4.6 4.6 0 0 1-6.5-6.5l7.8-7.8a3.1 3.1 0 0 1 4.4 4.4l-7.8 7.8a1.5 1.5 0 0 1-2.2-2.2l7.2-7.2\"/>'),\n  mic: sv('<rect x=\"9\" y=\"2.5\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5.5 11a6.5 6.5 0 0 0 13 0\"/><path d=\"M12 17.5V21\"/>'),\n  send: '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M2.2 21 23 12 2.2 3l.01 7L17 12 2.21 14z\"/></svg>',\n  x: sv('<path d=\"M18 6 6 18M6 6l12 12\"/>'),\n  down: sv('<path d=\"M12 4v12m0 0 4.5-4.5M12 16l-4.5-4.5\"/><path d=\"M4.5 19.5h15\"/>'),\n  play: '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M7 4.5 19.5 12 7 19.5z\"/></svg>',\n  pause: '<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><rect x=\"6.5\" y=\"4.5\" width=\"4\" height=\"15\" rx=\"1\"/><rect x=\"13.5\" y=\"4.5\" width=\"4\" height=\"15\" rx=\"1\"/></svg>',\n  chev: sv('<path d=\"m6 9.5 6 6 6-6\"/>', 2.2),\n  trash: sv('<path d=\"M4 7h16M10 7V4.8h4V7M6.5 7l.9 12.2h9.2L17.5 7\"/>'),\n  back: sv('<path d=\"M19 12H5m0 0 6.5-6.5M5 12l6.5 6.5\"/>'),\n  phone: sv('<path d=\"M6.5 3.5h3l1.5 4-2 1.5a12 12 0 0 0 6 6L16.5 13l4 1.5v3a2 2 0 0 1-2.2 2A16.8 16.8 0 0 1 4.5 5.7a2 2 0 0 1 2-2.2Z\"/>'),\n  video: sv('<rect x=\"2.5\" y=\"6\" width=\"13\" height=\"12\" rx=\"2.5\"/><path d=\"m15.5 11 6-3.2v8.4l-6-3.2z\"/>'),\n  lock: sv('<rect x=\"4.5\" y=\"10.5\" width=\"15\" height=\"10\" rx=\"2.5\"/><path d=\"M8 10.5V7.8a4 4 0 0 1 8 0v2.7\"/>'),\n  image: sv('<rect x=\"3\" y=\"4.5\" width=\"18\" height=\"15\" rx=\"2.5\"/><circle cx=\"8.5\" cy=\"10\" r=\"1.7\"/><path d=\"m4 17 5-4.5 4.5 4L17 13l3 3\"/>'),\n  doc: sv('<path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\"/><path d=\"M14 3v5h5M9 13h6M9 17h4\"/>'),\n  reply: sv('<path d=\"M9.5 6.5 4 12l5.5 5.5\"/><path d=\"M4 12h9a7 7 0 0 1 7 7v1\"/>'),\n  smile: sv('<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8.5 14.5a4.5 4.5 0 0 0 7 0\"/><path d=\"M9 9.5h.01M15 9.5h.01\" stroke-width=\"2.6\"/>'),\n  bug: sv('<rect x=\"7\" y=\"7\" width=\"10\" height=\"13\" rx=\"5\"/><path d=\"M9 6.5a3 3 0 0 1 6 0M3.5 11h3.5M17 11h3.5M3.5 17H7M17 17h3.5M12 7V4.5\"/>')\n}\nconst TICK = {\n  pending: '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\"><circle cx=\"8\" cy=\"8\" r=\"6\"/><path d=\"M8 4.8V8l2.2 1.4\"/></svg>',\n  sent: '<svg viewBox=\"0 0 16 15\" fill=\"currentColor\"><path d=\"M10.9 3.3 5.6 10 3 7.6l-.9 1 3.6 3.3 6.1-7.7z\"/></svg>',\n  two: '<svg viewBox=\"0 0 18 15\" fill=\"currentColor\"><path d=\"M12.9 3.3 7.6 10 5.9 8.4l-.9 1L7.6 12l6.2-7.7zM8.3 3.3 3 10 1.2 8.3l-.9 1L3 12l6.2-7.7z\"/></svg>'\n}\n\nconst esc = (s) => String(s == null ? '' : s).replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst hhmm = (ts) => { const d = new Date(ts); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') }\nconst mmss = (s) => Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0')\nconst wait = (ms) => new Promise((r) => setTimeout(r, ms))\nconst uid = (p) => p + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)\n\nfunction bars(id, n) {\n  let h = 0\n  for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0\n  const out = []\n  for (let i = 0; i < n; i++) { h = (h * 1103515245 + 12345) >>> 0; out.push(22 + (h % 78)) }\n  return out\n}\n\n/* ---- estado no reactivo (propiedad del DOM / del navegador) ---- */\nlet draft = ''\nlet stick = true\nlet audioEl = null\nlet rec = null\nlet recChunks = []\nlet recTimer = null\nlet recStart = 0\nlet SID = uid('s-')\nlet blobs = []\n\n/* ---------------- burbujas: UNA sola función, usada por los dos paneles ---------------- */\nfunction quoteHTML(q) {\n  if (!q) return ''\n  const label = q.mine ? T.you : esc(q.author || '')\n  const prev = q.kind === 'text' ? q.preview : (KL[q.kind] || '') + (q.preview ? ' · ' + q.preview : '')\n  return '<div class=\"quote ' + (q.mine ? 'qme' : 'qthem') + '\"><span class=\"qa\">' + label + '</span><span class=\"qt\">' + esc(prev) + '</span></div>'\n}\n\nfunction metaHTML(m) {\n  let t = ''\n  if (m.mine) {\n    if (m.status === 'pending') t = '<span class=\"tk\">' + TICK.pending + '</span>'\n    else if (m.status === 'sent') t = '<span class=\"tk\">' + TICK.sent + '</span>'\n    else if (m.status === 'delivered') t = '<span class=\"tk\">' + TICK.two + '</span>'\n    else if (m.status === 'read') t = '<span class=\"tk blue\">' + TICK.two + '</span>'\n  }\n  return '<span class=\"meta\"><span class=\"mt\">' + hhmm(m.ts) + '</span>' + t + '</span>'\n}\n\nfunction reactHTML(m) {\n  const ks = Object.keys(m.reactions || {})\n  if (!ks.length) return ''\n  return '<span class=\"reacts\">' + ks.map((k) => '<span>' + k + '</span>').join('') + '</span>'\n}\n\nfunction bodyHTML(m) {\n  const cap = m.body || m.caption || ''\n  const capH = cap ? '<div class=\"txt\">' + esc(cap) + '</div>' : ''\n  if (m.kind === 'image') return '<div class=\"media img\" data-open=\"' + esc(m.media) + '\" style=\"background-image:url(' + esc(m.media) + ')\"></div>' + capH\n  if (m.kind === 'sticker') return '<div class=\"media stk\" style=\"background-image:url(' + esc(m.media) + ')\"></div>'\n  if (m.kind === 'video') return '<div class=\"media vid\"><video src=\"' + esc(m.media) + '\" controls preload=\"metadata\"></video></div>' + capH\n  if (m.kind === 'document') return '<div class=\"docrow\"><span class=\"dic\">' + IC.doc + '</span><span class=\"dmeta\"><b>' + esc(m.fname || 'documento') + '</b><i>' + esc(m.fsize || '') + '</i></span><a class=\"ddl\" href=\"' + esc(m.media) + '\" download=\"' + esc(m.fname || 'archivo') + '\" title=\"Descargar\">' + IC.down + '</a></div>' + capH\n  if (m.kind === 'audio') {\n    const bb = bars(m.id, 34).map((h) => '<i style=\"height:' + Math.round(h * 0.26) + 'px\"></i>').join('')\n    return '<div class=\"aud\"><button class=\"apl\" data-play=\"' + esc(m.id) + '\" data-src=\"' + esc(m.media) + '\">' + IC.play + '</button><span class=\"awrap\"><span class=\"abars\" data-seek=\"' + esc(m.id) + '\">' + bb + '<span class=\"afill\" data-fill=\"' + esc(m.id) + '\"></span></span><span class=\"atime\" data-time=\"' + esc(m.id) + '\">' + mmss(m.durSec || 0) + '</span></span></div>'\n  }\n  if (m.kind === 'system') return '<div class=\"txt sys\">' + esc(cap) + '</div>'\n  return '<div class=\"txt\">' + esc(cap) + '</div>'\n}\n\nfunction bubbleHTML(m, st, name) {\n  if (m.kind === 'system') return '<div class=\"row sysrow\" data-key=\"' + esc(m.id) + '\"><div class=\"syschip\">' + esc(m.body) + '</div></div>'\n  const cls = ['bub', m.mine ? 'me' : 'them']\n  if (m.kind === 'sticker') cls.push('plain')\n  if (m.kind === 'image' || m.kind === 'video') cls.push('hasmedia')\n  const hasR = Object.keys(m.reactions || {}).length ? ' hasreact' : ''\n  const open = st && st.menuFor === m.id\n  const menu = open ? '<div class=\"bmenu\"><div class=\"qrow\">' + QUICK.map((e) => '<button class=\"qr\" data-react=\"' + esc(m.id) + '\" data-emo=\"' + e + '\">' + e + '</button>').join('') + '</div><button class=\"mitem\" data-replyto=\"' + esc(m.id) + '\">' + IC.reply + '<span>' + T.reply + '</span></button></div>' : ''\n  return '<div class=\"row ' + (m.mine ? 'rme' : 'rthem') + hasR + '\" data-key=\"' + esc(m.id) + '\" data-id=\"' + esc(m.id) + '\">' +\n    '<div class=\"bwrap\">' +\n      '<div class=\"' + cls.join(' ') + '\">' + quoteHTML(m.replyTo) + bodyHTML(m) + metaHTML(m) + reactHTML(m) + '</div>' +\n      '<button class=\"bmb\" data-menu=\"' + esc(m.id) + '\">' + IC.chev + '</button>' + menu +\n    '</div></div>'\n}\n\nfunction threadHTML(msgs, st, name) {\n  let out = '<div class=\"enc\">' + IC.lock + '<span>' + T.encrypted + '</span></div>'\n  if (!msgs.length) {\n    out += '<div class=\"empty\"><div class=\"eico\">' + IC.smile + '</div><b>' + T.emptyT + '</b><span>Escribe abajo para empezar a hablar con ' + esc(name) + '.</span></div>'\n    return out\n  }\n  out += '<div class=\"day\"><span>' + T.today + '</span></div>'\n  for (const m of msgs) out += bubbleHTML(m, st, name)\n  if (st.bellaTyping) out += '<div class=\"row rthem\" data-key=\"__typing\"><div class=\"bwrap\"><div class=\"bub them typing\"><i></i><i></i><i></i></div></div></div>'\n  return out\n}\n\n/* ---------------------------------- componente ---------------------------------- */\nexport default {\n  css: `\n:host{display:block;height:100%}\n*{box-sizing:border-box;margin:0;padding:0}\n.root{--wp:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cg fill='none' stroke='%23ffffff' stroke-opacity='0.033' stroke-width='1.4'%3E%3Ccircle cx='24' cy='28' r='7'/%3E%3Cpath d='M62 20l10 10-10 10-10-10z'/%3E%3Cpath d='M104 24h18M104 32h11'/%3E%3Cpath d='M20 78c6-8 15-8 21 0'/%3E%3Ccircle cx='79' cy='84' r='9'/%3E%3Cpath d='M118 74l9 15h-18z'/%3E%3Cpath d='M32 118h19v13H32z'/%3E%3Cpath d='M75 124h15M75 131h9'/%3E%3Ccircle cx='124' cy='124' r='6'/%3E%3C/g%3E%3C/svg%3E\");display:flex;height:100%;min-height:520px;background:#0b141a;color:#e9edef;font-family:'Segoe UI',Helvetica,system-ui,-apple-system,sans-serif;font-size:14.2px;position:relative;overflow:hidden}\nbutton{font:inherit;color:inherit;background:none;border:0;cursor:pointer}\nsvg{width:100%;height:100%;display:block}\n.ib{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;color:#aebac1;flex:0 0 auto}\n.ib>svg{width:21px;height:21px}\n.ib:hover{background:rgba(134,150,160,.12)}\n\n/* ---------- panel web (80%) ---------- */\n.web{flex:1 1 80%;display:flex;min-width:0;border-right:1px solid #0a1014}\n.side{flex:0 0 400px;max-width:44%;display:flex;flex-direction:column;background:#111b21;border-right:1px solid #222d34;min-width:0}\n.shead{height:59px;background:#202c33;display:flex;align-items:center;gap:6px;padding:0 16px;flex:0 0 auto}\n.av{border-radius:50%;background:#3b4a54 center/cover no-repeat;flex:0 0 auto}\n.shead .av{width:40px;height:40px}\n.meav{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%236a7c85'/%3E%3Ccircle cx='20' cy='15' r='7' fill='%23cfd8dc'/%3E%3Cpath d='M6 40c0-8 6.5-12 14-12s14 4 14 12z' fill='%23cfd8dc'/%3E%3C/svg%3E\")}\n.sp{flex:1}\n.srow{padding:7px 12px;background:#111b21;flex:0 0 auto}\n.sbox{display:flex;align-items:center;gap:10px;height:35px;background:#202c33;border-radius:8px;padding:0 12px;color:#8696a0}\n.sbox>svg{width:17px;height:17px;flex:0 0 auto}\n.sbox span{font-size:14px}\n.tabs{display:flex;gap:8px;padding:2px 12px 10px;flex:0 0 auto}\n.tab{font-size:13.2px;color:#8696a0;background:#202c33;border-radius:999px;padding:5px 13px;white-space:nowrap}\n.tab.on{background:rgba(0,168,132,.2);color:#00a884}\n.convs{flex:1;overflow-y:auto;border-top:1px solid #222d34}\n.conv{display:flex;gap:14px;padding:10px 14px;cursor:pointer;background:#2a3942}\n.conv .av{width:49px;height:49px}\n.cmid{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:3px}\n.cn{display:flex;justify-content:space-between;gap:8px;align-items:baseline}\n.cn b{font-weight:400;font-size:16.5px;color:#e9edef;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.cn i{font-style:normal;font-size:12px;color:#8696a0;flex:0 0 auto}\n.cp{font-size:13.6px;color:#8696a0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n.chat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative}\n.chead{height:59px;background:#202c33;display:flex;align-items:center;gap:14px;padding:0 16px;flex:0 0 auto;z-index:3}\n.chead .av{width:40px;height:40px}\n.ci{flex:1;min-width:0;line-height:1.25}\n.ci b{display:block;font-weight:400;font-size:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ci span{display:block;font-size:12.6px;color:#8696a0}\n.ci span.act{color:#00a884}\n.thread{flex:1;overflow-y:auto;padding:14px 6.5% 10px;background-color:#0b141a;background-image:var(--wp)}\n.enc{max-width:560px;margin:2px auto 14px;background:#182229;color:#ffd279;border-radius:8px;padding:7px 12px;font-size:12.4px;line-height:1.45;display:flex;gap:7px;align-items:flex-start;text-align:center}\n.enc>svg{width:13px;height:13px;flex:0 0 auto;margin-top:2px}\n.day{display:flex;justify-content:center;margin:8px 0 12px}\n.day span{background:#182229;color:#8696a0;font-size:12.2px;border-radius:7px;padding:5px 12px;text-transform:uppercase;letter-spacing:.3px}\n.empty{display:flex;flex-direction:column;align-items:center;gap:8px;color:#8696a0;padding:52px 20px;text-align:center}\n.eico{width:46px;height:46px;color:#2a3942}\n.empty b{color:#e9edef;font-weight:400;font-size:17px}\n.empty span{font-size:13.4px;max-width:330px}\n\n.row{display:flex;margin-bottom:3px}\n.rme{justify-content:flex-end}\n.rthem{justify-content:flex-start}\n.sysrow{justify-content:center;margin:10px 0}\n.syschip{background:#3b2b2b;color:#f0a6a6;border-radius:8px;padding:6px 12px;font-size:12.6px;max-width:70%}\n.bwrap{position:relative;max-width:66%;display:flex}\n.bub{position:relative;border-radius:8px;padding:6px 9px 8px;min-width:78px;box-shadow:0 1px .5px rgba(11,20,26,.13);word-wrap:break-word;overflow-wrap:anywhere}\n.bub.them{background:#202c33;border-top-left-radius:0}\n.bub.me{background:#005c4b;border-top-right-radius:0}\n.bub.plain{background:none!important;box-shadow:none;padding:0;min-width:0}\n.txt{font-size:14.2px;line-height:1.36;white-space:pre-wrap;padding-right:58px}\n.txt.sys{color:#f0a6a6}\n.bub.hasmedia .txt{padding-top:5px}\n.meta{position:absolute;right:9px;bottom:5px;display:flex;align-items:center;gap:3px;font-size:11px;color:rgba(233,237,239,.6);pointer-events:none}\n.bub.plain .meta{right:2px;bottom:2px;background:rgba(11,20,26,.5);border-radius:6px;padding:1px 5px}\n.tk{width:16px;height:11px;color:rgba(233,237,239,.6);display:block}\n.tk.blue{color:#53bdeb}\n.tk svg{width:100%;height:100%}\n.reacts{position:absolute;bottom:-11px;right:8px;background:#202c33;border:1px solid #0b141a;border-radius:999px;padding:1px 5px;font-size:12px;line-height:1.5;display:flex;gap:1px}\n.rme .reacts{background:#005c4b}\n.row.hasreact{margin-bottom:16px}\n.reacts,.qr,.ec{font-family:'Noto Color Emoji','Apple Color Emoji','Segoe UI Emoji','Twemoji Mozilla',sans-serif}\n.pthread .row.hasreact{margin-bottom:13px}\n.quote{display:flex;flex-direction:column;gap:2px;border-left:4px solid #53bdeb;border-radius:5px;padding:4px 8px;margin-bottom:4px;background:rgba(11,20,26,.28);max-width:100%;overflow:hidden}\n.quote.qme{border-left-color:#25d366}\n.qa{font-size:12.8px;color:#53bdeb}\n.qme .qa{color:#25d366}\n.qt{font-size:13px;color:rgba(233,237,239,.65);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.media{border-radius:6px;overflow:hidden}\n.img{width:296px;max-width:100%;height:198px;background:#0b141a center/cover no-repeat;cursor:pointer}\n.stk{width:168px;height:168px;background:center/contain no-repeat}\n.vid video{width:296px;max-width:100%;height:198px;border-radius:6px;background:#000;display:block}\n.docrow{display:flex;align-items:center;gap:10px;background:rgba(11,20,26,.22);border-radius:6px;padding:10px;min-width:262px}\n.dic{width:28px;height:28px;color:#8696a0;flex:0 0 auto}\n.dmeta{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}\n.dmeta b{font-weight:400;font-size:13.6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.dmeta i{font-style:normal;font-size:11.6px;color:#8696a0}\n.ddl{width:30px;height:30px;color:#8696a0;flex:0 0 auto;display:grid;place-items:center;border-radius:50%}\n.ddl svg{width:18px;height:18px}\n.ddl:hover{background:rgba(134,150,160,.16)}\n.aud{display:flex;align-items:center;gap:9px;min-width:252px;padding:2px 0}\n.apl{width:32px;height:32px;color:#8696a0;flex:0 0 auto;display:grid;place-items:center}\n.apl svg{width:20px;height:20px}\n.awrap{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}\n.abars{position:relative;display:flex;align-items:center;gap:2px;height:26px;cursor:pointer}\n.abars i{flex:1;background:#5b6b73;border-radius:2px;min-width:1.6px}\n.afill{position:absolute;left:0;top:0;bottom:0;width:0;background:rgba(0,168,132,.32);border-radius:3px;pointer-events:none}\n.atime{font-size:11.4px;color:rgba(233,237,239,.6);padding-right:56px}\n.typing{display:flex;gap:4px;align-items:center;min-width:0;padding:11px 13px}\n.typing i{width:7px;height:7px;border-radius:50%;background:#8696a0;animation:bl 1.3s infinite}\n.typing i:nth-child(2){animation-delay:.18s}\n.typing i:nth-child(3){animation-delay:.36s}\n@keyframes bl{0%,60%,100%{opacity:.35}30%{opacity:1}}\n.bmb{position:absolute;top:2px;right:2px;width:26px;height:26px;color:#8696a0;opacity:0;border-radius:4px;display:grid;place-items:center}\n.bmb svg{width:17px;height:17px}\n.row:hover .bmb{opacity:1;background:linear-gradient(90deg,transparent,rgba(32,44,51,.95) 45%)}\n.rme .row:hover .bmb,.rme .bmb{background:linear-gradient(90deg,transparent,rgba(0,92,75,.95) 45%)}\n.bmenu{position:absolute;top:26px;right:0;z-index:20;background:#233138;border-radius:10px;box-shadow:0 6px 22px rgba(0,0,0,.5);padding:6px;min-width:190px}\n.qrow{display:flex;gap:2px;padding:2px 2px 6px;border-bottom:1px solid #2f3b43;margin-bottom:4px}\n.qr{width:29px;height:29px;border-radius:50%;font-size:17px;line-height:1}\n.qr:hover{background:rgba(134,150,160,.16)}\n.mitem{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border-radius:6px;font-size:14px;color:#d1d7db}\n.mitem svg{width:17px;height:17px}\n.mitem:hover{background:rgba(134,150,160,.14)}\n\n.replybar{background:#1d282f;padding:8px 14px;display:flex;align-items:center;gap:10px;flex:0 0 auto}\n.replybar .quote{flex:1;margin:0;background:rgba(11,20,26,.35)}\n.composer{background:#202c33;padding:8px 14px;display:flex;align-items:flex-end;gap:8px;flex:0 0 auto;position:relative}\n.cinwrap{flex:1;background:#2a3942;border-radius:8px;display:flex;align-items:center;padding:0 12px;min-height:42px}\n.cin{flex:1;background:none;border:0;outline:0;color:#e9edef;font:inherit;font-size:15px;resize:none;padding:11px 0;max-height:96px;line-height:1.35}\n.cin::placeholder{color:#8696a0}\n.recbar{flex:1;display:flex;align-items:center;gap:12px;background:#2a3942;border-radius:8px;min-height:42px;padding:0 14px}\n.recdot{width:9px;height:9px;border-radius:50%;background:#f15c6d;animation:pulse 1.1s infinite}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}\n.rectime{font-variant-numeric:tabular-nums;font-size:14px;color:#e9edef;min-width:44px}\n.rechint{flex:1;font-size:13px;color:#8696a0}\n.sendbtn{color:#8696a0}\n.sendbtn.on{color:#00a884}\n\n.pop{position:absolute;z-index:40;background:#233138;border-radius:12px;box-shadow:0 10px 34px rgba(0,0,0,.55);overflow:hidden}\n.emojip{left:12px;bottom:66px;width:352px;padding:10px}\n.egrid{display:grid;grid-template-columns:repeat(9,1fr);gap:2px;max-height:238px;overflow-y:auto}\n.ec{height:35px;border-radius:6px;font-size:21px;line-height:1}\n.ec:hover{background:rgba(134,150,160,.16)}\n.attp{left:56px;bottom:66px;width:214px;padding:8px}\n.scrim{position:absolute;inset:0;z-index:35}\n.lightbox{position:absolute;inset:0;z-index:60;background:rgba(11,20,26,.95);display:flex;align-items:center;justify-content:center}\n.lightbox img{max-width:86%;max-height:86%;border-radius:4px}\n.lbx{position:absolute;top:18px;right:22px;width:40px;height:40px;color:#e9edef}\n\n.dbgbtn{position:absolute;right:0;top:50%;transform:translateY(-50%);z-index:30;width:30px;height:74px;background:#202c33;border-radius:8px 0 0 8px;color:#8696a0;display:grid;place-items:center;border:1px solid #2f3b43;border-right:0}\n.dbgbtn svg{width:17px;height:17px}\n.dbg{position:absolute;top:0;right:0;bottom:0;width:430px;max-width:80%;z-index:70;background:#111b21;border-left:1px solid #2f3b43;display:flex;flex-direction:column;box-shadow:-12px 0 34px rgba(0,0,0,.45)}\n.dhead{height:52px;display:flex;align-items:center;gap:10px;padding:0 10px 0 16px;background:#202c33;flex:0 0 auto}\n.dhead b{flex:1;font-weight:400;font-size:15.5px}\n.dbody{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:12px}\n.dturn{background:#182229;border:1px solid #2f3b43;border-radius:8px;overflow:hidden}\n.dtop{display:flex;align-items:center;gap:8px;padding:7px 10px;background:#1d282f;font-size:12.4px;color:#8696a0}\n.dtop .ok{color:#00a884}\n.dtop .bad{color:#f15c6d}\n.dtop .ms{margin-left:auto;font-variant-numeric:tabular-nums}\n.dlab{font-size:11px;color:#8696a0;text-transform:uppercase;letter-spacing:.5px;padding:8px 10px 3px}\n.dpre{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11.4px;line-height:1.45;color:#c7d1d6;white-space:pre-wrap;word-break:break-word;padding:0 10px 10px;max-height:190px;overflow:auto}\n.dpre.err{color:#f0a6a6}\n.dempty{color:#8696a0;font-size:13.4px;text-align:center;padding:30px 16px}\n\n/* ---------- móvil espejo (20%) ---------- */\n.phone{flex:0 0 clamp(272px,20%,376px);display:flex;align-items:center;justify-content:center;padding:16px 14px;background:#0a1014;pointer-events:none;user-select:none}\n.frame{width:100%;max-width:330px;height:100%;max-height:712px;background:#0b141a;border:9px solid #1c262c;border-radius:34px;box-shadow:0 18px 44px rgba(0,0,0,.55),inset 0 0 0 1px #2b373e;display:flex;flex-direction:column;overflow:hidden}\n.pstat{height:26px;flex:0 0 auto;background:#1f2c33;display:flex;align-items:center;justify-content:space-between;padding:0 14px;font-size:11px;color:#e9edef;font-variant-numeric:tabular-nums}\n.pbat{display:flex;align-items:center;gap:4px}\n.pbat i{display:block;width:17px;height:9px;border:1px solid #e9edef;border-radius:2px;position:relative}\n.pbat i::after{content:'';position:absolute;inset:1.5px;right:5px;background:#e9edef;border-radius:1px}\n.phead{height:50px;flex:0 0 auto;background:#1f2c33;display:flex;align-items:center;gap:8px;padding:0 10px}\n.phead .bk{width:20px;height:20px;color:#e9edef;flex:0 0 auto}\n.phead .av{width:32px;height:32px}\n.pi{flex:1;min-width:0;line-height:1.2}\n.pi b{display:block;font-size:14px;font-weight:400;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.pi span{display:block;font-size:11px;color:#8696a0}\n.pi span.act{color:#00a884}\n.pact{display:flex;gap:14px;color:#aebac1;flex:0 0 auto}\n.pact svg{width:19px;height:19px}\n.pthread{flex:1;overflow-y:auto;padding:8px 9px;background-color:#0b141a;background-image:var(--wp);background-size:104px}\n.pcomp{flex:0 0 auto;background:#1f2c33;padding:7px 8px;display:flex;align-items:center;gap:7px}\n.pin{flex:1;background:#2a3942;border-radius:18px;min-height:33px;display:flex;align-items:center;padding:0 11px;gap:7px;color:#8696a0;overflow:hidden}\n.pin svg{width:16px;height:16px;flex:0 0 auto}\n.ptx{flex:1;font-size:12.4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.ptx.has{color:#e9edef}\n.pmic{width:33px;height:33px;border-radius:50%;background:#00a884;color:#0b141a;display:grid;place-items:center;flex:0 0 auto}\n.pmic svg{width:17px;height:17px}\n\n/* la misma burbuja, a escala móvil */\n.pthread .bwrap{max-width:82%}\n.pthread .txt{font-size:12.4px;padding-right:50px}\n.pthread .meta{font-size:9.6px;right:7px;bottom:4px}\n.pthread .tk{width:14px;height:10px}\n.pthread .img{width:186px;height:126px}\n.pthread .stk{width:104px;height:104px}\n.pthread .vid video{width:186px;height:126px}\n.pthread .docrow{min-width:0;padding:8px;gap:7px}\n.pthread .dic{width:22px;height:22px}\n.pthread .dmeta b{font-size:11.6px}\n.pthread .dmeta i{font-size:10px}\n.pthread .ddl{width:22px;height:22px}\n.pthread .aud{min-width:0;gap:6px}\n.pthread .apl{width:24px;height:24px}\n.pthread .abars{height:19px;gap:1.4px}\n.pthread .atime{font-size:9.6px;padding-right:46px}\n.pthread .enc{font-size:9.8px;padding:6px 9px;margin-bottom:9px}\n.pthread .day span{font-size:10px;padding:4px 9px}\n.pthread .empty{padding:30px 10px;gap:5px}\n.pthread .eico{width:34px;height:34px}\n.pthread .empty b{font-size:13.5px}\n.pthread .empty span{font-size:11.2px}\n.pthread .bmb,.pthread .bmenu{display:none}\n.pthread .quote{padding:3px 6px}\n.pthread .qa{font-size:10.6px}\n.pthread .qt{font-size:11px}\n.pthread .reacts{font-size:10px;bottom:-9px}\n.pthread .typing{padding:9px 11px}\n.pthread .typing i{width:5.5px;height:5.5px}\n.pthread .syschip{font-size:10.6px;padding:5px 9px}\n`,\n\n  render(d2) {\n    const st = d2.state\n    const msgs = st.msgs || []\n    const name = d2.param('bellaName') || 'Bella'\n    const avatar = d2.param('bellaAvatar') || ''\n    const phone = d2.param('bellaPhone') || ''\n    const avCss = avatar ? 'background-image:url(' + esc(avatar) + ')' : ''\n    const status = st.bellaTyping ? T.typing : (st.presence === 'online' ? T.online : (st.lastSeen || T.online))\n    const actCls = st.bellaTyping || st.presence === 'online' ? ' act' : ''\n    const last = msgs.length ? msgs[msgs.length - 1] : null\n    const preview = last ? (last.mine ? T.you + ': ' : '') + (last.kind === 'text' || last.kind === 'system' ? (last.body || '') : (KL[last.kind] || '') + (last.caption ? ' · ' + last.caption : '')) : 'Toca para empezar a chatear'\n    const thread = threadHTML(msgs, st, name)\n    const overlay = st.emojiOpen || st.attachOpen || st.menuFor\n    const clock = hhmm(Date.now())\n\n    return '<div class=\"root\">' +\n\n      '<div class=\"web\">' +\n        '<aside class=\"side\">' +\n          '<div class=\"shead\"><div class=\"av meav\"></div><div class=\"sp\"></div>' +\n            '<button class=\"ib\">' + IC.newchat + '</button><button class=\"ib\">' + IC.menu + '</button></div>' +\n          '<div class=\"srow\"><div class=\"sbox\">' + IC.search + '<span>' + T.searchPh + '</span></div></div>' +\n          '<div class=\"tabs\">' + T.tabs.map((t, i) => '<span class=\"tab' + (i === 0 ? ' on' : '') + '\">' + t + '</span>').join('') + '</div>' +\n          '<div class=\"convs\"><div class=\"conv\"><div class=\"av\" style=\"' + avCss + '\"></div>' +\n            '<div class=\"cmid\"><div class=\"cn\"><b>' + esc(name) + '</b><i>' + (last ? hhmm(last.ts) : clock) + '</i></div>' +\n            '<div class=\"cp\">' + esc(preview) + '</div></div></div></div>' +\n        '</aside>' +\n\n        '<section class=\"chat\">' +\n          '<div class=\"chead\"><div class=\"av\" style=\"' + avCss + '\"></div>' +\n            '<div class=\"ci\"><b>' + esc(name) + '</b><span class=\"' + actCls.trim() + '\">' + esc(status) + '</span></div>' +\n            '<button class=\"ib\">' + IC.search + '</button><button class=\"ib\">' + IC.menu + '</button></div>' +\n\n          '<div class=\"thread\" id=\"thread\">' + thread + '</div>' +\n\n          (st.replyTo ? '<div class=\"replybar\">' + quoteHTML(st.replyTo) + '<button class=\"ib\" id=\"reply-x\">' + IC.x + '</button></div>' : '') +\n\n          '<div class=\"composer\">' +\n            (st.recording\n              ? '<button class=\"ib\" id=\"rec-cancel\" title=\"Cancelar\">' + IC.trash + '</button>' +\n                '<div class=\"recbar\"><span class=\"recdot\"></span><span class=\"rectime\" id=\"rectime\">0:00</span><span class=\"rechint\">' + T.recHint + '</span></div>' +\n                '<button class=\"ib sendbtn on\" id=\"rec-send\">' + IC.send + '</button>'\n              : '<button class=\"ib\" id=\"btn-emoji\">' + IC.emoji + '</button>' +\n                '<button class=\"ib\" id=\"btn-attach\">' + IC.clip + '</button>' +\n                '<div class=\"cinwrap\"><textarea class=\"cin\" id=\"cin\" rows=\"1\" placeholder=\"' + T.composerPh + '\"></textarea></div>' +\n                '<button class=\"ib sendbtn\" id=\"btn-send\" style=\"display:none\">' + IC.send + '</button>' +\n                '<button class=\"ib\" id=\"btn-mic\">' + IC.mic + '</button>') +\n\n            (st.emojiOpen ? '<div class=\"pop emojip\"><div class=\"egrid\">' + EMO.map((e) => '<button class=\"ec\" data-emo=\"' + e + '\">' + e + '</button>').join('') + '</div></div>' : '') +\n            (st.attachOpen ? '<div class=\"pop attp\"><button class=\"mitem\" data-att=\"media\">' + IC.image + '<span>' + T.attPhotos + '</span></button><button class=\"mitem\" data-att=\"doc\">' + IC.doc + '<span>' + T.attDoc + '</span></button></div>' : '') +\n          '</div>' +\n\n          (overlay ? '<div class=\"scrim\" id=\"scrim\"></div>' : '') +\n          (d2.param('debugEnabled') !== false && !st.debugOpen ? '<button class=\"dbgbtn\" id=\"dbg-open\" title=\"Inspector\">' + IC.bug + '</button>' : '') +\n        '</section>' +\n      '</div>' +\n\n      '<div class=\"phone\"><div class=\"frame\">' +\n        '<div class=\"pstat\"><span>' + clock + '</span><span class=\"pbat\">5G<i></i></span></div>' +\n        '<div class=\"phead\"><span class=\"bk\">' + IC.back + '</span><div class=\"av\" style=\"' + avCss + '\"></div>' +\n          '<div class=\"pi\"><b>' + esc(name) + '</b><span class=\"' + actCls.trim() + '\">' + esc(status) + '</span></div>' +\n          '<div class=\"pact\">' + IC.video + IC.phone + '</div></div>' +\n        '<div class=\"pthread\" id=\"pthread\">' + thread + '</div>' +\n        '<div class=\"pcomp\"><div class=\"pin\">' + IC.emoji + '<span class=\"ptx\" id=\"ptx\">' + T.composerPh + '</span>' + IC.clip + '</div>' +\n          '<span class=\"pmic\">' + IC.mic + '</span></div>' +\n      '</div></div>' +\n\n      (st.debugOpen ? debugHTML(st) : '') +\n      (st.lightbox ? '<div class=\"lightbox\" id=\"lightbox\"><img src=\"' + esc(st.lightbox) + '\"><button class=\"ib lbx\">' + IC.x + '</button></div>' : '') +\n      '<input type=\"file\" id=\"fin\" style=\"display:none\">' +\n    '</div>'\n  },\n\n  mounted(d2, root) {\n    if (!d2.state.msgs) d2.setState({ msgs: [], log: [], presence: 'idle', lastSeen: 'últ. vez hoy a las ' + hhmm(Date.now()) })\n    ensureAudio(d2)\n\n    d2.on('#cin', 'input', (e, el) => { draft = el.value; syncDraft(d2) })\n    d2.on('#cin', 'keydown', (e, el) => {\n      if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); draft = el.value; sendText(d2) }\n    })\n    d2.on('#btn-send', 'click', () => sendText(d2))\n    d2.on('#btn-emoji', 'click', () => { d2.state.attachOpen = false; d2.state.emojiOpen = !d2.state.emojiOpen })\n    d2.on('#btn-attach', 'click', () => { d2.state.emojiOpen = false; d2.state.attachOpen = !d2.state.attachOpen })\n    d2.on('.ec', 'click', (e, el) => {\n      const ta = d2.$('#cin')\n      draft = (ta ? ta.value : draft) + el.dataset.emo\n      if (ta) { ta.value = draft; ta.focus() }\n      syncDraft(d2)\n    })\n    d2.on('#scrim', 'click', () => d2.setState({ emojiOpen: false, attachOpen: false, menuFor: '' }))\n\n    d2.on('.mitem[data-att]', 'click', (e, el) => {\n      const fin = d2.$('#fin')\n      if (!fin) return\n      fin.accept = el.dataset.att === 'media' ? 'image/*,video/*' : ''\n      d2.state.attachOpen = false\n      fin.click()\n    })\n    d2.on('#fin', 'change', (e, el) => { const f = el.files && el.files[0]; el.value = ''; if (f) sendFile(d2, f) })\n\n    d2.on('#btn-mic', 'click', () => startRec(d2))\n    d2.on('#rec-send', 'click', () => stopRec(d2, true))\n    d2.on('#rec-cancel', 'click', () => stopRec(d2, false))\n\n    d2.on('.bmb', 'click', (e, el) => { e.stopPropagation(); d2.setState({ emojiOpen: false, attachOpen: false, menuFor: d2.state.menuFor === el.dataset.menu ? '' : el.dataset.menu }) })\n    d2.on('.qr', 'click', (e, el) => {\n      const id = el.dataset.react, emo = el.dataset.emo\n      d2.state.msgs = (d2.state.msgs || []).map((m) => {\n        if (m.id !== id) return m\n        const r = Object.assign({}, m.reactions || {})\n        if (r[emo]) delete r[emo]; else r[emo] = 1\n        return Object.assign({}, m, { reactions: r })\n      })\n      d2.state.menuFor = ''\n    })\n    d2.on('.mitem[data-replyto]', 'click', (e, el) => {\n      const m = (d2.state.msgs || []).find((x) => x.id === el.dataset.replyto)\n      if (!m) return\n      d2.setState({\n        menuFor: '',\n        replyTo: { id: m.id, mine: m.mine, author: d2.param('bellaName') || 'Bella', kind: m.kind, preview: (m.body || m.caption || m.fname || '').slice(0, 90) }\n      })\n      const ta = d2.$('#cin'); if (ta) ta.focus()\n    })\n    d2.on('#reply-x', 'click', () => { d2.state.replyTo = null })\n\n    d2.on('.img', 'click', (e, el) => { if (el.dataset.open) d2.state.lightbox = el.dataset.open })\n    d2.on('#lightbox', 'click', () => { d2.state.lightbox = '' })\n\n    d2.on('.apl', 'click', (e, el) => {\n      const id = el.dataset.play, src = el.dataset.src\n      if (!ensureAudio(d2)) return\n      if (d2.state.playingId === id) { audioEl.pause(); d2.state.playingId = ''; return }\n      if (audioEl.src !== src) { audioEl.src = src; audioEl.currentTime = 0 }\n      audioEl.play().catch(() => {})\n      d2.state.playingId = id\n    })\n    d2.on('.abars', 'click', (e, el) => {\n      if (!audioEl || d2.state.playingId !== el.dataset.seek || !audioEl.duration) return\n      const r = el.getBoundingClientRect()\n      audioEl.currentTime = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) * audioEl.duration\n      paintAudio(d2)\n    })\n\n    d2.on('#dbg-open', 'click', () => { d2.state.debugOpen = true })\n    d2.on('#dbg-close', 'click', () => { d2.state.debugOpen = false })\n\n    d2.on('#thread', 'scroll', (e, el) => {\n      stick = el.scrollHeight - el.scrollTop - el.clientHeight < 60\n      mirrorScroll(d2)\n    })\n\n    d2.onUnmount(() => {\n      try { if (audioEl) audioEl.pause() } catch (x) {}\n      if (recTimer) clearInterval(recTimer)\n      try { if (rec && rec.state === 'recording') rec.stream.getTracks().forEach((t) => t.stop()) } catch (x) {}\n      blobs.forEach((u) => { try { URL.revokeObjectURL(u) } catch (x) {} })\n      blobs = []\n    })\n  },\n\n  updated(d2) {\n    const ta = d2.$('#cin')\n    if (ta && draft && !ta.value) ta.value = draft\n    syncDraft(d2)\n    paintAudio(d2)\n    const th = d2.$('#thread'), pt = d2.$('#pthread')\n    if (stick) {\n      if (th) th.scrollTop = th.scrollHeight\n      if (pt) pt.scrollTop = pt.scrollHeight\n    } else {\n      mirrorScroll(d2)\n    }\n  }\n}\n\n/* ---------------------------------- inspector ---------------------------------- */\nfunction debugHTML(st) {\n  const log = st.log || []\n  const body = !log.length\n    ? '<div class=\"dempty\">' + T.dbgEmpty + '</div>'\n    : log.slice().reverse().map((e) =>\n        '<div class=\"dturn\"><div class=\"dtop\"><span class=\"' + (e.error ? 'bad' : 'ok') + '\">' + (e.error ? 'ERROR' : 'OK') + '</span><span>' + hhmm(e.t) + '</span><span class=\"ms\">' + e.ms + ' ms</span></div>' +\n        '<div class=\"dlab\">Petición</div><div class=\"dpre\">' + esc(JSON.stringify(e.request, null, 2)) + '</div>' +\n        (e.error\n          ? '<div class=\"dlab\">Error</div><div class=\"dpre err\">' + esc(e.error) + '</div>'\n          : '<div class=\"dlab\">Respuesta</div><div class=\"dpre\">' + esc(JSON.stringify(e.response, null, 2)) + '</div>') +\n        '</div>').join('')\n  return '<div class=\"dbg\"><div class=\"dhead\">' + IC.bug + '<b>Inspector</b><button class=\"ib\" id=\"dbg-close\">' + IC.x + '</button></div><div class=\"dbody\">' + body + '</div></div>'\n}\n\n/* ---------------------------------- helpers de DOM ---------------------------------- */\nfunction ensureAudio(d2) {\n  if (audioEl) return audioEl\n  if (typeof Audio === 'undefined') return null\n  audioEl = new Audio()\n  audioEl.addEventListener('timeupdate', () => paintAudio(d2))\n  audioEl.addEventListener('ended', () => { d2.state.playingId = ''; paintAudio(d2) })\n  return audioEl\n}\n\nfunction syncDraft(d2) {\n  const has = !!draft.trim()\n  const px = d2.$('#ptx')\n  if (px) { px.textContent = has ? draft : T.composerPh; px.className = 'ptx' + (has ? ' has' : '') }\n  const sb = d2.$('#btn-send'), mb = d2.$('#btn-mic')\n  if (sb) sb.style.display = has ? '' : 'none'\n  if (mb) mb.style.display = has ? 'none' : ''\n}\n\nfunction mirrorScroll(d2) {\n  const th = d2.$('#thread'), pt = d2.$('#pthread')\n  if (!th || !pt) return\n  const den = th.scrollHeight - th.clientHeight\n  const r = den > 0 ? th.scrollTop / den : 1\n  pt.scrollTop = r * Math.max(0, pt.scrollHeight - pt.clientHeight)\n}\n\nfunction paintAudio(d2) {\n  const id = d2.state.playingId\n  d2.$$('.afill').forEach((el) => { if (el.dataset.fill !== id) el.style.width = '0' })\n  d2.$$('.apl').forEach((el) => { el.innerHTML = el.dataset.play === id ? IC.pause : IC.play })\n  if (!id || !audioEl) return\n  const dur = audioEl.duration && isFinite(audioEl.duration) ? audioEl.duration : 0\n  const pct = dur ? (audioEl.currentTime / dur) * 100 : 0\n  d2.$$('.afill').forEach((el) => { if (el.dataset.fill === id) el.style.width = pct + '%' })\n  d2.$$('.atime').forEach((el) => { if (el.dataset.time === id) el.textContent = mmss(audioEl.currentTime || 0) })\n}\n\n/* ---------------------------------- envío ---------------------------------- */\nfunction pushMsg(d2, m) { d2.state.msgs = (d2.state.msgs || []).concat([m]); stick = true }\nfunction setStatus(d2, id, s) {\n  d2.state.msgs = (d2.state.msgs || []).map((m) => (m.id === id && m.status !== 'read' ? Object.assign({}, m, { status: s }) : m))\n}\n\nfunction sendText(d2) {\n  const ta = d2.$('#cin')\n  const txt = ((ta ? ta.value : draft) || '').trim()\n  if (!txt || d2.state.sending) return\n  if (ta) ta.value = ''\n  draft = ''\n  syncDraft(d2)\n  dispatch(d2, { kind: 'text', body: txt })\n}\n\nfunction sendFile(d2, f) {\n  const url = URL.createObjectURL(f)\n  blobs.push(url)\n  const mime = f.type || ''\n  const kind = mime.indexOf('image/') === 0 ? 'image' : (mime.indexOf('video/') === 0 ? 'video' : (mime.indexOf('audio/') === 0 ? 'audio' : 'document'))\n  const kb = f.size < 1048576 ? Math.max(1, Math.round(f.size / 1024)) + ' kB' : (f.size / 1048576).toFixed(1) + ' MB'\n  const ta = d2.$('#cin')\n  const cap = ((ta ? ta.value : draft) || '').trim()\n  if (ta) ta.value = ''\n  draft = ''\n  syncDraft(d2)\n  dispatch(d2, { kind: kind, caption: cap, media: url, fname: f.name, fsize: kb + ' · ' + (f.name.split('.').pop() || '').toUpperCase(), durSec: 0 })\n}\n\nfunction dispatch(d2, part) {\n  const prev = d2.state.msgs || []\n  const history = prev.filter((m) => m.kind !== 'system').slice(-20).map((m) => ({\n    from: m.mine ? 'user' : 'bella', kind: m.kind, text: m.body || m.caption || m.fname || ''\n  }))\n  const m = Object.assign({\n    id: uid('u'), mine: true, kind: 'text', body: '', caption: '', media: '', fname: '', fsize: '', durSec: 0,\n    ts: Date.now(), status: 'pending', replyTo: d2.state.replyTo || null, reactions: {}\n  }, part)\n  pushMsg(d2, m)\n  d2.setState({ replyTo: null, sending: true, presence: 'online' })\n  setTimeout(() => setStatus(d2, m.id, 'sent'), 320)\n  setTimeout(() => setStatus(d2, m.id, 'delivered'), 820)\n  askBella(d2, m, history)\n}\n\nasync function askBella(d2, m, history) {\n  const request = {\n    sessionId: SID,\n    message: { kind: m.kind, text: m.body || m.caption || '', fileName: m.fname || '', durSec: m.durSec || 0 },\n    history: history\n  }\n  const t0 = Date.now()\n  let res = null, err = null\n  d2.state.bellaTyping = true\n  try {\n    res = await d2.rush(d2.param('api')).invoke(request, { headers: { Authorization: d2.param('apiKey') || '' } })\n  } catch (e) {\n    err = String((e && e.message) || e)\n  }\n  const ms = Date.now() - t0\n  d2.state.log = (d2.state.log || []).concat([{ t: Date.now(), ms: ms, request: request, response: res, error: err }]).slice(-25)\n  setStatus(d2, m.id, 'read')\n\n  const replies = (res && (res.replies || (res.data && res.data.replies))) || null\n  if (!replies || !replies.length) {\n    d2.state.bellaTyping = false\n    pushMsg(d2, { id: uid('e'), mine: false, kind: 'system', body: T.noReply + (err ? ' (' + err + ')' : ''), ts: Date.now(), reactions: {} })\n    d2.setState({ sending: false, presence: 'idle', lastSeen: 'últ. vez hoy a las ' + hhmm(Date.now()) })\n    return\n  }\n  for (let i = 0; i < replies.length; i++) {\n    const r = replies[i]\n    d2.state.bellaTyping = true\n    await wait(Math.max(400, Math.min(3000, r.delayMs || 1100)))\n    d2.state.bellaTyping = false\n    const kind = r.kind || 'text'\n    pushMsg(d2, {\n      id: uid('b'), mine: false, kind: kind,\n      body: kind === 'text' ? (r.text || '') : '',\n      caption: kind === 'text' ? '' : (r.text || ''),\n      media: r.media || '', fname: r.fileName || '', fsize: r.fileSize || '',\n      durSec: r.durSec || 0, ts: Date.now(), status: '', replyTo: null, reactions: {}\n    })\n  }\n  d2.setState({ sending: false, presence: 'idle', lastSeen: 'últ. vez hoy a las ' + hhmm(Date.now()) })\n}\n\n/* ---------------------------------- grabación de voz ---------------------------------- */\nasync function startRec(d2) {\n  if (d2.state.recording) return\n  try {\n    const stream = await navigator.mediaDevices.getUserMedia({ audio: true })\n    recChunks = []\n    rec = new MediaRecorder(stream)\n    rec.ondataavailable = (e) => { if (e.data && e.data.size) recChunks.push(e.data) }\n    rec.start()\n    recStart = Date.now()\n    d2.state.recording = true\n    recTimer = setInterval(() => {\n      const el = d2.$('#rectime')\n      if (el) el.textContent = mmss((Date.now() - recStart) / 1000)\n    }, 250)\n  } catch (e) {\n    pushMsg(d2, { id: uid('e'), mine: false, kind: 'system', body: 'No se pudo acceder al micrófono: ' + String((e && e.message) || e), ts: Date.now(), reactions: {} })\n  }\n}\n\nfunction stopRec(d2, keep) {\n  if (!rec || rec.state !== 'recording') { d2.state.recording = false; rec = null; return }\n  const secs = Math.max(1, Math.round((Date.now() - recStart) / 1000))\n  if (recTimer) { clearInterval(recTimer); recTimer = null }\n  rec.onstop = () => {\n    try { rec.stream.getTracks().forEach((t) => t.stop()) } catch (x) {}\n    if (keep && recChunks.length) {\n      const blob = new Blob(recChunks, { type: rec.mimeType || 'audio/webm' })\n      const url = URL.createObjectURL(blob)\n      blobs.push(url)\n      dispatch(d2, { kind: 'audio', media: url, durSec: secs, fname: 'nota-de-voz.webm' })\n    }\n    rec = null\n    recChunks = []\n  }\n  d2.state.recording = false\n  try { rec.stop() } catch (x) { rec = null }\n}"}},"api":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"api"}}}},"apiKey":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"apiKey"}}}},"bellaName":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bellaName"}}}},"bellaAvatar":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bellaAvatar"}}}},"bellaPhone":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"bellaPhone"}}}},"debugEnabled":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"debugEnabled"}}}}}}}}}},{"_id":"6adfb7ea580bb624d29aa2f8","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"filaRetro","package":"lucas-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Fila da retro — cada squad declara quantos slots de 5 min vai usar; a ordem é a de chegada.\n// Dados e regras ficam no listener (param `api`); a tela só lê o estado e manda as ações.\n// Durante a retro, o Lucas ajusta o andamento ao vivo (param `controle`) e o socket (param `socket`) avisa as outras telas.\n\n// Gradientes das 8 bolinhas do logo da Data2, no sentido horário a partir do topo.\nconst CORES = [\n  ['#489afa', '#8448fa'],\n  ['#f74848', '#f7a048'],\n  ['#fadc48', '#bffa48'],\n  ['#48faa1', '#4bfafa'],\n  ['#4dfafa', '#4da4fa'],\n  ['#fa48dc', '#f74882'],\n  ['#8c48fa', '#dc48fa'],\n  ['#bffa48', '#66fa48'],\n]\nconst LOGO_XY = [[21, 0], [36, 6], [42, 21], [36, 36], [21, 41], [6, 36], [0, 21], [6, 6]]\n\nconst MIN_POR_SLOT = 5\nconst META_SLOTS = 12\nconst MAX_SLOTS = 6\nconst FUSO = 'America/Sao_Paulo'\nconst HORA_RETRO_UTC = 20 // a retro começa sexta às 17:00 em Brasília (UTC-3)\nconst TOLERANCIA_MIN = 10 // conversa aberta, esperando todos chegarem\nconst MIN_MS = 60000\nconst SIM_VELOCIDADE = 60 // na simulação, 1 min de retro passa em 1 s\nconst FILA_EXEMPLO = [['Squad Gabz', 2], ['Squad Fernando', 3], ['Squad Mylena', 2], ['Squad Aninha', 1], ['Squad Vini', 2], ['Squad Bazan', 1]]\n\n// Controle ao vivo e tempo real. O socket só fica ligado na janela da retro, para não gastar à toa.\nconst ADMIN_EMAIL = 'lucas.barduco@data2.app'\nconst CHAVE_LOCAL = 'fila-retro-chave-controle'\nconst SALA = 'fila-retro'\nconst JANELA_ANTES_MS = 20 * MIN_MS // a janela abre às 16:40…\nconst JANELA_DEPOIS_MS = 150 * MIN_MS // …e fecha às 19:30 de sexta\nconst BUSCA_FORA_MS = 60000 // fora da janela, relê a fila a cada 1 min\nconst BUSCA_JANELA_MS = 120000 // na janela com o socket ligado, só uma conferência a cada 2 min\nconst BUSCA_SEM_SOCKET_MS = 20000 // na janela, se o socket não ligar\nconst ACOES = ['comecar', 'segurar', 'proximo', 'conversa', 'mais', 'menos', 'encerrar', 'auto']\nconst ROTULO_ACAO = {\n  comecar: 'Vez liberada',\n  segurar: 'Conversa segurada',\n  proximo: 'Vez passada para o próximo',\n  conversa: 'Conversa aberta',\n  mais: '+1 min',\n  menos: '−1 min',\n  encerrar: 'Retro encerrada',\n  auto: 'De volta ao automático',\n}\n\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst grad = (i) => {\n  const [a, b] = CORES[((i % CORES.length) + CORES.length) % CORES.length]\n  return `linear-gradient(90deg, ${a}, ${b})`\n}\nconst curto = (nome) => String(nome).replace(/^Squad\\s+/i, '')\nconst stamp = (iso) => {\n  const t = Date.parse(iso || '')\n  return Number.isFinite(t) ? t : Number.MAX_SAFE_INTEGER\n}\nconst plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`\n\nfunction quando(iso) {\n  const t = Date.parse(iso || '')\n  if (!Number.isFinite(t)) return ''\n  return new Date(t)\n    .toLocaleString('pt-BR', { weekday: 'short', hour: '2-digit', minute: '2-digit', timeZone: FUSO })\n    .replace('.', '')\n}\n\nconst inicioRetro = (ciclo) => Date.parse(`${ciclo}T${HORA_RETRO_UTC}:00:00Z`)\nconst relogio = (t) => new Date(t).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', timeZone: FUSO })\nconst mmss = (ms) => {\n  const s = Math.max(0, Math.ceil(ms / 1000))\n  return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`\n}\n\nfunction faltaTexto(ms) {\n  const min = Math.max(1, Math.ceil(ms / MIN_MS))\n  const d = Math.floor(min / 1440)\n  const h = Math.floor((min % 1440) / 60)\n  const m = min % 60\n  if (d) return `${d} d ${h} h`\n  if (h) return `${h} h ${String(m).padStart(2, '0')} min`\n  return `${m} min`\n}\n\nconst esperar = (ms) => new Promise((r) => setTimeout(r, ms))\nconst lerChave = () => { try { return localStorage.getItem(CHAVE_LOCAL) || '' } catch (e) { return '' } }\nconst gravarChave = (k) => { try { localStorage.setItem(CHAVE_LOCAL, k) } catch (e) { /* sem storage: a chave é pedida de novo */ } }\nconst apagarChave = () => { try { localStorage.removeItem(CHAVE_LOCAL) } catch (e) { /* idem */ } }\nconst souAdmin = (d2) => d2.state.euEmail === ADMIN_EMAIL\n\n// Relógio de verdade: o do navegador corrigido pelo do servidor.\nconst agoraReal = (d2) => Date.now() + (d2.state.desvio || 0)\n\n// Relógio da tela: o de verdade, ou o acelerado da simulação.\nfunction agora(d2) {\n  const s = d2.state\n  if (s.sim) return inicioRetro(s.sim.ciclo) + (Date.now() - s.sim.desde) * SIM_VELOCIDADE\n  return agoraReal(d2)\n}\n\n// Janela em que a retro pode estar acontecendo: sexta das 16:40 às 19:30.\nfunction naJanela(d2) {\n  const dados = d2.state.dados\n  if (!dados) return false\n  const t = agoraReal(d2)\n  const ini = inicioRetro(dados.ciclo)\n  return t >= ini - JANELA_ANTES_MS && t <= ini + JANELA_DEPOIS_MS\n}\n\n// Na simulação com a fila vazia, usa uma fila de exemplo para mostrar como a retro vai aparecer.\nfunction filaDaSimulacao(v) {\n  if (v.fila.length) return { fila: v.fila, exemplo: false }\n  let minuto = 0\n  const fila = FILA_EXEMPLO.filter(([s]) => v.squads.includes(s)).map(([s, n], k) => {\n    const r = { squad: s, slots: n, n, pos: k + 1, de: minuto, ate: minuto + n * MIN_POR_SLOT, cor: v.squads.indexOf(s), chegada: new Date(k * 1000).toISOString() }\n    minuto += n * MIN_POR_SLOT\n    return r\n  })\n  return { fila, exemplo: true }\n}\n\nfunction rotuloCiclo(ciclo) {\n  const m = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(ciclo || '')\n  return m ? `sexta, ${m[3]}/${m[2]}` : 'sexta'\n}\n\n// Quem está usando a tela: a sessão do builder no preview. Serve para o \"por\" e para mostrar o controle ao Lucas.\n// Lê apenas nome e e-mail; as credenciais da sessão trazem outros campos que nunca saem daqui.\nasync function descobrirQuemSou(d2) {\n  const tentativas = [\n    () => d2.layer.environment.layers.HostApplicationLayer.getProcess('Data2BuilderAuthentication').instance.getSelectedSession(),\n    () => ({ credentials: d2.auth().user }),\n  ]\n  for (const tentar of tentativas) {\n    try {\n      let s = tentar()\n      if (s && typeof s.then === 'function') s = await s\n      const u = s && s.credentials\n      const nome = u && (u.name || u.email)\n      if (nome) {\n        d2.setState({ eu: String(nome).slice(0, 80), euEmail: String(u.email || '').trim().toLowerCase() })\n        return\n      }\n    } catch (e) { /* segue para a próxima forma */ }\n  }\n}\n\nfunction quemSou(d2) {\n  return d2.state.eu || ''\n}\n\n// Ordena por chegada, ignora duplicata do mesmo squad e calcula a posição de cada um na hora da retro.\nfunction derivar(dados) {\n  const squads = (dados && dados.squads) || []\n  const vistos = new Set()\n  const reservas = ((dados && dados.reservas) || [])\n    .filter((r) => squads.includes(r.squad))\n    .slice()\n    .sort((a, b) => stamp(a.chegada) - stamp(b.chegada))\n    .filter((r) => (vistos.has(r.squad) ? false : (vistos.add(r.squad), true)))\n  const porSquad = new Map(reservas.map((r) => [r.squad, r]))\n  const fila = []\n  let minuto = 0\n  for (const r of reservas) {\n    const n = Math.max(0, Math.round(Number(r.slots) || 0))\n    if (n === 0) continue\n    fila.push({ ...r, n, pos: fila.length + 1, de: minuto, ate: minuto + n * MIN_POR_SLOT, cor: squads.indexOf(r.squad) })\n    minuto += n * MIN_POR_SLOT\n  }\n  const posicao = new Map(fila.map((r) => [r.squad, r.pos]))\n  const semApresentacao = reservas.filter((r) => !(Number(r.slots) > 0))\n  const pendentes = squads.filter((s) => !porSquad.has(s))\n  const totalSlots = fila.reduce((s, r) => s + r.n, 0)\n  return { squads, porSquad, posicao, fila, semApresentacao, pendentes, totalSlots, totalMin: totalSlots * MIN_POR_SLOT }\n}\n\nconst timersAviso = new WeakMap()\n\nfunction avisar(d2, tipo, txt) {\n  d2.state.aviso = { tipo, txt }\n  clearTimeout(timersAviso.get(d2))\n  timersAviso.set(d2, setTimeout(() => { d2.state.aviso = null }, tipo === 'erro' ? 7000 : 3500))\n}\n\nfunction mensagemDeErro(e) {\n  const msg = String((e && e.message) || e || '')\n  if (/401|403|unauthori|permiss/i.test(msg)) {\n    return `Sem permissão. Esta tela só funciona aberta pelo preview do builder do lucas-playground. (${msg.slice(0, 160)})`\n  }\n  return `Não deu para falar com o servidor (${msg.slice(0, 120) || 'erro desconhecido'}).`\n}\n\n// ---------------------------------------------------------------- cronograma ao vivo\n\n// Quem pode ter vez na retro: os declarados agora e quem saiu da fila neste ciclo.\n// Quem saiu depois de a vez começar continua no histórico; quem saiu antes da vez é pulado.\nfunction participantes(v, movimentos) {\n  const lista = v.fila.map((r) => ({ squad: r.squad, n: r.n, cor: r.cor, chegada: stamp(r.chegada), saiu: Infinity }))\n  const presentes = new Set(lista.map((r) => r.squad))\n  const hist = new Map()\n  const evs = (movimentos || []).filter((e) => e && v.squads.includes(e.squad)).slice().sort((a, b) => stamp(a.em) - stamp(b.em))\n  for (const e of evs) {\n    const x = hist.get(e.squad) || { chegada: null, n: 0, saiu: null, nAntes: 0 }\n    const em = stamp(e.em)\n    if (e.acao === 'declarou') hist.set(e.squad, { chegada: em, n: Number(e.depois) || 0, saiu: null, nAntes: 0 })\n    else if (e.acao === 'alterou' && Number(e.depois) > 0) hist.set(e.squad, { ...x, n: Number(e.depois), saiu: null })\n    else if (e.acao === 'alterou' || e.acao === 'retirou') hist.set(e.squad, { ...x, n: 0, saiu: em, nAntes: Number(e.antes) || 0 })\n  }\n  for (const [squad, x] of hist) {\n    if (presentes.has(squad) || x.saiu == null || !(x.nAntes > 0)) continue\n    const reg = v.porSquad.get(squad)\n    const chegada = x.chegada != null ? x.chegada : reg ? stamp(reg.chegada) : null\n    if (chegada == null || chegada === Number.MAX_SAFE_INTEGER) continue\n    lista.push({ squad, n: x.nAntes, cor: v.squads.indexOf(squad), chegada, saiu: x.saiu })\n  }\n  return lista.sort((a, b) => a.chegada - b.chegada)\n}\n\n// O cronograma da retro: o automático (conversa aberta + vezes na ordem da fila) com os ajustes ao vivo do Lucas.\n// É recalculado do zero a partir da fila e da lista de ações, então todas as telas chegam ao mesmo resultado.\nfunction cronograma(lista, ciclo, eventos, t) {\n  const ini = inicioRetro(ciclo)\n  const comeco = ini + TOLERANCIA_MIN * MIN_MS\n  let evs = (eventos || [])\n    .map((e) => ({ acao: e.acao, em: stamp(e.em) }))\n    .filter((e) => ACOES.includes(e.acao) && e.em <= t)\n    .sort((a, b) => a.em - b.em)\n  const corte = evs.map((e) => e.acao).lastIndexOf('auto')\n  if (corte >= 0) evs = evs.slice(corte + 1)\n  const inicio = evs.length ? Math.min(ini, evs[0].em) : ini\n  const dur = (r) => r.n * MIN_POR_SLOT * MIN_MS\n  const feitos = []\n  const falou = new Set()\n  const sobra = new Map()\n  let st = { fase: 'conversa', desde: inicio, fim: comeco, retomar: null }\n\n  const disponivel = (x) => lista.find((r) => !falou.has(r.squad) && r.chegada <= x && r.saiu > x) || null\n  const chegaDepois = (x) => {\n    let c = Infinity\n    for (const r of lista) if (!falou.has(r.squad) && r.chegada > x && r.saiu > r.chegada && r.chegada < c) c = r.chegada\n    return c\n  }\n  const fechar = (ate) => {\n    if (st.fase === 'conversa' || st.fase === 'turno') feitos.push({ ...st, fim: ate })\n  }\n  const darVez = (x, r) => {\n    if (!r) { st = { fase: 'esgotou', desde: x, fim: Infinity }; return }\n    falou.add(r.squad)\n    let fim = x + (sobra.has(r.squad) ? sobra.get(r.squad) : dur(r))\n    sobra.delete(r.squad)\n    if (r.saiu > x && r.saiu < fim) fim = r.saiu // saiu da fila no meio da própria vez\n    st = { fase: 'turno', squad: r.squad, cor: r.cor, n: r.n, desde: x, fim }\n  }\n  const avancar = (x) => {\n    for (let i = 0; i < 500; i++) {\n      if (st.fase === 'esgotou') {\n        const c = chegaDepois(st.desde)\n        if (c > x) return\n        darVez(c, disponivel(c))\n        continue\n      }\n      if (st.fase === 'encerrada' || !(st.fim <= x)) return\n      const quando = st.fim\n      const volta = st.fase === 'conversa' && st.retomar && st.retomar.saiu > quando ? st.retomar : null\n      fechar(quando)\n      darVez(quando, volta || disponivel(quando))\n    }\n  }\n\n  for (const e of evs) {\n    avancar(e.em)\n    const x = e.em\n    const a = e.acao\n    if (a === 'comecar' || a === 'proximo') {\n      if (st.fase === 'conversa') {\n        const r = st.retomar && st.retomar.saiu > x ? st.retomar : disponivel(x)\n        fechar(x)\n        darVez(x, r)\n      } else if (st.fase === 'turno' && a === 'proximo') {\n        fechar(x)\n        darVez(x, disponivel(x))\n      } else if (st.fase === 'esgotou' || st.fase === 'encerrada') {\n        const r = disponivel(x)\n        if (r) darVez(x, r)\n      }\n    } else if (a === 'segurar') {\n      if (st.fase === 'conversa') st = { ...st, fim: Infinity }\n    } else if (a === 'conversa') {\n      if (st.fase === 'turno') {\n        const r = lista.find((y) => y.squad === st.squad) || null\n        sobra.set(st.squad, Math.max(0, st.fim - x))\n        fechar(x)\n        st = { fase: 'conversa', desde: x, fim: Infinity, retomar: r }\n      } else if (st.fase === 'esgotou' || st.fase === 'encerrada') {\n        st = { fase: 'conversa', desde: x, fim: Infinity, retomar: null }\n      }\n    } else if (a === 'mais' || a === 'menos') {\n      if ((st.fase === 'turno' || st.fase === 'conversa') && Number.isFinite(st.fim)) {\n        st = { ...st, fim: a === 'mais' ? st.fim + MIN_MS : Math.max(x, st.fim - MIN_MS) }\n      }\n    } else if (a === 'encerrar') {\n      if (st.fase !== 'encerrada') {\n        fechar(x)\n        st = { fase: 'encerrada', desde: x, fim: Infinity }\n      }\n    }\n  }\n  avancar(t)\n\n  // O que ainda vem pela frente, a partir de agora.\n  const futuros = []\n  if (st.fase === 'turno' || st.fase === 'conversa') {\n    let x = Number.isFinite(st.fim) ? st.fim : Math.max(t, st.desde)\n    if (st.fase === 'conversa' && st.retomar && st.retomar.saiu > x) {\n      const r = st.retomar\n      const d = sobra.has(r.squad) ? sobra.get(r.squad) : dur(r)\n      futuros.push({ fase: 'turno', squad: r.squad, cor: r.cor, n: r.n, desde: x, fim: x + d, retoma: true })\n      x += d\n    }\n    for (const r of lista) {\n      if (falou.has(r.squad) || r.saiu <= t) continue\n      futuros.push({ fase: 'turno', squad: r.squad, cor: r.cor, n: r.n, desde: x, fim: x + dur(r) })\n      x += dur(r)\n    }\n  }\n  const ultimo = futuros.length ? futuros[futuros.length - 1] : st.fase === 'turno' ? st : null\n  return {\n    ini,\n    comeco,\n    inicio,\n    t,\n    fase: t < inicio ? 'antes' : st.fase,\n    atual: st,\n    feitos: feitos.filter((b) => b.fim - b.desde >= 1000),\n    futuros,\n    ajustado: evs.length > 0,\n    pausa: st.fase === 'conversa' && (!!st.retomar || feitos.some((b) => b.fase === 'turno')),\n    chamavel: st.fase === 'esgotou' || st.fase === 'encerrada' ? disponivel(t) : null,\n    fimPrevisto: ultimo ? ultimo.fim : null,\n  }\n}\n\n// A fila e as ações que valem agora (as reais, ou as da simulação) e o cronograma resultante.\nfunction estadoRetro(d2, v) {\n  const s = d2.state\n  const simEx = s.sim ? filaDaSimulacao(v) : { fila: v.fila, exemplo: false }\n  const vt = simEx.exemplo ? { ...v, fila: simEx.fila, totalSlots: simEx.fila.reduce((a, r) => a + r.n, 0) } : v\n  if (simEx.exemplo) vt.totalMin = vt.totalSlots * MIN_POR_SLOT\n  const eventos = s.sim ? s.simEventos || [] : s.dados.controle || []\n  const c = cronograma(participantes(vt, s.sim ? [] : s.dados.movimentos), s.dados.ciclo, eventos, agora(d2))\n  return { vt, c, exemplo: simEx.exemplo, eventos }\n}\n\n// ---------------------------------------------------------------- controle (só o Lucas)\n\nasync function salvarChave(d2, valor) {\n  const chave = String(valor || '').trim()\n  const api = d2.param('controle')\n  if (!chave || !api || d2.state.enviando) return\n  d2.state.enviando = 'chave'\n  try {\n    const r = await d2.rush(api).invoke({ acao: 'ping', chave, por: quemSou(d2) })\n    const corpo = r && r.feito ? r : r && r.data && r.data.feito ? r.data : null\n    if (corpo && corpo.ok) {\n      gravarChave(chave)\n      d2.state.chaveTique = Date.now()\n      avisar(d2, 'ok', 'Chave confirmada. O controle está liberado.')\n    } else avisar(d2, 'erro', (corpo && corpo.erro) || 'Chave recusada.')\n  } catch (e) {\n    avisar(d2, 'erro', mensagemDeErro(e))\n  } finally {\n    d2.state.enviando = null\n  }\n}\n\nasync function controlar(d2, acao) {\n  if (!ACOES.includes(acao) || d2.state.enviando) return\n  if (d2.state.sim) {\n    d2.setState({ simEventos: [...(d2.state.simEventos || []), { acao, em: new Date(agora(d2)).toISOString(), por: quemSou(d2) }] })\n    return\n  }\n  const api = d2.param('controle')\n  const chave = lerChave()\n  if (!api || !chave) return\n  d2.state.enviando = acao\n  try {\n    const r = await d2.rush(api).invoke({ acao, chave, por: quemSou(d2) })\n    const corpo = r && Array.isArray(r.eventos) ? r : r && r.data && Array.isArray(r.data.eventos) ? r.data : null\n    if (!corpo) throw new Error('resposta fora do formato esperado')\n    if (corpo.ok === false) {\n      if (/chave/i.test(corpo.erro || '')) { apagarChave(); d2.state.chaveTique = Date.now() }\n      avisar(d2, 'erro', corpo.erro || 'O servidor recusou a ação.')\n      return\n    }\n    d2.setState({ dados: { ...d2.state.dados, controle: corpo.eventos } })\n    avisar(d2, 'ok', `${ROTULO_ACAO[acao]} · atualizado para todos.`)\n  } catch (e) {\n    avisar(d2, 'erro', mensagemDeErro(e))\n  } finally {\n    d2.state.enviando = null\n  }\n}\n\n// `fundo`: atualização automática, sem indicador e sem aviso de erro (a próxima tentativa resolve).\nasync function chamar(d2, corpo, { squad, fundo } = {}) {\n  const api = d2.param('api')\n  if (!api) {\n    d2.state.erro = 'A tela está sem o listener configurado (param api).'\n    return\n  }\n  if (squad) d2.state.salvando = squad\n  else if (!fundo) d2.state.carregando = true\n  try {\n    const r = await d2.rush(api).invoke({ ...corpo, por: quemSou(d2) })\n    const dados = r && Array.isArray(r.reservas) ? r : r && r.data && Array.isArray(r.data.reservas) ? r.data : null\n    if (!dados) throw new Error('resposta fora do formato esperado')\n    const servidor = Date.parse(dados.agora || '')\n    const desvio = Number.isFinite(servidor) && Math.abs(servidor - Date.now()) < 10 * MIN_MS ? servidor - Date.now() : 0\n    d2.setState({ dados, erro: '', lidoEm: Date.now(), desvio })\n    if (dados.ok === false) avisar(d2, 'erro', dados.erro || 'O servidor recusou a ação.')\n    else if (corpo.op === 'declarar') {\n      avisar(d2, 'ok', corpo.slots > 0\n        ? `${curto(corpo.squad)}: ${plural(corpo.slots, 'slot', 'slots')} (${corpo.slots * MIN_POR_SLOT} min) registrados.`\n        : `${curto(corpo.squad)}: registrado sem apresentação.`)\n    } else if (corpo.op === 'retirar') avisar(d2, 'ok', `${curto(corpo.squad)} saiu da fila.`)\n  } catch (e) {\n    if (d2.state.dados && fundo) return\n    if (d2.state.dados) avisar(d2, 'erro', mensagemDeErro(e))\n    else d2.state.erro = mensagemDeErro(e)\n  } finally {\n    d2.setState({ salvando: null, otimista: null, carregando: false })\n  }\n}\n\n// ---------------------------------------------------------------- pedaços da tela\n\nfunction logo(v) {\n  const dots = LOGO_XY.map(([x, y], i) => {\n    const s = v.squads[i]\n    const on = s && v.porSquad.has(s)\n    return `<i class=\"${on ? 'on' : ''}\" style=\"left:${x}px;top:${y}px;background:${grad(i)}\" title=\"${esc(s ? `${s}: ${on ? 'declarou' : 'pendente'}` : '')}\"></i>`\n  }).join('')\n  return `<div class=\"logo\" aria-hidden=\"true\"><div class=\"logo-in\">${dots}</div></div>`\n}\n\nfunction linhaDoTempo(v, c) {\n  const sobra = META_SLOTS - v.totalSlots\n  let sit = ''\n  let classe = ''\n  if (v.totalSlots === 0) sit = 'Ninguém pegou slot ainda.'\n  else if (sobra > 0) sit = `Sobram ${plural(sobra, 'slot', 'slots')} (${sobra * MIN_POR_SLOT} min) até 1h.`\n  else if (sobra === 0) { sit = 'Fechou 1h certinho.'; classe = 'ok' }\n  else { sit = `Passou ${-sobra * MIN_POR_SLOT} min de 1h. Tudo bem, a fila segue.`; classe = 'warn' }\n  if (c.fimPrevisto) sit += ` Termina às ${relogio(c.fimPrevisto)}.`\n\n  // Blocos na ordem do tempo: o que já aconteceu, o que está acontecendo e o que está previsto.\n  const meta = c.comeco + META_SLOTS * MIN_POR_SLOT * MIN_MS\n  const st = c.atual\n  const blocos = c.feitos.map((b) => ({ ...b, estado: 'feito' }))\n  if (st.fase === 'conversa' || st.fase === 'turno') {\n    const fimAgora = Number.isFinite(st.fim) ? st.fim : Math.max(c.t, st.desde)\n    blocos.push({ ...st, fim: fimAgora, estado: c.fase === 'antes' ? 'previsto' : 'agora' })\n  }\n  for (const b of c.futuros) blocos.push({ ...b, estado: 'previsto' })\n  const visiveis = blocos.filter((b) => b.fim - b.desde >= 1000)\n  const turnos = visiveis.filter((b) => b.fase === 'turno')\n  const ultimoTurno = turnos.length ? Math.max(...turnos.map((b) => b.fim)) : c.comeco\n  const fimBlocos = visiveis.length ? Math.max(...visiveis.map((b) => b.fim)) : c.comeco\n  const tVisivel = c.fase !== 'antes' && c.t <= c.ini + JANELA_DEPOIS_MS ? c.t : c.ini\n  const passo = MIN_POR_SLOT * MIN_MS\n  const fimEixo = c.ini + Math.ceil((Math.max(meta, fimBlocos, tVisivel) - c.ini) / passo) * passo\n  const pos = (x) => ((x - c.ini) / (fimEixo - c.ini)) * 100\n  const caixa = (l, w) => `left:calc(${l}% + 2px);width:max(2px,calc(${w}% - 4px))`\n  const numero = new Map()\n  for (const b of turnos) if (!numero.has(b.squad)) numero.set(b.squad, numero.size + 1)\n\n  const segs = visiveis.map((b) => {\n    const l = pos(b.desde)\n    const w = pos(b.fim) - l\n    if (b.fase === 'conversa') {\n      return `<div class=\"bloco tol\" style=\"${caixa(l, w)}\" title=\"Conversa aberta: ${relogio(b.desde)}–${relogio(b.fim)}\">${w > 9 ? 'conversa aberta' : ''}</div>`\n    }\n    const mins = Math.round((b.fim - b.desde) / MIN_MS)\n    return `<div class=\"bloco seg\" style=\"${caixa(l, w)};background:${grad(b.cor)}\" title=\"${esc(`${numero.get(b.squad)}. ${b.squad}: ${relogio(b.desde)}–${relogio(b.fim)}`)}\">\n      <span>${w > 3.5 ? `${numero.get(b.squad)}. ${esc(curto(b.squad))}` : ''}</span>${w > 7 && mins > 0 ? `<em>${mins}′</em>` : ''}\n    </div>`\n  }).join('')\n  const vazios = []\n  for (let x = Math.max(ultimoTurno, c.comeco); x < meta - 1000; x += passo) {\n    const l = pos(x)\n    vazios.push(`<div class=\"bloco vazio\" style=\"${caixa(l, pos(Math.min(meta, x + passo)) - l)}\"></div>`)\n  }\n  const pct = pos(meta)\n  const excesso = fimBlocos > meta + 1000 ? `<div class=\"excesso\" style=\"left:${pct}%;width:${pos(fimBlocos) - pct}%\"></div>` : ''\n  const marcas = [[c.ini, ''], [c.comeco, 'sec'], [c.comeco + 15 * MIN_MS, 'sec'], [c.comeco + 30 * MIN_MS, ''], [c.comeco + 45 * MIN_MS, 'sec']]\n    .map(([x, cls]) => `<span class=\"${cls}\" style=\"left:${pos(x)}%\">${relogio(x)}</span>`).join('')\n    + `<span class=\"h1 ${pct >= 99.5 ? 'fim' : ''}\" style=\"left:${pct}%\">${relogio(meta)} · 1h</span>`\n    + (fimEixo > meta + 4 * MIN_MS ? `<span class=\"fim\" style=\"left:100%\">${relogio(fimEixo)}</span>` : '')\n  const correndo = c.fase !== 'antes' && c.t >= c.ini && c.t <= fimEixo\n  const andou = correndo ? pos(c.t) : 0\n  const tempoPassando = correndo\n    ? `<div class=\"passado\" style=\"width:${andou}%\"></div><div class=\"agora\" style=\"left:${andou}%\"><b>${relogio(c.t)}</b></div>`\n    : ''\n\n  return `\n  <section class=\"card tempo\">\n    <div class=\"tempo-topo\">\n      <div class=\"grande\"><b>${v.totalMin}</b><span>de 60 min</span></div>\n      <p class=\"sit ${classe}\">${sit}</p>\n    </div>\n    <div class=\"trilho-wrap ${correndo ? 'com-agora' : ''}\">\n      <div class=\"trilho\">${segs}${vazios.join('')}</div>\n      ${excesso}\n      <div class=\"linha1h\" style=\"left:${pct}%\"></div>\n      ${tempoPassando}\n    </div>\n    <div class=\"regua\">${marcas}</div>\n  </section>`\n}\n\nfunction ordem(v, c) {\n  // Horário de cada squad pelo cronograma: o que aconteceu ou o previsto (uma vez retomada junta início e fim).\n  const horario = new Map()\n  const numero = new Map()\n  for (const b of [...c.feitos, c.atual, ...c.futuros]) {\n    if (b.fase !== 'turno') continue\n    if (!numero.has(b.squad)) numero.set(b.squad, numero.size + 1)\n    const h = horario.get(b.squad)\n    horario.set(b.squad, h ? { ...h, fim: b.fim } : { desde: b.desde, fim: b.fim })\n  }\n  const falando = c.fase === 'turno' ? c.atual.squad : null\n  const vem = new Set(c.futuros.map((b) => b.squad))\n  const itens = v.fila.map((r) => {\n    const fala = falando === r.squad\n    const h = horario.get(r.squad)\n    const passou = !fala && !vem.has(r.squad) && !!h && h.fim <= c.t\n    return `\n    <li data-key=\"${esc(r.squad)}\" class=\"${fala ? 'atual' : passou ? 'passou' : ''}\">\n      <span class=\"num\" style=\"background:${grad(r.cor)}\">${numero.get(r.squad) || r.pos}</span>\n      <div class=\"info\">\n        <b>${esc(r.squad)}</b>\n        <span class=\"meta\">${plural(r.n, 'slot', 'slots')} · declarou ${esc(quando(r.chegada))}${r.por ? ` · ${esc(r.por)}` : ''}</span>\n      </div>\n      <span class=\"min\">${fala ? '<em class=\"tag-agora\">agora</em>' : ''}${h ? `${relogio(h.desde)}–${relogio(h.fim)}` : '—'}</span>\n    </li>`\n  }).join('')\n  const sem = v.semApresentacao.length\n    ? `<p class=\"sem-lista\">Sem apresentação: ${v.semApresentacao.map((r) => `<span class=\"chip\">${esc(curto(r.squad))}</span>`).join('')}</p>`\n    : ''\n  return `\n  <section class=\"card\">\n    <h2>Ordem de apresentação <small>por ordem de chegada</small></h2>\n    ${itens ? `<ol class=\"ordem\">${itens}</ol>` : '<p class=\"vazio-txt\">A fila está vazia. Quem declarar primeiro abre a retro.</p>'}\n    ${sem}\n  </section>`\n}\n\n// O \"telão\" da retro: quem fala agora e quanto falta. Segue o relógio e os ajustes do Lucas; ninguém mais comanda.\nfunction painelAoVivo(d2, v, c, exemplo, eventos) {\n  const sim = d2.state.sim\n  const ultimo = c.ajustado ? eventos.filter((e) => ACOES.includes(e.acao) && e.acao !== 'auto').slice(-1)[0] : null\n  const tags = (sim ? `<span class=\"selo sim\">Simulação · 1 min = 1 s${exemplo ? ' · fila de exemplo' : ''}</span>` : '')\n    + (c.ajustado ? `<span class=\"selo ajuste\">Ajustado ao vivo${ultimo && ultimo.por ? ` por ${esc(String(ultimo.por).split(' ')[0])}` : ''}</span>` : '')\n  const rolando = c.fase === 'conversa' || c.fase === 'turno'\n  const botao = sim ? '<button class=\"btn js-parar\">■ Parar simulação</button>' : rolando ? '' : '<button class=\"btn js-simular\">▶ Simular retro</button>'\n  const topo = (selo, cls) => `\n    <div class=\"av-topo\">\n      <span class=\"selo ${cls}\">${selo}</span>\n      <span class=\"av-hora\">${relogio(c.t)}</span>\n      ${tags}\n      <span class=\"av-acoes\">${botao}</span>\n    </div>`\n  const dia = rotuloCiclo(d2.state.dados.ciclo)\n  const st = c.atual\n  const prox = c.futuros[0] || null\n\n  if (c.fase === 'antes') {\n    return `\n    <section class=\"ao-vivo\">\n      ${topo('Próxima retro', '')}\n      <div class=\"av-corpo\">\n        <div><p class=\"av-rot\">${esc(dia)} às ${relogio(c.ini)}</p><p class=\"av-quem\">Começa em ${faltaTexto(c.ini - c.t)}</p></div>\n        <p class=\"av-info\">Conversa aberta até ${relogio(c.comeco)}, esperando todos.<br>${c.futuros.length\n          ? `${plural(c.futuros.length, 'squad', 'squads')} na fila · apresentações das ${relogio(c.futuros[0].desde)} às ${relogio(c.fimPrevisto)}.`\n          : 'Ninguém na fila ainda.'}</p>\n      </div>\n    </section>`\n  }\n  if (c.fase === 'conversa') {\n    const aberta = !Number.isFinite(st.fim)\n    const titulo = c.pausa ? 'Pausa na fila' : aberta ? 'Conversa estendida' : `A retro começa às ${relogio(st.fim)}`\n    const quem = prox ? `${c.pausa ? 'Na sequência' : 'Primeiro'}: ${esc(curto(prox.squad))}` : 'Ninguém na fila'\n    const feito = aberta ? 100 : ((c.t - st.desde) / (st.fim - st.desde)) * 100\n    const rodape = c.pausa\n      ? prox && prox.retoma ? `${esc(prox.squad)} volta com ${mmss(prox.fim - prox.desde)} na vez.` : 'A fila continua quando a conversa terminar.'\n      : aberta ? 'As apresentações começam em instantes.' : 'Esperando todos chegarem.'\n    return `\n    <section class=\"ao-vivo vivo\">\n      ${topo('Conversa aberta', 'vivo')}\n      <div class=\"av-corpo\">\n        <div><p class=\"av-rot\">${titulo}</p><p class=\"av-quem\">${quem}</p></div>\n        <div><p class=\"av-rot av-dir\">${aberta ? 'Há' : 'Faltam'}</p><p class=\"av-relogio\">${mmss(aberta ? c.t - st.desde : st.fim - c.t)}</p></div>\n      </div>\n      <div class=\"av-barra ${aberta ? 'indef' : ''}\"><i style=\"width:${feito}%;background:var(--ink3)\"></i></div>\n      <div class=\"av-rodape\"><span>${rodape}</span>${prox ? `<span>${esc(prox.squad)} · ${Math.round((prox.fim - prox.desde) / MIN_MS)} min</span>` : ''}</div>\n    </section>`\n  }\n  if (c.fase === 'turno') {\n    const resta = st.fim - c.t\n    const feito = ((c.t - st.desde) / (st.fim - st.desde)) * 100\n    const pouco = resta <= MIN_MS\n    const vez = [...new Set([...c.feitos, st, ...c.futuros].filter((b) => b.fase === 'turno').map((b) => b.squad))]\n    return `\n    <section class=\"ao-vivo vivo\" style=\"--cor:${grad(st.cor)}\">\n      ${topo('Ao vivo', 'vivo')}\n      <div class=\"av-corpo\">\n        <div><p class=\"av-rot\">Agora fala · ${vez.indexOf(st.squad) + 1}º de ${vez.length}</p>\n          <p class=\"av-quem\"><i class=\"dot\" style=\"background:${grad(st.cor)}\"></i>${esc(st.squad)}</p></div>\n        <div><p class=\"av-rot av-dir\">${pouco ? 'Último minuto' : 'Faltam'}</p><p class=\"av-relogio ${pouco ? 'pouco' : ''}\">${mmss(resta)}</p></div>\n      </div>\n      <div class=\"av-barra\"><i style=\"width:${feito}%;background:${grad(st.cor)}\"></i></div>\n      <div class=\"av-rodape\">\n        <span>${prox ? `Depois: <b>${esc(prox.squad)}</b> às ${relogio(prox.desde)}` : 'É o último da fila.'}</span>\n        <span>Fim previsto: <b>${relogio(c.fimPrevisto)}</b></span>\n      </div>\n    </section>`\n  }\n  const ultimaVez = c.feitos.filter((b) => b.fase === 'turno').slice(-1)[0]\n  if (c.fase === 'esgotou') {\n    return `\n    <section class=\"ao-vivo\">\n      ${topo('Fila concluída', '')}\n      <div class=\"av-corpo\">\n        <div><p class=\"av-rot\">${esc(dia)}</p><p class=\"av-quem\">${ultimaVez ? `Apresentações até ${relogio(ultimaVez.fim)}` : 'Nenhuma apresentação'}</p></div>\n        <p class=\"av-info\">Se alguém entrar na fila agora, a vez começa na hora.</p>\n      </div>\n    </section>`\n  }\n  return `\n  <section class=\"ao-vivo\">\n    ${topo('Retro encerrada', '')}\n    <div class=\"av-corpo\">\n      <div><p class=\"av-rot\">${esc(dia)}</p><p class=\"av-quem\">${ultimaVez ? `Apresentações até ${relogio(ultimaVez.fim)}` : 'Nenhuma apresentação'}</p></div>\n      <p class=\"av-info\">A fila zera no sábado e abre para a próxima sexta.</p>\n    </div>\n  </section>`\n}\n\n// Os botões do Lucas. Aparecem só para ele, na janela da retro ou na simulação; o servidor confere a chave em toda ação.\nfunction painelControle(d2, c) {\n  const sim = d2.state.sim\n  const enviando = !!d2.state.enviando\n  const cabeca = `\n    <div class=\"ct-topo\">\n      <b>Controle da retro</b>\n      <span>só você vê este painel · todos veem o resultado${sim ? ' · na simulação nada é salvo' : ''}</span>\n      <em class=\"ct-modo ${c.ajustado ? '' : 'auto'}\">${c.ajustado ? 'ajustado ao vivo' : 'automático'}</em>\n    </div>`\n  if (!sim && !lerChave()) {\n    return `\n    <section class=\"controle\">\n      ${cabeca}\n      <div class=\"ct-chave\">\n        <label>Chave de controle <input type=\"password\" class=\"js-chave\" autocomplete=\"off\" spellcheck=\"false\" placeholder=\"cole aqui a chave\"></label>\n        <button class=\"ct-btn forte js-chave-salvar\" ${enviando ? 'disabled' : ''}>Salvar</button>\n      </div>\n      <p class=\"ct-nota\">A chave fica só neste navegador. O servidor confere em toda ação.</p>\n    </section>`\n  }\n  const b = (acao, rotulo, cls = '') => `<button class=\"ct-btn ${cls} js-acao\" data-acao=\"${acao}\" ${enviando ? 'disabled' : ''}>${rotulo}</button>`\n  const st = c.atual\n  const prox = c.futuros[0] || null\n  let principais = ''\n  if (c.fase === 'antes') {\n    principais = b('comecar', `▶ Começar apresentações agora${prox ? ` (${esc(curto(prox.squad))})` : ''}`, 'forte')\n  } else if (c.fase === 'conversa' && !c.pausa) {\n    principais = b('comecar', `▶ Começar apresentações${prox ? ` (${esc(curto(prox.squad))})` : ''}`, 'forte')\n      + (Number.isFinite(st.fim) ? b('segurar', '⏸ Segurar a conversa') + b('mais', '+1 min de conversa') : '')\n  } else if (c.fase === 'conversa') {\n    principais = b('comecar', prox ? `▶ ${prox.retoma ? 'Retomar com' : 'Seguir com'} ${esc(curto(prox.squad))}` : '▶ Seguir', 'forte')\n  } else if (c.fase === 'turno') {\n    principais = b('proximo', prox ? `⏭ Próximo: ${esc(curto(prox.squad))}` : '⏭ Encerrar esta vez', 'forte')\n      + b('mais', '+1 min') + b('menos', '−1 min') + b('conversa', '💬 Abrir conversa')\n  } else {\n    principais = (c.chamavel ? b('proximo', `▶ Chamar ${esc(curto(c.chamavel.squad))}`, 'forte') : '') + b('conversa', '💬 Abrir conversa')\n  }\n  const secundarios = (c.fase !== 'encerrada' && c.fase !== 'antes' ? b('encerrar', '■ Encerrar retro', 'perigo') : '')\n    + (c.ajustado ? b('auto', '↺ Voltar ao automático', 'leve') : '')\n    + (sim ? '' : '<button class=\"ct-link js-chave-trocar\">trocar chave</button>')\n  return `\n  <section class=\"controle\">\n    ${cabeca}\n    <div class=\"ct-botoes\">${principais}</div>\n    <div class=\"ct-botoes ct-sec\">${secundarios}</div>\n  </section>`\n}\n\nfunction faltam(v) {\n  const chips = v.pendentes.map((s) => {\n    const i = v.squads.indexOf(s)\n    return `<span class=\"chip pend\" data-key=\"${esc(s)}\"><i style=\"background:${grad(i)}\"></i>${esc(curto(s))}</span>`\n  }).join('')\n  return `\n  <section class=\"card\">\n    <h2>Faltam declarar <span class=\"cont ${v.pendentes.length ? '' : 'zero'}\">${v.pendentes.length}</span></h2>\n    ${chips ? `<div class=\"chips\">${chips}</div>` : '<p class=\"tudo\">Todos os squads já declararam.</p>'}\n  </section>`\n}\n\nfunction cartao(d2, v, s, i) {\n  const r = v.porSquad.get(s)\n  const salvando = d2.state.salvando === s\n  const otimista = d2.state.otimista && d2.state.otimista.squad === s ? d2.state.otimista.n : null\n  const atual = otimista != null ? otimista : r ? Math.round(Number(r.slots) || 0) : null\n  const pos = v.posicao.get(s)\n  let tag = 'Pendente'\n  let estado = 'pendente'\n  if (salvando) { tag = 'Salvando…'; estado = 'salvando' }\n  else if (r && pos) { tag = `${pos}º na fila`; estado = 'na-fila' }\n  else if (r) { tag = 'Sem apresentação'; estado = 'fora' }\n\n  const botoes = Array.from({ length: MAX_SLOTS + 1 }, (_, n) => {\n    const sel = atual === n\n    const dica = n === 0 ? 'Não vai apresentar' : `${n * MIN_POR_SLOT} min`\n    return `<button class=\"js-slot ${sel ? 'sel' : ''}\" data-squad=\"${esc(s)}\" data-n=\"${n}\" aria-pressed=\"${sel}\"\n      title=\"${dica}\" ${d2.state.salvando ? 'disabled' : ''} ${sel ? `style=\"background:${grad(i)}\"` : ''}>${n}</button>`\n  }).join('')\n\n  const rodape = atual == null\n    ? 'Escolha quantos slots o squad vai usar.'\n    : atual === 0 ? 'Não vai apresentar nesta retro.' : `${plural(atual, 'slot', 'slots')} = ${atual * MIN_POR_SLOT} min`\n\n  return `\n  <article class=\"sq ${estado}\" data-key=\"${esc(s)}\">\n    <div class=\"faixa\" style=\"background:${grad(i)}\"></div>\n    <div class=\"sq-topo\"><b>${esc(s)}</b><span class=\"tag\">${tag}</span></div>\n    <div class=\"slots\" role=\"group\" aria-label=\"Slots do ${esc(s)}\">${botoes}</div>\n    <div class=\"sq-rodape\">\n      <span>${rodape}</span>\n      ${r && !salvando ? `<button class=\"link js-retirar\" data-squad=\"${esc(s)}\">tirar da fila</button>` : ''}\n    </div>\n  </article>`\n}\n\n// ---------------------------------------------------------------- módulo\n\nexport default {\n  css: `\n:host{display:block;height:100%;\n  --bg:#161b26;--bg2:#1c2230;--card:#1f2533;--card2:#262d3d;--hair:#343d50;--hair2:#2b3345;\n  --ink:#e8ecf3;--ink2:#b4bdcb;--ink3:#8b96a8;--ok:#5ee6a8;--warn:#f7a048;--bad:#ff7a88}\n*{box-sizing:border-box}\n.pagina{height:100%;overflow:auto;background:radial-gradient(1200px 500px at 15% -10%,rgba(72,154,250,.10),transparent 60%),\n  radial-gradient(900px 400px at 95% 0%,rgba(140,72,250,.10),transparent 60%),var(--bg);\n  color:var(--ink);font-family:Inter,\"Segoe UI\",system-ui,-apple-system,Roboto,sans-serif;font-size:14px;line-height:1.45;\n  -webkit-font-smoothing:antialiased}\n.app{max-width:1240px;margin:0 auto;padding:28px 24px 40px}\nbutton{font:inherit;color:inherit}\n\n.top{display:flex;align-items:center;gap:18px;margin-bottom:22px;flex-wrap:wrap}\n.logo{width:62px;height:61px;flex:none}\n.logo-in{position:relative;width:54px;height:53px;transform:scale(1.15);transform-origin:0 0}\n.logo i{position:absolute;width:12px;height:12px;border-radius:50%;opacity:.22;filter:saturate(.35);transition:opacity .3s,filter .3s,box-shadow .3s}\n.logo i.on{opacity:1;filter:none;box-shadow:0 0 10px rgba(255,255,255,.18)}\n.tit{flex:1;min-width:220px}\nh1{margin:0;font-size:26px;font-weight:750;letter-spacing:-.02em}\n.sub{margin:2px 0 0;color:var(--ink2)}\n.acoes{display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n.pill{padding:6px 12px;border-radius:999px;background:var(--card2);border:1px solid var(--hair);color:var(--ink2);font-weight:600;font-size:13px}\n.pill b{color:var(--ink)}\n.pill.ok{border-color:rgba(94,230,168,.45);color:var(--ok)}\n.btn{padding:7px 12px;border-radius:10px;border:1px solid var(--hair);background:transparent;color:var(--ink2);cursor:pointer;font-weight:600;font-size:13px}\n.btn:hover{background:var(--card2);color:var(--ink)}\n.btn:disabled{opacity:.5;cursor:default}\na.btn{text-decoration:none}\n.eu{font-size:12px;color:var(--ink3);width:100%;text-align:right}\n\n.card{background:linear-gradient(180deg,rgba(255,255,255,.025),transparent 40%),var(--card);border:1px solid var(--hair2);\n  border-radius:16px;padding:18px 20px}\nh2{margin:0 0 14px;font-size:15px;font-weight:700;display:flex;align-items:center;gap:8px}\nh2 small{font-weight:500;color:var(--ink3);font-size:12.5px}\n\n.tempo{margin-bottom:18px}\n.tempo-topo{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:14px;flex-wrap:wrap}\n.grande{display:flex;align-items:baseline;gap:8px}\n.grande b{font-size:40px;line-height:1;font-weight:800;letter-spacing:-.03em;font-variant-numeric:tabular-nums}\n.grande span{color:var(--ink2);font-size:15px;font-weight:600}\n.sit{margin:0;color:var(--ink2);font-weight:600}\n.sit.ok{color:var(--ok)}\n.sit.warn{color:var(--warn)}\n.trilho-wrap{position:relative}\n.trilho{position:relative;height:46px}\n.bloco{position:absolute;top:0;bottom:0}\n.seg{border-radius:9px;display:flex;align-items:center;justify-content:space-between;gap:6px;padding:0 10px;min-width:0;\n  color:#0b0f1a;font-weight:750;font-size:12.5px;overflow:hidden;white-space:nowrap}\n.seg span{overflow:hidden;text-overflow:ellipsis}\n.seg em{font-style:normal;font-weight:700;opacity:.75;flex:none}\n.vazio{border-radius:9px;border:1.5px dashed var(--hair);background:rgba(255,255,255,.015)}\n.excesso{position:absolute;top:0;bottom:0;border-radius:0 9px 9px 0;pointer-events:none;\n  background:repeating-linear-gradient(135deg,rgba(11,15,26,.28) 0 6px,transparent 6px 12px)}\n.linha1h{position:absolute;top:-6px;bottom:-6px;width:2px;margin-left:-1px;background:var(--ink);border-radius:2px;box-shadow:0 0 0 2px var(--card)}\n.regua{position:relative;height:22px;margin-top:8px;color:var(--ink3);font-size:12px;font-variant-numeric:tabular-nums}\n.regua span{position:absolute;transform:translateX(-50%);white-space:nowrap}\n.regua span:first-child{transform:none}\n.regua .h1{color:var(--ink);font-weight:700}\n.regua .fim{transform:translateX(-100%)}\n\n.tol{border-radius:9px;border:1.5px solid var(--hair);background:repeating-linear-gradient(135deg,rgba(255,255,255,.06) 0 5px,transparent 5px 10px);\n  display:grid;place-items:center;color:var(--ink3);font-size:11px;font-weight:700;min-width:0;overflow:hidden}\n.trilho-wrap.com-agora{margin-top:26px}\n.passado{position:absolute;left:0;top:0;bottom:0;background:rgba(11,15,26,.5);border-radius:9px 0 0 9px;pointer-events:none}\n.agora{position:absolute;top:-8px;bottom:-8px;width:2px;margin-left:-1px;background:#ff7a88;border-radius:2px;box-shadow:0 0 0 2px var(--card);pointer-events:none}\n.agora b{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);margin-bottom:3px;font-size:11.5px;color:#ffb0b8;white-space:nowrap;font-variant-numeric:tabular-nums}\n\n.ao-vivo{position:relative;overflow:hidden;border-radius:18px;padding:16px 22px 18px;margin-bottom:18px;border:1px solid var(--hair2);\n  background:linear-gradient(180deg,rgba(255,255,255,.03),transparent 60%),var(--card)}\n.ao-vivo.vivo::before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--cor,#ff7a88)}\n.av-topo{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px;min-height:32px}\n.selo{padding:3px 10px;border-radius:999px;font-weight:800;font-size:11.5px;letter-spacing:.05em;text-transform:uppercase;background:var(--card2);color:var(--ink2)}\n.selo.vivo{background:rgba(255,122,136,.15);color:#ffb0b8}\n.selo.vivo::before{content:\"\";display:inline-block;width:7px;height:7px;border-radius:50%;background:#ff7a88;margin-right:6px;vertical-align:1px;animation:pulsa 1.4s ease-in-out infinite}\n.selo.sim{background:rgba(72,154,250,.16);color:#a9cdff;text-transform:none;letter-spacing:0}\n@keyframes pulsa{50%{opacity:.25}}\n@media (prefers-reduced-motion:reduce){.selo.vivo::before{animation:none}}\n.av-hora{color:var(--ink2);font-weight:700;font-variant-numeric:tabular-nums}\n.av-acoes{margin-left:auto;display:flex;gap:8px}\n.av-corpo{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;flex-wrap:wrap}\n.av-rot{margin:0;color:var(--ink3);font-weight:600;font-size:13px}\n.av-dir{text-align:right}\n.av-quem{margin:2px 0 0;font-size:clamp(24px,3.6vw,40px);font-weight:850;letter-spacing:-.03em;line-height:1.1;display:flex;align-items:center}\n.av-relogio{margin:2px 0 0;font-size:clamp(34px,5vw,54px);font-weight:850;letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums;text-align:right}\n.av-relogio.pouco{color:var(--warn)}\n.av-info{margin:0;color:var(--ink2);text-align:right}\n.av-barra{height:8px;border-radius:999px;background:var(--bg2);margin:14px 0 10px;overflow:hidden}\n.av-barra i{display:block;height:100%;border-radius:999px;transition:width 1s linear}\n.av-rodape{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;color:var(--ink2);font-size:13px}\n.av-rodape b{color:var(--ink)}\n.dot{display:inline-block;width:.42em;height:.42em;border-radius:50%;margin-right:.3em;flex:none}\n.ordem li.atual{border-color:rgba(255,122,136,.6);box-shadow:0 0 0 1px rgba(255,122,136,.25)}\n.ordem li.passou{opacity:.5}\n.selo.ajuste{background:rgba(247,160,72,.16);color:#ffc890;text-transform:none;letter-spacing:0}\n.av-barra.indef i{width:100% !important;background:repeating-linear-gradient(135deg,rgba(255,255,255,.16) 0 6px,transparent 6px 12px) !important}\n.controle{border-radius:16px;padding:14px 18px;margin:-4px 0 18px;border:1.5px dashed rgba(247,160,72,.45);background:rgba(247,160,72,.05)}\n.ct-topo{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:10px}\n.ct-topo b{font-size:14px}\n.ct-topo span{color:var(--ink3);font-size:12.5px}\n.ct-modo{margin-left:auto;font-style:normal;font-size:12px;font-weight:700;padding:2px 9px;border-radius:999px;background:rgba(247,160,72,.16);color:#ffc890}\n.ct-modo.auto{background:var(--card2);color:var(--ink2)}\n.ct-botoes{display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n.ct-sec{margin-top:8px}\n.ct-btn{padding:9px 14px;border-radius:11px;border:1px solid var(--hair);background:var(--card);color:var(--ink);font-weight:700;font-size:13.5px;cursor:pointer}\n.ct-btn:hover:not(:disabled){border-color:var(--ink3)}\n.ct-btn.forte{background:#489afa;border-color:#489afa;color:#0b0f1a}\n.ct-btn.perigo{background:transparent;color:#ffb0b8;border-color:rgba(255,122,136,.45)}\n.ct-btn.leve{background:transparent;color:var(--ink2)}\n.ct-btn:disabled{opacity:.5;cursor:default}\n.ct-btn:focus-visible,.ct-link:focus-visible,.ct-chave input:focus-visible{outline:2px solid #489afa;outline-offset:2px}\n.ct-link{margin-left:auto;border:0;background:none;padding:0;color:var(--ink3);text-decoration:underline;text-underline-offset:2px;cursor:pointer;font-size:12px}\n.ct-chave{display:flex;gap:10px;align-items:center;flex-wrap:wrap}\n.ct-chave label{display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:var(--ink2);font-weight:600;font-size:13px}\n.ct-chave input{width:260px;max-width:70vw;padding:8px 10px;border-radius:9px;border:1px solid var(--hair);background:var(--bg2);color:var(--ink);font:inherit}\n.ct-nota{margin:8px 0 0;color:var(--ink3);font-size:12px}\n.tag-agora{font-style:normal;margin-right:8px;padding:2px 7px;border-radius:999px;background:rgba(255,122,136,.15);color:#ffb0b8;font-size:11px;font-weight:800;text-transform:uppercase}\n\n.cols{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);gap:18px;align-items:start}\n.col{display:grid;gap:18px}\n\n.ordem{list-style:none;margin:0;padding:0;display:grid;gap:8px}\n.ordem li{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:12px;background:var(--bg2);border:1px solid var(--hair2)}\n.num{width:30px;height:30px;border-radius:50%;flex:none;display:grid;place-items:center;color:#0b0f1a;font-weight:800;font-size:14px}\n.info{flex:1;min-width:0;display:grid}\n.info b{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.meta{color:var(--ink3);font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.min{flex:none;font-variant-numeric:tabular-nums;font-weight:700;color:var(--ink2);font-size:13px}\n.vazio-txt,.tudo{margin:0;color:var(--ink3)}\n.tudo{color:var(--ok);font-weight:600}\n.sem-lista{margin:12px 0 0;color:var(--ink3);font-size:12.5px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n\n.chips{display:flex;flex-wrap:wrap;gap:8px}\n.chip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:999px;background:var(--bg2);border:1px solid var(--hair);\n  font-weight:600;font-size:13px;color:var(--ink2)}\n.chip.pend{border-style:dashed}\n.chip i{width:9px;height:9px;border-radius:50%}\n.cont{min-width:22px;height:22px;padding:0 7px;border-radius:999px;display:inline-grid;place-items:center;font-size:12px;\n  background:rgba(247,160,72,.16);color:var(--warn)}\n.cont.zero{background:rgba(94,230,168,.14);color:var(--ok)}\n\n.grade{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}\n.sq{position:relative;border-radius:14px;background:var(--bg2);border:1px solid var(--hair2);padding:14px 14px 12px;overflow:hidden;\n  transition:border-color .2s}\n.sq.pendente{border-style:dashed;border-color:var(--hair)}\n.sq.salvando{opacity:.8}\n.faixa{position:absolute;left:0;top:0;right:0;height:3px}\n.sq.pendente .faixa{opacity:.35}\n.sq-topo{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}\n.sq-topo b{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.tag{flex:none;font-size:11.5px;font-weight:700;padding:3px 8px;border-radius:999px;background:var(--card2);color:var(--ink2)}\n.sq.pendente .tag{background:rgba(247,160,72,.14);color:var(--warn)}\n.sq.na-fila .tag{background:rgba(94,230,168,.14);color:var(--ok)}\n.slots{display:grid;grid-template-columns:repeat(${MAX_SLOTS + 1},minmax(0,1fr));gap:5px}\n.slots button{height:34px;border-radius:9px;border:1px solid var(--hair);background:var(--card);color:var(--ink2);\n  font-weight:700;font-size:14px;cursor:pointer;font-variant-numeric:tabular-nums;transition:background .15s,border-color .15s,transform .1s}\n.slots button:hover:not(:disabled){border-color:var(--ink3);color:var(--ink)}\n.slots button:active:not(:disabled){transform:scale(.96)}\n.slots button.sel{color:#0b0f1a;border-color:transparent}\n.slots button:disabled{cursor:default}\n.slots button:not(.sel):disabled{opacity:.55}\n.slots button:focus-visible,.btn:focus-visible,.link:focus-visible{outline:2px solid #489afa;outline-offset:2px}\n.sq-rodape{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:9px;color:var(--ink3);font-size:12.5px;min-height:20px}\n.link{border:0;background:none;padding:0;color:var(--ink3);text-decoration:underline;text-underline-offset:2px;cursor:pointer;font-size:12.5px}\n.link:hover{color:var(--bad)}\n.legenda{margin:12px 0 0;color:var(--ink3);font-size:12.5px}\n\n.aviso{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:5;max-width:min(560px,calc(100% - 32px));\n  padding:11px 16px;border-radius:12px;background:#121722;border:1px solid var(--hair);box-shadow:0 10px 30px rgba(0,0,0,.4);font-weight:600}\n.aviso.ok{border-color:rgba(94,230,168,.5);color:var(--ok)}\n.aviso.erro{border-color:rgba(255,122,136,.55);color:var(--bad)}\n.estado-tela{max-width:520px;margin:12vh auto;text-align:center;color:var(--ink2)}\n.estado-tela b{display:block;color:var(--ink);font-size:18px;margin-bottom:6px}\n.estado-tela.erro b{color:var(--bad)}\n\n@media (max-width:960px){.cols{grid-template-columns:1fr}}\n@media (max-width:560px){.app{padding:18px 16px 32px}h1{font-size:22px}.grande b{font-size:32px}.eu{text-align:left}\n  .seg em{display:none}.seg{padding:0 6px;justify-content:center}.regua .sec{display:none}.tol{font-size:0}\n  .av-info{text-align:left}.av-corpo>div:last-child{margin-left:auto}}\n`,\n\n  render(d2) {\n    const dados = d2.state.dados\n    if (!dados) {\n      const erro = d2.state.erro\n      return `<div class=\"pagina\"><div class=\"app\">\n        <div class=\"estado-tela ${erro ? 'erro' : ''}\">\n          <b>${erro ? 'Não deu para abrir a fila' : 'Carregando a fila…'}</b>\n          ${erro ? `<p>${esc(erro)}</p><button class=\"btn js-atualizar\">Tentar de novo</button>` : ''}\n        </div></div></div>`\n    }\n    void d2.state.tique // redesenha a cada tique do relógio\n    void d2.state.chaveTique // e quando a chave de controle muda\n    const v = derivar(dados)\n    const { vt, c, exemplo, eventos } = estadoRetro(d2, v)\n    const declararam = v.squads.length - v.pendentes.length\n    const eu = quemSou(d2)\n    const linkRecap = d2.param('linkRecap')\n    const aviso = d2.state.aviso\n    const controle = souAdmin(d2) && (d2.state.sim || naJanela(d2)) ? painelControle(d2, c) : ''\n    return `<div class=\"pagina\"><div class=\"app\">\n      <header class=\"top\">\n        ${logo(v)}\n        <div class=\"tit\">\n          <h1>Fila da retro</h1>\n          <p class=\"sub\">Retro de ${esc(rotuloCiclo(dados.ciclo))} às ${relogio(c.ini)} · conversa aberta até ${relogio(c.comeco)} · slots de ${MIN_POR_SLOT} min · meta de 1h</p>\n        </div>\n        <div class=\"acoes\">\n          <span class=\"pill ${v.pendentes.length ? '' : 'ok'}\"><b>${declararam}</b> de ${v.squads.length} squads declararam</span>\n          <button class=\"btn js-atualizar\" ${d2.state.carregando ? 'disabled' : ''}>${d2.state.carregando ? 'Atualizando…' : '↻ Atualizar'}</button>\n          ${linkRecap ? `<a class=\"btn\" href=\"${esc(linkRecap)}\" target=\"_top\">✨ Ver recap</a>` : ''}\n          ${eu ? `<span class=\"eu\">Você: ${esc(eu)}</span>` : ''}\n        </div>\n      </header>\n      ${painelAoVivo(d2, vt, c, exemplo, eventos)}\n      ${controle}\n      ${linhaDoTempo(vt, c)}\n      <div class=\"cols\">\n        <div class=\"col\">${ordem(v, c)}${faltam(v)}</div>\n        <section class=\"card\">\n          <h2>Declare o seu squad <small>clique no número de slots e pronto</small></h2>\n          <div class=\"grade\">${v.squads.map((s, i) => cartao(d2, v, s, i)).join('')}</div>\n          <p class=\"legenda\">1 slot = ${MIN_POR_SLOT} min · 0 = não vai apresentar · mudar o número mantém o lugar; tirar da fila e voltar manda para o fim.</p>\n        </section>\n      </div>\n      ${aviso ? `<div class=\"aviso ${aviso.tipo}\" role=\"status\">${esc(aviso.txt)}</div>` : ''}\n    </div></div>`\n  },\n\n  mounted(d2) {\n    d2.on('.js-slot', 'click', (e, el) => {\n      const squad = el.dataset.squad\n      const n = Number(el.dataset.n)\n      if (d2.state.salvando || !Number.isInteger(n)) return\n      const atual = d2.state.dados && derivar(d2.state.dados).porSquad.get(squad)\n      if (atual && Math.round(Number(atual.slots) || 0) === n) return\n      d2.state.otimista = { squad, n }\n      chamar(d2, { op: 'declarar', squad, slots: n }, { squad })\n    })\n    d2.on('.js-retirar', 'click', async (e, el) => {\n      const squad = el.dataset.squad\n      if (d2.state.salvando) return\n      const ok = await d2.dialog.confirm(`Tirar o ${squad} da fila? Se declarar de novo, ele volta para o fim.`)\n      if (ok) chamar(d2, { op: 'retirar', squad }, { squad })\n    })\n    d2.on('.js-atualizar', 'click', () => chamar(d2, { op: 'estado' }))\n    d2.on('.js-simular', 'click', () => {\n      if (d2.state.dados) d2.setState({ sim: { desde: Date.now(), ciclo: d2.state.dados.ciclo }, simEventos: [] })\n    })\n    d2.on('.js-parar', 'click', () => d2.setState({ sim: null, simEventos: [] }))\n    d2.on('.js-acao', 'click', async (e, el) => {\n      const acao = el.dataset.acao\n      if (acao === 'encerrar' && !(await d2.dialog.confirm('Encerrar a retro agora para todos?'))) return\n      if (acao === 'auto' && !(await d2.dialog.confirm('Voltar ao automático? Os ajustes deixam de valer e a fila volta ao horário original.'))) return\n      controlar(d2, acao)\n    })\n    d2.on('.js-chave-salvar', 'click', () => { const el = d2.$('.js-chave'); salvarChave(d2, el && el.value) })\n    d2.on('.js-chave', 'keydown', (e, el) => { if (e.key === 'Enter') salvarChave(d2, el.value) })\n    d2.on('.js-chave-trocar', 'click', () => { apagarChave(); d2.state.chaveTique = Date.now() })\n\n    // Tempo real só na janela da retro: o socket liga às 16:40 de sexta, desliga às 19:30 ou quando a aba some.\n    // Cada aviso do servidor (\"algo mudou\") vira uma releitura do estado; o estado em si nunca viaja pelo socket.\n    const rt = { h: null, ligado: false, ligando: false, ouvindo: false, espera: null, buscouEm: 0 }\n    const socket = () => { if (!rt.h) rt.h = d2.socket('socket'); return rt.h }\n    const buscar = () => { rt.buscouEm = Date.now(); chamar(d2, { op: 'estado' }, { fundo: true }) }\n    const aoMudar = () => {\n      clearTimeout(rt.espera)\n      rt.espera = setTimeout(() => { if (Date.now() - rt.buscouEm > 1500) buscar() }, 600)\n    }\n    const ligar = async () => {\n      if (rt.ligado || rt.ligando || !d2.param('socket')) return\n      rt.ligando = true\n      try {\n        const s = socket()\n        if (!rt.ouvindo) { s.onMessage(aoMudar); rt.ouvindo = true }\n        if (!s.connected) s.connect()\n        for (let i = 0; i < 40 && !s.connected; i++) await esperar(250)\n        if (s.connected) {\n          await s.subscribe(SALA)\n          rt.ligado = true\n          aoMudar()\n        }\n      } catch (e) { /* tenta de novo no próximo tique */ } finally {\n        rt.ligando = false\n      }\n    }\n    const desligar = () => {\n      if (!rt.ligado) return\n      rt.ligado = false\n      try { const s = socket(); s.unsubscribe(SALA); s.close() } catch (e) { /* já estava fechado */ }\n    }\n\n    const tique = setInterval(() => {\n      const s = d2.state\n      if (!s.dados) return\n      const janela = naJanela(d2)\n      if (janela && !document.hidden) {\n        if (rt.ligado && !socket().connected) rt.ligado = false // o PowerCordSocket não reconecta sozinho\n        ligar()\n      } else desligar()\n      const intervalo = !janela ? BUSCA_FORA_MS : rt.ligado ? BUSCA_JANELA_MS : BUSCA_SEM_SOCKET_MS\n      if (!document.hidden && !s.salvando && Date.now() - Math.max(rt.buscouEm, s.lidoEm || 0) >= intervalo) buscar()\n      if (s.sim) {\n        const { c } = estadoRetro(d2, derivar(s.dados))\n        const acabou = c.fase === 'esgotou' || c.fase === 'encerrada'\n        if (acabou && c.t > (c.fimPrevisto || c.atual.desde) + 5 * MIN_MS) {\n          d2.setState({ sim: null, simEventos: [], tique: Date.now() })\n          return\n        }\n      }\n      if (s.sim || janela || Date.now() - (s.tique || 0) >= 30000) d2.state.tique = Date.now()\n    }, 1000)\n\n    descobrirQuemSou(d2)\n    rt.buscouEm = Date.now()\n    chamar(d2, { op: 'estado' })\n    d2.on(document, 'visibilitychange', () => {\n      if (!document.hidden && !d2.state.salvando && Date.now() - rt.buscouEm > 5000) buscar()\n    })\n    d2.onUnmount(() => {\n      clearInterval(tique)\n      clearTimeout(rt.espera)\n      clearTimeout(timersAviso.get(d2))\n      desligar()\n    })\n  },\n}\n"}},"api":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"api"}}}},"linkRecap":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"linkRecap"}}}},"controle":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"controle"}}}},"socket":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"socket"}}}}}}}}}},{"_id":"6ae00504ec41aa8820619508","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"filaRecap","package":"lucas-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Retro Wrapped — recap das retros no estilo Spotify Wrapped / Steam Replay.\n// Lê o histórico pelo listener da fila (param `api`, op `historico`) e calcula tudo aqui.\n\n// Gradientes das 8 bolinhas do logo da Data2 (mesma ordem da tela da fila).\nconst CORES = [\n  ['#489afa', '#8448fa'],\n  ['#f74848', '#f7a048'],\n  ['#fadc48', '#bffa48'],\n  ['#48faa1', '#4bfafa'],\n  ['#4dfafa', '#4da4fa'],\n  ['#fa48dc', '#f74882'],\n  ['#8c48fa', '#dc48fa'],\n  ['#bffa48', '#66fa48'],\n]\nconst LOGO_XY = [[21, 0], [36, 6], [42, 21], [36, 36], [21, 41], [6, 36], [0, 21], [6, 6]]\n\nconst MIN_POR_SLOT = 5\nconst META_MIN = 60\nconst HORA = 3600000\nconst DIAS = ['domingo', 'segunda', 'terça', 'quarta', 'quinta', 'sexta', 'sábado']\nconst PERIODOS = [['tudo', 'Tudo'], ['ano', 'Ano'], ['tri', 'Trimestre'], ['ult4', 'Últimas 4']]\nconst COR_BARRA = '#489afa'\n\nconst esc = (s) => String(s ?? '').replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]))\nconst idxCor = (i) => ((i % CORES.length) + CORES.length) % CORES.length\nconst corBase = (i) => CORES[idxCor(i)][0]\nconst grad = (i) => `linear-gradient(90deg, ${CORES[idxCor(i)][0]}, ${CORES[idxCor(i)][1]})`\nconst curto = (nome) => String(nome).replace(/^Squad\\s+/i, '')\nconst stamp = (iso) => {\n  const t = Date.parse(iso || '')\n  return Number.isFinite(t) ? t : Number.MAX_SAFE_INTEGER\n}\nconst plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`\nconst num = (n) => Math.round(n).toLocaleString('pt-BR')\nconst dm = (ciclo) => `${ciclo.slice(8, 10)}/${ciclo.slice(5, 7)}`\nconst inicioSexta = (ciclo) => Date.parse(`${ciclo}T03:00:00Z`) // sexta 00:00 em Brasília\nconst tri = (ciclo) => Math.floor((Number(ciclo.slice(5, 7)) - 1) / 3) + 1\nconst juntar = (xs) => (xs.length <= 1 ? xs.join('') : `${xs.slice(0, -1).join(', ')} e ${xs[xs.length - 1]}`)\n\n// Texto sobre cor chapada: quase-preto em cor clara, branco em cor escura (luminância relativa).\nfunction tinta(hex) {\n  const c = [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255)\n    .map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))\n  const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]\n  return L > 0.2 ? '#0b0f1a' : '#ffffff'\n}\n\nfunction antecedencia(h) {\n  if (h == null || !Number.isFinite(h)) return '—'\n  if (h < 0) return 'no dia da retro'\n  if (h < 1) return 'menos de 1 h antes'\n  if (h < 48) return `${Math.round(h)} h antes`\n  return `${(h / 24).toLocaleString('pt-BR', { maximumFractionDigits: 1 })} dias antes`\n}\n\n// Os que empatam no topo de uma métrica (só valores válidos).\nfunction topo(lista, valor, { menor = false, min = -Infinity } = {}) {\n  const vals = lista.map((x) => ({ x, v: valor(x) })).filter((o) => o.v != null && Number.isFinite(o.v) && o.v > min)\n  if (!vals.length) return { quem: [], v: null }\n  const alvo = menor ? Math.min(...vals.map((o) => o.v)) : Math.max(...vals.map((o) => o.v))\n  return { quem: vals.filter((o) => o.v === alvo).map((o) => o.x), v: alvo }\n}\n\nfunction mediana(xs) {\n  if (!xs.length) return 0\n  const s = xs.slice().sort((a, b) => a - b)\n  const m = Math.floor(s.length / 2)\n  return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2\n}\n\n// ---------------------------------------------------------------- cálculo (puro)\n\nfunction calcular(dados, { periodo = 'tudo', incluirAtual = false } = {}) {\n  const squads = dados.squads || []\n  const atual = dados.ciclo || ''\n  const registros = (dados.registros || []).filter((r) => r && r.ciclo && squads.includes(r.squad))\n  let ciclos = [...new Set(registros.map((r) => r.ciclo))]\n    .filter((c) => (incluirAtual ? c <= atual : c < atual))\n    .sort()\n  if (periodo === 'ano') ciclos = ciclos.filter((c) => c.slice(0, 4) === atual.slice(0, 4))\n  if (periodo === 'tri') ciclos = ciclos.filter((c) => c.slice(0, 4) === atual.slice(0, 4) && tri(c) === tri(atual))\n  if (periodo === 'ult4') ciclos = ciclos.slice(-4)\n  const noPeriodo = new Set(ciclos)\n\n  const porCiclo = ciclos.map((c) => {\n    const vistos = new Set()\n    const regs = registros.filter((r) => r.ciclo === c)\n      .sort((a, b) => stamp(a.chegada) - stamp(b.chegada))\n      .filter((r) => (vistos.has(r.squad) ? false : (vistos.add(r.squad), true)))\n    const porSquad = new Map(regs.map((r) => [r.squad, r]))\n    const fila = regs.filter((r) => Number(r.slots) > 0)\n    const total = fila.reduce((s, r) => s + Math.round(Number(r.slots)) * MIN_POR_SLOT, 0)\n    return { ciclo: c, regs, porSquad, fila, total, primeiro: fila[0] && fila[0].squad, ultimo: regs.length > 1 ? regs[regs.length - 1].squad : null }\n  })\n\n  const st = new Map(squads.map((s, i) => [s, {\n    squad: s, i, min: 0, apres: 0, zeros: 0, pend: 0, primeiros: 0, ultimos: 0, leads: [], alter: 0,\n    maior: { min: 0, ciclo: null }, serie: 0, maiorSerie: 0,\n  }]))\n  const diasSemana = Array(7).fill(0)\n  for (const pc of porCiclo) {\n    for (const s of squads) {\n      const x = st.get(s)\n      const r = pc.porSquad.get(s)\n      if (!r) { x.pend++; x.serie = 0; continue }\n      const n = Math.max(0, Math.round(Number(r.slots) || 0))\n      if (n > 0) {\n        x.apres++\n        x.min += n * MIN_POR_SLOT\n        if (n * MIN_POR_SLOT > x.maior.min) x.maior = { min: n * MIN_POR_SLOT, ciclo: pc.ciclo }\n        x.serie++\n        x.maiorSerie = Math.max(x.maiorSerie, x.serie)\n      } else { x.zeros++; x.serie = 0 }\n      const t = Date.parse(r.chegada || '')\n      if (Number.isFinite(t)) {\n        x.leads.push((inicioSexta(pc.ciclo) - t) / HORA)\n        diasSemana[new Date(t - 3 * HORA).getUTCDay()]++\n      }\n    }\n    if (pc.primeiro) st.get(pc.primeiro).primeiros++\n    if (pc.ultimo) st.get(pc.ultimo).ultimos++\n  }\n\n  const eventos = (dados.eventos || []).filter((e) => e && noPeriodo.has(e.ciclo) && squads.includes(e.squad))\n  for (const e of eventos) {\n    if ((e.acao === 'alterou' && Number(e.antes) !== Number(e.depois)) || e.acao === 'retirou') st.get(e.squad).alter++\n  }\n  const pessoas = new Map()\n  const fontePessoas = eventos.length\n    ? eventos.filter((e) => e.acao === 'declarou' || e.acao === 'alterou')\n    : porCiclo.flatMap((pc) => pc.regs)\n  for (const e of fontePessoas) {\n    const p = String(e.por || '').trim()\n    if (p) pessoas.set(p, (pessoas.get(p) || 0) + 1)\n  }\n\n  const lista = [...st.values()].map((x) => ({\n    ...x,\n    media: x.apres ? x.min / x.apres : 0,\n    lead: x.leads.length ? x.leads.reduce((a, b) => a + b, 0) / x.leads.length : null,\n    declarou: x.apres + x.zeros,\n  }))\n  const minTotal = lista.reduce((s, x) => s + x.min, 0)\n  const med = mediana(lista.map((x) => x.min))\n  const declaracoes = lista.reduce((s, x) => s + x.declarou, 0)\n\n  const g = {\n    retros: ciclos.length,\n    minTotal,\n    mediaRetro: ciclos.length ? minTotal / ciclos.length : 0,\n    passou: porCiclo.filter((pc) => pc.total > META_MIN).length,\n    taxa: ciclos.length && squads.length ? declaracoes / (ciclos.length * squads.length) : 0,\n    mediana: med,\n    longa: topo(porCiclo, (pc) => pc.total),\n    dia: topo(diasSemana.map((n, d) => ({ d, n })), (o) => o.n, { min: 0 }),\n    diaTotal: diasSemana.reduce((a, b) => a + b, 0),\n    pessoa: topo([...pessoas.entries()].map(([p, n]) => ({ p, n })), (o) => o.n, { min: 0 }),\n  }\n  const premios = {\n    maisFalou: topo(lista, (x) => x.min, { min: 0 }),\n    menosFalou: topo(lista.filter((x) => x.declarou > 0), (x) => x.min, { menor: true }),\n    gatilho: topo(lista, (x) => x.primeiros, { min: 0 }),\n    ultimaHora: topo(lista, (x) => x.lead, { menor: true }),\n    planejador: topo(lista, (x) => x.lead),\n    maratona: topo(lista, (x) => x.maior.min, { min: 0 }),\n    fiel: topo(lista, (x) => x.maiorSerie, { min: 1 }),\n    indeciso: topo(lista, (x) => x.alter, { min: 0 }),\n    aviao: topo(lista, (x) => x.pend, { min: 0 }),\n  }\n  // \"Menos falou\" só faz sentido entre 2+ squads que declararam e sem coincidir com quem mais falou.\n  if (lista.filter((x) => x.declarou > 0).length < 2\n    || premios.menosFalou.quem.some((x) => premios.maisFalou.quem.includes(x))) premios.menosFalou = { quem: [], v: null }\n  // Antecedência só compara quando há pelo menos 2 squads com declaração.\n  if (lista.filter((x) => x.lead != null).length < 2) {\n    premios.ultimaHora = { quem: [], v: null }\n    premios.planejador = { quem: [], v: null }\n  }\n  // Badges: cada squad leva os títulos em que está no topo (empates levam todos).\n  const titulos = [\n    ['maisFalou', 'Maratonista'], ['menosFalou', 'Minimalista'], ['gatilho', 'Relâmpago'],\n    ['ultimaHora', 'Última hora'], ['planejador', 'Planejador'], ['fiel', 'Fiel'],\n    ['indeciso', 'Indeciso'], ['aviao', 'Modo avião'],\n  ]\n  for (const x of lista) {\n    x.badges = titulos.filter(([k]) => premios[k].quem.some((q) => q.squad === x.squad)).map(([, t]) => t)\n    if (!x.badges.length) x.badges = [x.declarou ? 'Equilibrado' : 'Sem dados ainda']\n  }\n  return { squads, atual, ciclos, porCiclo, lista, g, premios, periodo, incluirAtual }\n}\n\nfunction rotuloPeriodo(v) {\n  const ano = (v.atual || '').slice(0, 4)\n  if (v.periodo === 'ano') return `de ${ano}`\n  if (v.periodo === 'tri') return `do ${tri(v.atual)}º trimestre de ${ano}`\n  if (v.periodo === 'ult4') return 'das últimas 4 retros'\n  return 'de todas as retros'\n}\n\n// ---------------------------------------------------------------- dados de exemplo\n\nfunction rng(seed) {\n  let a = seed >>> 0\n  return () => {\n    a = (a + 0x6D2B79F5) >>> 0\n    let t = a\n    t = Math.imul(t ^ (t >>> 15), t | 1)\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n  }\n}\n\nfunction exemplo(squads, cicloAtual) {\n  const r = rng(20261030)\n  // perfil fictício por squad: [chance de apresentar, slots típicos, antecedência média em dias]\n  const perfis = [[0.95, 2, 4.5], [0.9, 4, 2], [0.85, 3, 1.2], [0.8, 2, 3], [0.7, 1, 0.6], [0.75, 1, 2.5], [0.6, 2, 0.3], [0.5, 1, 1.5]]\n  const registros = []\n  const eventos = []\n  const base = inicioSexta(cicloAtual)\n  for (let k = 14; k >= 1; k--) {\n    const sexta = new Date(base - k * 7 * 24 * HORA)\n    const ciclo = sexta.toISOString().slice(0, 10)\n    squads.forEach((s, i) => {\n      const [p, slots, dias] = perfis[i % perfis.length]\n      if (r() > 0.92) return // não declarou\n      const n = r() < p ? Math.max(1, Math.min(6, slots + Math.round((r() - 0.5) * 2))) : 0\n      const chegada = new Date(sexta.getTime() - Math.max(0.05, dias + (r() - 0.5) * 2) * 24 * HORA).toISOString()\n      const por = curto(s)\n      registros.push({ ciclo, squad: s, slots: n, chegada, por })\n      eventos.push({ ciclo, squad: s, acao: 'declarou', depois: n, em: chegada, por })\n      if (r() < 0.18 + (i === 2 ? 0.3 : 0)) eventos.push({ ciclo, squad: s, acao: 'alterou', antes: n + 1, depois: n, em: chegada, por })\n    })\n  }\n  return { ciclo: cicloAtual, squads, registros, eventos }\n}\n\n// ---------------------------------------------------------------- stories\n\nfunction historias(v) {\n  const p = v.premios\n  const nomes = (t) => juntar(t.quem.map((x) => curto(x.squad)))\n  const cor = (t) => (t.quem.length === 1 ? corBase(t.quem[0].i) : '#e8ecf3')\n  const out = []\n  out.push({\n    tipo: 'abertura', bg: '#0f131c',\n    eyebrow: `Retro Wrapped · ${rotuloPeriodo(v)}`,\n    big: num(v.g.minTotal), unid: 'min',\n    titulo: 'de apresentações na retro',\n    sub: `${plural(v.g.retros, 'retro', 'retros')} · média de ${num(v.g.mediaRetro)} min por retro`,\n  })\n  if (p.maisFalou.quem.length && p.maisFalou.quem.length <= 3) {\n    const x = p.maisFalou.quem[0]\n    const pct = v.g.minTotal ? Math.round((p.maisFalou.v / v.g.minTotal) * 100) : 0\n    const top3 = v.lista.slice().sort((a, b) => b.min - a.min).slice(0, 3)\n    out.push({\n      tipo: 'maisFalou', bg: cor(p.maisFalou), eyebrow: 'Quem mais falou', big: nomes(p.maisFalou),\n      titulo: `${num(p.maisFalou.v)} min no total`,\n      sub: `${pct}% de todo o tempo de retro${p.maisFalou.quem.length === 1 ? ` · média de ${num(x.media)} min quando apresenta` : ''}`,\n      lista: top3.map((y, k) => `${k + 1}. ${curto(y.squad)} · ${num(y.min)} min`),\n    })\n  }\n  if (p.menosFalou.quem.length && p.menosFalou.quem.length <= 3) {\n    out.push({\n      tipo: 'menosFalou', bg: cor(p.menosFalou), eyebrow: 'Quem menos falou', big: nomes(p.menosFalou),\n      titulo: p.menosFalou.v ? `só ${num(p.menosFalou.v)} min no total` : 'nenhum minuto no período',\n      sub: p.menosFalou.v ? 'Poucas palavras, muito impacto.' : 'O silêncio também comunica.',\n    })\n  }\n  if (p.gatilho.quem.length) {\n    out.push({\n      tipo: 'gatilho', bg: cor(p.gatilho), eyebrow: 'Mais rápido no gatilho', big: nomes(p.gatilho),\n      titulo: `1º da fila ${plural(p.gatilho.v, 'vez', 'vezes')}`,\n      sub: 'Abriu a fila antes de todo mundo.',\n    })\n  }\n  if (p.ultimaHora.quem.length && v.lista.filter((x) => x.lead != null).length > 1) {\n    out.push({\n      tipo: 'ultimaHora', bg: cor(p.ultimaHora), eyebrow: 'Deixou pra última hora', big: nomes(p.ultimaHora),\n      titulo: `declara em média ${antecedencia(p.ultimaHora.v)}`,\n      sub: 'Adrenalina é um estilo de planejamento.',\n    })\n  }\n  if (p.maratona.quem.length) {\n    const x = p.maratona.quem[0]\n    out.push({\n      tipo: 'maratona', bg: cor(p.maratona), eyebrow: 'A maior maratona', big: `${num(p.maratona.v)}`, unid: 'min',\n      titulo: `${nomes(p.maratona)} numa retro só`,\n      sub: p.maratona.quem.length === 1 && x.maior.ciclo ? `Foi na retro de ${dm(x.maior.ciclo)}.` : 'Tempo bem usado.',\n    })\n  }\n  if (p.fiel.quem.length) {\n    out.push({\n      tipo: 'fiel', bg: cor(p.fiel), eyebrow: 'Presença garantida', big: `${p.fiel.v}`, unid: 'seguidas',\n      titulo: `${nomes(p.fiel)} apresentou ${p.fiel.v} retros sem pular`,\n      sub: 'Constância é tudo.',\n    })\n  }\n  if (p.indeciso.quem.length) {\n    out.push({\n      tipo: 'indeciso', bg: cor(p.indeciso), eyebrow: 'Mudou de ideia', big: nomes(p.indeciso),\n      titulo: `${plural(p.indeciso.v, 'alteração', 'alterações')} na fila`,\n      sub: 'Planejamento é um processo.',\n    })\n  }\n  if (v.g.longa.quem.length && v.g.retros > 1) {\n    const pc = v.g.longa.quem[0]\n    out.push({\n      tipo: 'longa', bg: '#f7a048', eyebrow: 'A retro mais longa', big: `${num(v.g.longa.v)}`, unid: 'min',\n      titulo: `na retro de ${dm(pc.ciclo)}`,\n      sub: v.g.passou ? `${v.g.passou} de ${v.g.retros} retros passaram de 1h.` : 'Nenhuma retro passou de 1h.',\n    })\n  }\n  if (v.g.dia.quem.length === 1 && v.g.diaTotal > 2) {\n    const d = v.g.dia.quem[0].d\n    out.push({\n      tipo: 'dia', bg: '#48faa1', eyebrow: 'O dia da fila', big: DIAS[d][0].toUpperCase() + DIAS[d].slice(1),\n      titulo: `${Math.round((v.g.dia.v / v.g.diaTotal) * 100)}% das declarações acontecem na ${DIAS[d]}`,\n      sub: 'É quando a fila enche de verdade.',\n    })\n  }\n  if (v.g.pessoa.quem.length === 1) {\n    out.push({\n      tipo: 'pessoa', bg: '#fa48dc', eyebrow: 'Quem mais preencheu a fila', big: v.g.pessoa.quem[0].p.split(' ')[0],\n      titulo: `${plural(v.g.pessoa.v, 'declaração', 'declarações')} feitas`,\n      sub: 'Obrigado por manter a retro organizada.',\n    })\n  }\n  out.push({ tipo: 'resumo', bg: '#0f131c' })\n  return out\n}\n\nfunction resumoTexto(v) {\n  const p = v.premios\n  const nomes = (t) => juntar(t.quem.map((x) => curto(x.squad)))\n  const linhas = [`🎤 **Retro Wrapped · ${rotuloPeriodo(v)}**`,\n    `${plural(v.g.retros, 'retro', 'retros')} · ${num(v.g.minTotal)} min de apresentações`]\n  if (p.maisFalou.quem.length) linhas.push(`🏆 Quem mais falou: ${nomes(p.maisFalou)} (${num(p.maisFalou.v)} min)`)\n  if (p.menosFalou.quem.length) linhas.push(`🤫 Quem menos falou: ${nomes(p.menosFalou)} (${num(p.menosFalou.v)} min)`)\n  if (p.gatilho.quem.length) linhas.push(`⚡ Mais rápido no gatilho: ${nomes(p.gatilho)} (${p.gatilho.v}× em 1º)`)\n  if (p.ultimaHora.quem.length) linhas.push(`⏰ Deixou pra última hora: ${nomes(p.ultimaHora)} (${antecedencia(p.ultimaHora.v)})`)\n  if (p.maratona.quem.length) linhas.push(`🏃 Maior maratona: ${nomes(p.maratona)} (${p.maratona.v} min numa retro)`)\n  if (p.fiel.quem.length) linhas.push(`🔥 Maior sequência: ${nomes(p.fiel)} (${p.fiel.v} retros seguidas)`)\n  if (p.indeciso.quem.length) linhas.push(`🔁 Mudou de ideia: ${nomes(p.indeciso)} (${p.indeciso.v}×)`)\n  return linhas.join('\\n')\n}\n\n// ---------------------------------------------------------------- pedaços da tela\n\nfunction logo() {\n  const dots = LOGO_XY.map(([x, y], i) => `<i style=\"left:${x}px;top:${y}px;background:${grad(i)}\"></i>`).join('')\n  return `<div class=\"logo\" aria-hidden=\"true\"><div class=\"logo-in\">${dots}</div></div>`\n}\n\nfunction anel(cor) {\n  return `<div class=\"anel\" aria-hidden=\"true\">${LOGO_XY.map(([x, y]) =>\n    `<i style=\"left:${(x / 54) * 100}%;top:${(y / 53) * 100}%;background:${cor}\"></i>`).join('')}</div>`\n}\n\nfunction cardHistoria(v, h, i, total) {\n  const ink = tinta(h.bg)\n  const fundoAnel = ink === '#ffffff' ? 'rgba(255,255,255,.14)' : 'rgba(11,15,26,.12)'\n  let corpo\n  if (h.tipo === 'resumo') {\n    const p = v.premios\n    const linha = (rot, t, val) => (t.quem.length ? `<li><span>${rot}</span><b>${esc(juntar(t.quem.map((x) => curto(x.squad))))}</b><em>${val}</em></li>` : '')\n    corpo = `\n      <p class=\"eyebrow\">Resumo ${esc(rotuloPeriodo(v))}</p>\n      <ul class=\"premios\">\n        ${linha('Quem mais falou', p.maisFalou, `${num(p.maisFalou.v)} min`)}\n        ${linha('Quem menos falou', p.menosFalou, `${num(p.menosFalou.v)} min`)}\n        ${linha('Mais rápido no gatilho', p.gatilho, `${p.gatilho.v}× em 1º`)}\n        ${linha('Deixou pra última hora', p.ultimaHora, antecedencia(p.ultimaHora.v))}\n        ${linha('Maior maratona', p.maratona, `${p.maratona.v} min`)}\n        ${linha('Maior sequência', p.fiel, `${p.fiel.v} retros`)}\n        ${linha('Mudou de ideia', p.indeciso, `${p.indeciso.v}×`)}\n      </ul>\n      <button class=\"btn-copiar js-copiar\">Copiar resumo pro Discord</button>`\n  } else {\n    corpo = `\n      <p class=\"eyebrow\">${esc(h.eyebrow)}</p>\n      <p class=\"big\">${esc(h.big)}${h.unid ? `<small>${esc(h.unid)}</small>` : ''}</p>\n      <p class=\"titulo\">${esc(h.titulo)}</p>\n      <p class=\"sub\">${esc(h.sub)}</p>\n      ${h.lista ? `<ol class=\"top3\">${h.lista.map((l) => `<li>${esc(l)}</li>`).join('')}</ol>` : ''}`\n  }\n  const barras = Array.from({ length: total }, (_, k) =>\n    `<span class=\"${k < i ? 'feito' : k === i ? 'agora' : ''}\"></span>`).join('')\n  return `\n  <div class=\"story\" data-key=\"story-${i}-${h.tipo}\" style=\"background:${h.bg};color:${ink};--ink:${ink}\">\n    <div class=\"progresso\">${barras}</div>\n    ${h.tipo === 'abertura' || h.tipo === 'resumo' ? `<div class=\"anel-logo\" aria-hidden=\"true\">${LOGO_XY.map(([x, y], k) =>\n      `<i style=\"left:${(x / 54) * 100}%;top:${(y / 53) * 100}%;background:${grad(k)}\"></i>`).join('')}</div>` : anel(fundoAnel)}\n    <div class=\"conteudo\">${corpo}</div>\n    <button class=\"nav ant js-ant\" aria-label=\"Anterior\" ${i === 0 ? 'disabled' : ''}>‹</button>\n    <button class=\"nav prox js-prox\" aria-label=\"Próximo\" ${i === total - 1 ? 'disabled' : ''}>›</button>\n  </div>`\n}\n\nfunction kpis(v) {\n  const t = (rot, val, sub) => `<div class=\"kpi\"><span>${rot}</span><b>${val}</b>${sub ? `<em>${sub}</em>` : ''}</div>`\n  return `<div class=\"kpis\">\n    ${t('Retros no período', num(v.g.retros))}\n    ${t('Minutos apresentados', num(v.g.minTotal))}\n    ${t('Média por retro', `${num(v.g.mediaRetro)} min`, 'meta de 60 min')}\n    ${t('Passaram de 1h', `${v.g.passou}`, v.g.retros ? `de ${v.g.retros} retros` : '')}\n    ${t('Squads declarando', `${Math.round(v.g.taxa * 100)}%`, 'das vagas por retro')}\n  </div>`\n}\n\n// Colunas de minutos por retro: uma série só, uma cor, linha de referência em 60 min.\nfunction graficoRetros(v) {\n  const pcs = v.porCiclo\n  if (!pcs.length) return ''\n  const teto = Math.max(META_MIN, ...pcs.map((pc) => pc.total))\n  const passo = teto > 120 ? 60 : 30\n  const max = Math.ceil(teto / passo) * passo\n  const ticks = []\n  for (let t = 0; t <= max; t += passo) ticks.push(t)\n  const cada = Math.max(1, Math.ceil(pcs.length / 7))\n  const cols = pcs.map((pc, k) => {\n    const top3 = pc.fila.slice().sort((a, b) => b.slots - a.slots).slice(0, 3).map((r) => `${curto(r.squad)} ${Number(r.slots) * MIN_POR_SLOT}′`).join(', ')\n    const tip = `Retro de ${dm(pc.ciclo)} · ${pc.total} min · ${plural(pc.fila.length, 'squad', 'squads')}${top3 ? ` · ${top3}` : ''}`\n    return `<div class=\"col\" tabindex=\"0\" data-tip=\"${esc(tip)}\">\n      <div class=\"barra\" style=\"height:${(pc.total / max) * 100}%\"></div>\n      <span class=\"xl\">${k % cada === 0 ? dm(pc.ciclo) : ''}</span>\n    </div>`\n  }).join('')\n  return `\n  <section class=\"card\">\n    <h2>Minutos por retro <small>linha = meta de 1h</small></h2>\n    <div class=\"grafico\">\n      <div class=\"eixo\">${ticks.map((t) => `<span style=\"bottom:${(t / max) * 100}%\">${t}</span>`).join('')}</div>\n      <div class=\"area\">\n        ${ticks.map((t) => `<i class=\"grade\" style=\"bottom:${(t / max) * 100}%\"></i>`).join('')}\n        <i class=\"meta\" style=\"bottom:${(META_MIN / max) * 100}%\"></i>\n        <div class=\"cols\">${cols}</div>\n      </div>\n    </div>\n  </section>`\n}\n\n// Barras horizontais de minutos por squad: nome + marcador de cor ao lado, uma cor nas barras, mediana marcada.\nfunction graficoSquads(v) {\n  const ord = v.lista.slice().sort((a, b) => b.min - a.min || a.i - b.i)\n  const max = Math.max(1, ...ord.map((x) => x.min))\n  const pm = (v.g.mediana / max) * 100\n  const linhas = ord.map((x) => {\n    const tip = `${x.squad} · ${x.min} min · ${plural(x.apres, 'apresentação', 'apresentações')} · média ${num(x.media)} min`\n    return `<div class=\"hl\" tabindex=\"0\" data-tip=\"${esc(tip)}\">\n      <span class=\"nome\"><i style=\"background:${grad(x.i)}\"></i>${esc(curto(x.squad))}</span>\n      <span class=\"trilho\"><span class=\"hbar\" style=\"width:${(x.min / max) * 100}%\"></span></span>\n      <span class=\"val\">${num(x.min)} min</span>\n    </div>`\n  }).join('')\n  return `\n  <section class=\"card\">\n    <h2>Tempo total por squad <small>traço = mediana (${num(v.g.mediana)} min)</small></h2>\n    <div class=\"hbars\" style=\"--pm:${pm}%\">${linhas}</div>\n  </section>`\n}\n\nfunction perfis(v) {\n  const cards = v.lista.map((x) => {\n    const dif = x.min - v.g.mediana\n    const comp = !v.g.retros ? '' : dif === 0 ? 'na mediana' : `${num(Math.abs(dif))} min ${dif > 0 ? 'acima' : 'abaixo'} da mediana`\n    return `\n    <article class=\"perfil\" data-key=\"${esc(x.squad)}\">\n      <div class=\"faixa\" style=\"background:${grad(x.i)}\"></div>\n      <div class=\"p-topo\"><b>${esc(x.squad)}</b></div>\n      <div class=\"badges\">${x.badges.map((b) => `<span>${esc(b)}</span>`).join('')}</div>\n      <dl>\n        <div><dt>Minutos</dt><dd>${num(x.min)}</dd></div>\n        <div><dt>Retros</dt><dd>${x.apres}</dd></div>\n        <div><dt>Média</dt><dd>${num(x.media)} min</dd></div>\n        <div><dt>1º da fila</dt><dd>${x.primeiros}×</dd></div>\n        <div><dt>Sequência</dt><dd>${x.maiorSerie}</dd></div>\n        <div><dt>Alterações</dt><dd>${x.alter}</dd></div>\n      </dl>\n      <p class=\"p-rodape\">Declara ${esc(antecedencia(x.lead))}${comp ? ` · ${comp}` : ''}</p>\n    </article>`\n  }).join('')\n  return `<section class=\"card\"><h2>Perfil de cada squad</h2><div class=\"perfis\">${cards}</div></section>`\n}\n\nfunction tabelas(v) {\n  const linhasSq = v.lista.map((x) => `<tr><td>${esc(x.squad)}</td><td>${x.min}</td><td>${x.apres}</td><td>${x.zeros}</td><td>${x.pend}</td>\n    <td>${num(x.media)}</td><td>${x.primeiros}</td><td>${esc(antecedencia(x.lead))}</td><td>${x.maiorSerie}</td><td>${x.alter}</td></tr>`).join('')\n  const linhasRet = v.porCiclo.map((pc) => `<tr><td>${dm(pc.ciclo)}/${pc.ciclo.slice(0, 4)}</td><td>${pc.total}</td><td>${pc.fila.length}</td>\n    <td>${esc(pc.fila.map((r) => `${curto(r.squad)} ${Number(r.slots) * MIN_POR_SLOT}′`).join(', '))}</td></tr>`).join('')\n  return `\n  <details class=\"card tabelas\">\n    <summary>Ver os dados em tabela</summary>\n    <table><thead><tr><th>Squad</th><th>Min</th><th>Apres.</th><th>Sem apres.</th><th>Não declarou</th><th>Média</th><th>1º</th><th>Antecedência</th><th>Sequência</th><th>Alterações</th></tr></thead><tbody>${linhasSq}</tbody></table>\n    <table><thead><tr><th>Retro</th><th>Min</th><th>Squads</th><th>Ordem</th></tr></thead><tbody>${linhasRet}</tbody></table>\n  </details>`\n}\n\n// ---------------------------------------------------------------- servidor\n\nasync function carregar(d2) {\n  const api = d2.param('api')\n  if (!api) { d2.state.erro = 'A tela está sem o listener configurado (param api).'; return }\n  d2.state.carregando = true\n  try {\n    const r = await d2.rush(api).invoke({ op: 'historico' })\n    const corpo = r && r.historico ? r : r && r.data && r.data.historico ? r.data : null\n    if (!corpo) throw new Error('resposta fora do formato esperado')\n    d2.setState({\n      dados: { ciclo: corpo.ciclo, squads: corpo.squads, registros: corpo.historico.registros || [], eventos: corpo.historico.eventos || [] },\n      erro: '',\n    })\n  } catch (e) {\n    const msg = String((e && e.message) || e || '')\n    d2.state.erro = /401|403|unauthori|permiss/i.test(msg)\n      ? `Sem permissão. Esta tela só funciona aberta pelo preview do builder do lucas-playground. (${msg.slice(0, 160)})`\n      : `Não deu para carregar o histórico (${msg.slice(0, 160) || 'erro desconhecido'}).`\n  } finally {\n    d2.state.carregando = false\n  }\n}\n\nfunction sextaSeguinte() {\n  const brt = new Date(Date.now() - 3 * HORA)\n  const delta = [5, 4, 3, 2, 1, 0, 6][brt.getUTCDay()]\n  return new Date(brt.getTime() + delta * 24 * HORA).toISOString().slice(0, 10)\n}\n\nconst SQUADS_PADRAO = ['Squad Gabz', 'Squad Fernando', 'Squad Mylena', 'Squad Barduco', 'Squad Aninha', 'Squad Bazan', 'Squad Vini', 'Squad Pedro']\n\nfunction mostrarTip(d2, el) {\n  const tip = d2.$('.tip')\n  if (!tip || !el) return\n  tip.textContent = el.getAttribute('data-tip') || ''\n  const r = el.getBoundingClientRect()\n  tip.style.left = `${Math.min(window.innerWidth - 16, Math.max(16, r.left + r.width / 2))}px`\n  tip.style.top = `${r.top - 8}px`\n  tip.classList.add('on')\n}\n\n// ---------------------------------------------------------------- módulo\n\nexport default {\n  css: `\n:host{display:block;height:100%;\n  --bg:#161b26;--bg2:#1c2230;--card:#1f2533;--card2:#262d3d;--hair:#343d50;--hair2:#2b3345;\n  --ink:#e8ecf3;--ink2:#b4bdcb;--ink3:#8b96a8;--ok:#5ee6a8;--warn:#f7a048;--bad:#ff7a88;--serie:${COR_BARRA}}\n*{box-sizing:border-box}\n.pagina{height:100%;overflow:auto;background:radial-gradient(1200px 500px at 15% -10%,rgba(72,154,250,.10),transparent 60%),\n  radial-gradient(900px 400px at 95% 0%,rgba(140,72,250,.10),transparent 60%),var(--bg);\n  color:var(--ink);font-family:Inter,\"Segoe UI\",system-ui,-apple-system,Roboto,sans-serif;font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased}\n.app{max-width:1180px;margin:0 auto;padding:28px 24px 48px}\nbutton{font:inherit;color:inherit}\na{color:inherit}\n\n.top{display:flex;align-items:center;gap:18px;margin-bottom:18px;flex-wrap:wrap}\n.logo{width:62px;height:61px;flex:none}\n.logo-in{position:relative;width:54px;height:53px;transform:scale(1.15);transform-origin:0 0}\n.logo i{position:absolute;width:12px;height:12px;border-radius:50%}\n.tit{flex:1;min-width:220px}\nh1{margin:0;font-size:26px;font-weight:800;letter-spacing:-.02em}\n.subt{margin:2px 0 0;color:var(--ink2)}\n.voltar{padding:7px 12px;border-radius:10px;border:1px solid var(--hair);color:var(--ink2);text-decoration:none;font-weight:600;font-size:13px}\n.voltar:hover{background:var(--card2);color:var(--ink)}\n\n.filtros{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:16px}\n.chip{padding:6px 12px;border-radius:999px;border:1px solid var(--hair);background:transparent;color:var(--ink2);font-weight:600;font-size:13px;cursor:pointer}\n.chip.on{background:var(--ink);color:#0b0f1a;border-color:var(--ink)}\n.chip:hover:not(.on){color:var(--ink);border-color:var(--ink3)}\n.toggle{display:inline-flex;align-items:center;gap:7px;margin-left:6px;color:var(--ink2);font-size:13px;cursor:pointer}\n.toggle input{accent-color:#489afa}\n.tag-ex{margin-left:auto;padding:4px 10px;border-radius:999px;background:rgba(247,160,72,.16);color:var(--warn);font-weight:700;font-size:12px}\n\n.story{position:relative;overflow:hidden;border-radius:22px;min-height:440px;padding:56px 64px 48px;margin-bottom:18px;\n  display:flex;align-items:center;user-select:none}\n.progresso{position:absolute;left:20px;right:20px;top:16px;display:flex;gap:4px}\n.progresso span{flex:1;height:3px;border-radius:2px;background:currentColor;opacity:.22}\n.progresso span.feito,.progresso span.agora{opacity:.9}\n.conteudo{position:relative;z-index:1;max-width:70%}\n.eyebrow{margin:0 0 6px;font-weight:700;font-size:15px;letter-spacing:.02em;text-transform:uppercase;opacity:.85}\n.big{margin:0;font-size:clamp(56px,9vw,120px);line-height:.95;font-weight:900;letter-spacing:-.045em;word-break:break-word}\n.big small{font-size:.32em;font-weight:800;letter-spacing:-.01em;margin-left:.25em}\n.titulo{margin:14px 0 0;font-size:clamp(20px,2.4vw,28px);font-weight:750;letter-spacing:-.01em}\n.sub{margin:6px 0 0;font-size:16px;font-weight:500;opacity:.85}\n.top3{margin:18px 0 0;padding:0;list-style:none;display:grid;gap:4px;font-weight:700;font-size:16px}\n.anel,.anel-logo{position:absolute;right:-40px;bottom:-60px;width:420px;height:412px;pointer-events:none}\n.anel i,.anel-logo i{position:absolute;width:22.2%;height:22.6%;border-radius:50%}\n.anel-logo{right:40px;top:50%;bottom:auto;width:300px;height:294px;margin-top:-147px}\n.nav{position:absolute;top:0;bottom:0;width:22%;border:0;background:transparent;cursor:pointer;font-size:0;z-index:2}\n.nav.ant{left:0}.nav.prox{right:0}\n.nav:disabled{cursor:default}\n.nav:focus-visible{outline:3px solid currentColor;outline-offset:-6px;border-radius:22px}\n.premios{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:8px;min-width:min(560px,100%)}\n.premios li{display:grid;grid-template-columns:200px 1fr auto;gap:12px;align-items:baseline;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.08)}\n.premios span{color:var(--ink3);font-size:13px}\n.premios b{font-size:18px}\n.premios em{font-style:normal;color:var(--ink2);font-weight:600}\n.btn-copiar{position:relative;z-index:3;margin-top:18px;padding:10px 16px;border-radius:12px;border:0;background:#489afa;color:#0b0f1a;font-weight:800;cursor:pointer}\n.btn-copiar:hover{filter:brightness(1.08)}\n.story .conteudo>*{animation:sobe .55s cubic-bezier(.2,.8,.2,1) both}\n.story .conteudo>*:nth-child(2){animation-delay:.08s}.story .conteudo>*:nth-child(3){animation-delay:.16s}\n.story .conteudo>*:nth-child(4){animation-delay:.24s}.story .conteudo>*:nth-child(5){animation-delay:.32s}\n.story .anel,.story .anel-logo{animation:gira .9s cubic-bezier(.2,.8,.2,1) both}\n@keyframes sobe{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}\n@keyframes gira{from{opacity:0;transform:rotate(-40deg) scale(.85)}to{opacity:1;transform:none}}\n@media (prefers-reduced-motion:reduce){.story .conteudo>*,.story .anel,.story .anel-logo{animation:none}}\n.dica{margin:-8px 0 18px;color:var(--ink3);font-size:12.5px;text-align:center}\n\n.card{background:linear-gradient(180deg,rgba(255,255,255,.025),transparent 40%),var(--card);border:1px solid var(--hair2);border-radius:16px;padding:18px 20px}\nh2{margin:0 0 14px;font-size:15px;font-weight:700;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}\nh2 small{font-weight:500;color:var(--ink3);font-size:12.5px}\n.painel{display:grid;gap:18px}\n.duas{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px;align-items:start}\n.kpis{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}\n.kpi{background:var(--card);border:1px solid var(--hair2);border-radius:14px;padding:14px 16px;display:grid;gap:2px}\n.kpi span{color:var(--ink3);font-size:12.5px;font-weight:600}\n.kpi b{font-size:28px;font-weight:800;letter-spacing:-.02em}\n.kpi em{font-style:normal;color:var(--ink3);font-size:12px}\n\n.grafico{display:grid;grid-template-columns:34px 1fr;height:230px;padding-bottom:22px}\n.eixo{position:relative;color:var(--ink3);font-size:11.5px;font-variant-numeric:tabular-nums}\n.eixo span{position:absolute;right:8px;transform:translateY(50%)}\n.area{position:relative;border-left:1px solid var(--hair)}\n.grade{position:absolute;left:0;right:0;height:1px;background:var(--hair2)}\n.meta{position:absolute;left:0;right:0;height:0;border-top:1.5px solid var(--ink2)}\n.cols{position:absolute;inset:0;display:flex;align-items:flex-end;gap:2px;padding:0 4px}\n.col{flex:1;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;position:relative;outline:none;cursor:default}\n.barra{width:min(24px,70%);background:var(--serie);border-radius:4px 4px 0 0;min-height:2px}\n.col:hover .barra,.col:focus .barra{filter:brightness(1.18)}\n.xl{position:absolute;bottom:-20px;font-size:11px;color:var(--ink3);white-space:nowrap;font-variant-numeric:tabular-nums}\n\n.hbars{position:relative;display:grid;gap:10px}\n.hl{display:grid;grid-template-columns:110px 1fr 64px;gap:10px;align-items:center;outline:none}\n.nome{display:flex;align-items:center;gap:7px;font-weight:600;color:var(--ink2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.nome i{width:9px;height:9px;border-radius:50%;flex:none}\n.trilho{position:relative;height:14px}\n.trilho::after{content:\"\";position:absolute;top:-5px;bottom:-5px;left:var(--pm);width:1.5px;background:var(--ink3)}\n.hbar{display:block;height:100%;background:var(--serie);border-radius:0 4px 4px 0;min-width:2px}\n.hl:hover .hbar,.hl:focus .hbar{filter:brightness(1.18)}\n.val{font-weight:700;color:var(--ink2);font-variant-numeric:tabular-nums;text-align:right;font-size:13px}\n\n.perfis{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}\n.perfil{position:relative;border-radius:14px;background:var(--bg2);border:1px solid var(--hair2);padding:16px 14px 12px;overflow:hidden}\n.faixa{position:absolute;left:0;top:0;right:0;height:4px}\n.p-topo b{font-size:15px}\n.badges{display:flex;flex-wrap:wrap;gap:6px;margin:8px 0 10px}\n.badges span{padding:3px 9px;border-radius:999px;background:var(--card2);border:1px solid var(--hair);color:var(--ink);font-weight:700;font-size:12px}\ndl{margin:0;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}\ndl div{display:grid}\ndt{color:var(--ink3);font-size:11.5px;white-space:nowrap}\ndd{margin:0;font-weight:750;font-size:15px}\n.p-rodape{margin:10px 0 0;color:var(--ink3);font-size:12.5px}\n\n.tabelas summary{cursor:pointer;font-weight:700}\n.tabelas table{width:100%;border-collapse:collapse;margin-top:14px;font-size:12.5px}\n.tabelas th,.tabelas td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--hair2);font-variant-numeric:tabular-nums}\n.tabelas th{color:var(--ink3);font-weight:600}\n.nota{margin:4px 0 0;color:var(--ink3);font-size:12.5px;text-align:center}\n\n.tip{position:fixed;z-index:9;transform:translate(-50%,-100%);max-width:320px;padding:8px 10px;border-radius:10px;background:#0f131c;\n  border:1px solid var(--hair);color:var(--ink);font-size:12.5px;font-weight:600;pointer-events:none;opacity:0;transition:opacity .12s}\n.tip.on{opacity:1}\n.estado-tela{max-width:560px;margin:10vh auto;text-align:center;color:var(--ink2)}\n.estado-tela b{display:block;color:var(--ink);font-size:20px;margin-bottom:8px}\n.estado-tela.erro b{color:var(--bad)}\n.estado-tela .acoes{display:flex;gap:10px;justify-content:center;margin-top:16px;flex-wrap:wrap}\n.btn{padding:8px 14px;border-radius:10px;border:1px solid var(--hair);background:transparent;color:var(--ink);cursor:pointer;font-weight:700}\n.btn.forte{background:#489afa;border-color:#489afa;color:#0b0f1a}\n.aviso{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:8;padding:11px 16px;border-radius:12px;background:#121722;\n  border:1px solid rgba(94,230,168,.5);color:var(--ok);font-weight:600;box-shadow:0 10px 30px rgba(0,0,0,.4)}\n\n@media (max-width:960px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.duas{grid-template-columns:1fr}\n  .story{padding:52px 28px 40px;min-height:520px;align-items:flex-start}.conteudo{max-width:100%}\n  .anel,.anel-logo{width:260px;height:255px;right:-50px;bottom:-60px;top:auto;margin:0;opacity:.9}\n  .premios li{grid-template-columns:1fr auto}.premios span{grid-column:1/-1}}\n@media (max-width:560px){.app{padding:18px 16px 32px}h1{font-size:22px}.hl{grid-template-columns:84px 1fr 56px}\n  .tabelas{overflow-x:auto}}\n`,\n\n  render(d2) {\n    const s = d2.state\n    let dados = s.dados\n    if (s.exemplo) dados = exemplo((s.dados && s.dados.squads) || SQUADS_PADRAO, (s.dados && s.dados.ciclo) || sextaSeguinte())\n    const linkFila = d2.param('linkFila')\n    const cabecalho = `\n      <header class=\"top\">\n        ${logo()}\n        <div class=\"tit\"><h1>Retro Wrapped</h1><p class=\"subt\">O recap das retros da Data2: quem falou mais, quem chegou primeiro e outras curiosidades.</p></div>\n        ${linkFila ? `<a class=\"voltar\" href=\"${esc(linkFila)}\" target=\"_top\">← Voltar pra fila</a>` : ''}\n      </header>`\n\n    if (!dados) {\n      const erro = s.erro\n      return `<div class=\"pagina\"><div class=\"app\">${cabecalho}\n        <div class=\"estado-tela ${erro ? 'erro' : ''}\">\n          <b>${erro ? 'Não deu para carregar o recap' : 'Carregando o histórico…'}</b>\n          ${erro ? `<p>${esc(erro)}</p><div class=\"acoes\"><button class=\"btn js-recarregar\">Tentar de novo</button><button class=\"btn js-exemplo\">Ver com dados de exemplo</button></div>` : ''}\n        </div></div></div>`\n    }\n\n    const v = calcular(dados, { periodo: s.periodo || 'tudo', incluirAtual: !!s.incluirAtual })\n    const filtros = `\n      <div class=\"filtros\" role=\"group\" aria-label=\"Período\">\n        ${PERIODOS.map(([k, rot]) => `<button class=\"chip js-periodo ${(s.periodo || 'tudo') === k ? 'on' : ''}\" data-p=\"${k}\">${rot}</button>`).join('')}\n        <label class=\"toggle\"><input type=\"checkbox\" class=\"js-atual\" ${s.incluirAtual ? 'checked' : ''}> incluir a semana atual</label>\n        ${s.exemplo ? '<span class=\"tag-ex\">Dados de exemplo</span><button class=\"chip js-real\">Voltar aos dados reais</button>' : ''}\n      </div>`\n\n    if (!v.ciclos.length) {\n      return `<div class=\"pagina\"><div class=\"app\">${cabecalho}${filtros}\n        <div class=\"estado-tela\">\n          <b>Ainda não tem retro no período</b>\n          <p>O recap conta as retros que já aconteceram. A semana atual entra no sábado, depois da retro de sexta${s.incluirAtual ? '' : ', ou marque \"incluir a semana atual\" para espiar'}.</p>\n          <div class=\"acoes\">${s.exemplo ? '' : '<button class=\"btn forte js-exemplo\">Ver com dados de exemplo</button>'}</div>\n        </div></div></div>`\n    }\n\n    const hs = historias(v)\n    const i = Math.min(Math.max(0, s.idx || 0), hs.length - 1)\n    return `<div class=\"pagina\"><div class=\"app\">\n      ${cabecalho}${filtros}\n      ${cardHistoria(v, hs[i], i, hs.length)}\n      <p class=\"dica\">Clique nas laterais do card ou use ← → para navegar · ${i + 1} de ${hs.length}</p>\n      <div class=\"painel\">\n        ${kpis(v)}\n        <div class=\"duas\">${graficoRetros(v)}${graficoSquads(v)}</div>\n        ${perfis(v)}\n        ${tabelas(v)}\n        <p class=\"nota\">Tempo = slots declarados × 5 min. Contamos só retros que já passaram${s.incluirAtual ? ' (e a semana atual)' : ''}; alterações contam a partir de quando o histórico começou a ser gravado.</p>\n      </div>\n      <div class=\"tip\" role=\"tooltip\"></div>\n      ${s.copiado ? '<div class=\"aviso\" role=\"status\">Resumo copiado. É só colar no Discord.</div>' : ''}\n    </div></div>`\n  },\n\n  mounted(d2) {\n    const total = () => {\n      const s = d2.state\n      const dados = s.exemplo ? exemplo((s.dados && s.dados.squads) || SQUADS_PADRAO, (s.dados && s.dados.ciclo) || sextaSeguinte()) : s.dados\n      return dados ? historias(calcular(dados, { periodo: s.periodo || 'tudo', incluirAtual: !!s.incluirAtual })).length : 0\n    }\n    const ir = (delta) => {\n      const n = total()\n      if (!n) return\n      d2.state.idx = Math.min(n - 1, Math.max(0, (d2.state.idx || 0) + delta))\n    }\n    d2.on('.js-prox', 'click', () => ir(1))\n    d2.on('.js-ant', 'click', () => ir(-1))\n    d2.on(document, 'keydown', (e) => {\n      if (e.key === 'ArrowRight') ir(1)\n      if (e.key === 'ArrowLeft') ir(-1)\n    })\n    d2.on('.js-periodo', 'click', (e, el) => d2.setState({ periodo: el.dataset.p, idx: 0 }))\n    d2.on('.js-atual', 'change', (e, el) => d2.setState({ incluirAtual: !!el.checked, idx: 0 }))\n    d2.on('.js-exemplo', 'click', () => d2.setState({ exemplo: true, idx: 0 }))\n    d2.on('.js-real', 'click', () => d2.setState({ exemplo: false, idx: 0 }))\n    d2.on('.js-recarregar', 'click', () => carregar(d2))\n    d2.on('.js-copiar', 'click', async () => {\n      const s = d2.state\n      const dados = s.exemplo ? exemplo((s.dados && s.dados.squads) || SQUADS_PADRAO, (s.dados && s.dados.ciclo) || sextaSeguinte()) : s.dados\n      const texto = resumoTexto(calcular(dados, { periodo: s.periodo || 'tudo', incluirAtual: !!s.incluirAtual }))\n      try {\n        await navigator.clipboard.writeText(texto)\n        d2.state.copiado = true\n        setTimeout(() => { d2.state.copiado = false }, 3000)\n      } catch (err) {\n        await d2.dialog.prompt('Copie o resumo abaixo:', texto)\n      }\n    })\n    d2.on('[data-tip]', 'mouseover', (e, el) => mostrarTip(d2, el))\n    d2.on('[data-tip]', 'focus', (e, el) => mostrarTip(d2, el))\n    d2.on('[data-tip]', 'mouseout', () => { const t = d2.$('.tip'); if (t) t.classList.remove('on') })\n    d2.on('[data-tip]', 'blur', () => { const t = d2.$('.tip'); if (t) t.classList.remove('on') })\n    carregar(d2)\n  },\n}\n"}},"api":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"api"}}}},"linkFila":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"linkFila"}}}}}}}}}},{"_id":"6af20f09a27a74fb164d34df","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"LoginEvaluacionAVI","package":"lucas-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Login de la Evaluación A/B de AVI (code component, es-MX) — lucas-playground.\n// Param: auth (service: AuthenticationStrategy eval-avi-auth). Usuario = analistXX, contraseña = analistXX.\nconst esc = (s) => String(s == null ? '' : s).replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]));\n\nasync function entrar(d2) {\n  const st = d2.state;\n  if (st.enviando) return;\n  const u = (d2.$('#u') || {}).value || '';\n  const p = (d2.$('#p') || {}).value || '';\n  if (!u.trim() || !p) { d2.setState({ erro: 'Escribe tu usuario y tu contraseña.' }); return; }\n  d2.setState({ enviando: true, erro: '', usuario: u.trim() });\n  try {\n    const a = d2.auth('auth');\n    if (!a) throw new Error('sin estrategia');\n    await a.login({ email: u.trim(), password: p });\n    d2.setState({ enviando: false });\n  } catch (e) {\n    d2.setState({ enviando: false, erro: 'Usuario o contraseña incorrectos.' });\n  }\n}\n\nexport default {\n  css: `\n:host{all:initial;display:block;width:100%;height:100%}\n*{box-sizing:border-box}\n.pg{min-height:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:16px;\n  font:15px/1.5 system-ui,-apple-system,\"Segoe UI\",Roboto,Arial,sans-serif;color:#1d2125;\n  background:linear-gradient(160deg,#fff7ec 0%,#f5f6f8 55%,#eef1f4 100%)}\n.card{width:100%;max-width:400px;background:#fff;border:1px solid #e2e5e9;border-radius:14px;padding:28px 26px;box-shadow:0 10px 30px rgba(29,33,37,.08)}\n.marca{display:flex;align-items:center;gap:10px;margin-bottom:18px}\n.logo{width:38px;height:38px;border-radius:10px;background:#f08a1c;color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;font-size:18px}\nh1{font-size:19px;margin:0}\n.sub{font-size:13px;color:#5a636e;margin:2px 0 0}\nlabel{display:block;font-size:13px;font-weight:600;color:#3b434c;margin:14px 0 6px}\ninput{width:100%;border:1px solid #cdd3da;border-radius:9px;padding:10px 12px;font:inherit;color:#1d2125;background:#fff}\ninput:focus{outline:2px solid #f6c07f;border-color:#f08a1c}\nbutton{width:100%;margin-top:20px;border:0;border-radius:9px;padding:11px 14px;font:inherit;font-weight:700;background:#f08a1c;color:#fff;cursor:pointer}\nbutton:disabled{opacity:.55;cursor:default}\n.erro{margin-top:12px;color:#a1261d;font-size:13px;min-height:18px}\n.ajuda{margin-top:16px;font-size:12px;color:#6a737d}\n`,\n  render(d2) {\n    const st = d2.state;\n    return `<div class=\"pg\"><form class=\"card\" autocomplete=\"on\" novalidate>\n      <div class=\"marca\"><div class=\"logo\">A</div><div><h1>Evaluación de respuestas de AVI</h1><p class=\"sub\">Inicia sesión con el usuario de tu país</p></div></div>\n      <label for=\"u\">Usuario</label>\n      <input id=\"u\" name=\"username\" autocomplete=\"username\" placeholder=\"analistMX\" value=\"${esc(st.usuario || '')}\">\n      <label for=\"p\">Contraseña</label>\n      <input id=\"p\" name=\"password\" type=\"password\" autocomplete=\"current-password\" placeholder=\"••••••••\">\n      <button type=\"submit\" ${st.enviando ? 'disabled' : ''}>${st.enviando ? 'Entrando…' : 'Entrar'}</button>\n      <div class=\"erro\" role=\"alert\">${esc(st.erro || '')}</div>\n      <div class=\"ajuda\">¿Problemas para entrar? Avisa al equipo de AVI.</div>\n    </form></div>`;\n  },\n  mounted(d2) {\n    d2.on('form', 'submit', (e) => { e.preventDefault(); entrar(d2); });\n  },\n};\n"}},"auth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"auth"}}}}}}}}}},{"_id":"6af20f60d321ff3c5404ca96","component":"data2-core-types@Component","system":"67850da249571825a6d31832","params":{"extends":"data2-core-primitives@Block","component":"EvaluacionABAVI_pg","package":"lucas-playground","definition":{"preventEvaluation":true,"component":"data2-core-types@Definition","params":{"def":{"component":"data2-code-components@CodeHost","params":{"code":{"component":"data2-core-primitives@String","params":{"content":"// Evaluación A/B de respuestas de AVI (code component, es-MX) — versión lucas-playground.\n// Params: auth (service: AuthenticationStrategy eval-avi-auth), itens (dataset: ab-itens-tabDataSet), votos (dataset: ab-votos-tabDataSet).\n// El país sale de la sesión (credenciales: email = analistXX, pais = XX). Los textos están en JSON ASCII (JSON.parse).\nconst VERSAO = 'v2-pg';\nconst PAISES = [['MX', 'México'], ['CO', 'Colombia'], ['PE', 'Perú'], ['CL', 'Chile'], ['AR', 'Argentina'], ['EC', 'Ecuador']];\nconst ESCOLHAS = [['A', 'A es mejor'], ['B', 'B es mejor'], ['empate', 'Empate'], ['ambas_malas', 'Ambas malas']];\n\n// ------------------------------------------------------------------ utilidades\nconst esc = (s) => String(s == null ? '' : s).replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' }[c]));\nconst urlOk = (u) => /^https?:\\/\\/[^\\s\"'<>]+$/i.test(u);\nfunction fnv(s) { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h >>> 0; }\nfunction asciiJson(s) { return JSON.stringify(String(s || '')).replace(/[\\u007f-￿]/g, (c) => '\\\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')); }\nfunction lerJson(v, fb) { try { return typeof v === 'string' ? JSON.parse(v) : (v == null ? fb : v); } catch (e) { return fb; } }\nfunction guardar(chave, valor) { try { localStorage.setItem(chave, JSON.stringify(valor)); } catch (e) { /* sin almacenamiento */ } }\nfunction cargar(chave, fb) { try { const v = localStorage.getItem(chave); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } }\n\nfunction linksInline(html) {\n  // html ya escapado: convierte [texto](url) y URLs sueltas en enlaces seguros\n  html = html.replace(/\\[([^\\]]{1,200})\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, (m, t, u) => urlOk(u) ? `<a href=\"${u}\" target=\"_blank\" rel=\"noopener noreferrer\">${t}</a>` : m);\n  return html.replace(/(^|[\\s(>])(https?:\\/\\/[^\\s<)\"]+)/g, (m, p, u) => {\n    const lim = u.replace(/[.,;:!?*]+$/, ''); const resto = u.slice(lim.length);\n    return urlOk(lim) ? `${p}<a href=\"${lim}\" target=\"_blank\" rel=\"noopener noreferrer\">${lim}</a>${resto}` : m;\n  });\n}\nfunction mdInline(t) {\n  let h = esc(t);\n  h = h.replace(/`([^`\\n]{1,200})`/g, '<code>$1</code>');\n  h = h.replace(/\\*\\*\\*([^*\\n]+?)\\*\\*\\*/g, '<b><i>$1</i></b>');\n  h = h.replace(/\\*\\*([^\\n]+?)\\*\\*/g, '<b>$1</b>');\n  h = h.replace(/(^|[^\\w*])\\*(?!\\s)([^*\\n]+?)\\*(?!\\w)/g, '$1<i>$2</i>');\n  h = h.replace(/(^|[^\\w])_(?!\\s)([^_\\n]+?)_(?!\\w)/g, '$1<i>$2</i>');\n  return linksInline(h);\n}\n// Markdown seguro (canal web): títulos, listas, citas, tablas simples, separadores. Todo se escapa antes.\nfunction markdown(texto) {\n  const L = String(texto || '').replace(/\\r\\n?/g, '\\n').split('\\n');\n  const out = []; let i = 0;\n  const lista = (re, tag) => { const it = []; while (i < L.length && re.test(L[i])) { it.push(`<li>${mdInline(L[i].replace(re, ''))}</li>`); i++; } out.push(`<${tag}>${it.join('')}</${tag}>`); };\n  while (i < L.length) {\n    const l = L[i];\n    if (!l.trim()) { i++; continue; }\n    let m;\n    if ((m = l.match(/^\\s{0,3}(#{1,6})\\s+(.*)$/))) { out.push(`<div class=\"h h${Math.min(m[1].length, 4)}\">${mdInline(m[2].replace(/\\s*#+\\s*$/, ''))}</div>`); i++; continue; }\n    if (/^\\s{0,3}([-*_])(\\s*\\1){2,}\\s*$/.test(l)) { out.push('<hr>'); i++; continue; }\n    if (/^\\s*[-*+•]\\s+/.test(l)) { lista(/^\\s*[-*+•]\\s+/, 'ul'); continue; }\n    if (/^\\s*\\d{1,3}[.)]\\s+/.test(l)) { lista(/^\\s*\\d{1,3}[.)]\\s+/, 'ol'); continue; }\n    if (/^\\s*>/.test(l)) { const q = []; while (i < L.length && /^\\s*>/.test(L[i])) { q.push(mdInline(L[i].replace(/^\\s*>\\s?/, ''))); i++; } out.push(`<blockquote>${q.join('<br>')}</blockquote>`); continue; }\n    if (/^\\s*\\|.*\\|\\s*$/.test(l) && i + 1 < L.length && /^\\s*\\|?\\s*:?-{2,}/.test(L[i + 1])) {\n      const cel = (s) => s.trim().replace(/^\\||\\|$/g, '').split('|').map((c) => mdInline(c.trim()));\n      const cab = cel(l); i += 2; const rows = [];\n      while (i < L.length && /^\\s*\\|.*\\|\\s*$/.test(L[i])) { rows.push(cel(L[i])); i++; }\n      out.push(`<table><thead><tr>${cab.map((c) => `<th>${c}</th>`).join('')}</tr></thead><tbody>${rows.map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join('')}</tr>`).join('')}</tbody></table>`);\n      continue;\n    }\n    const p = []; while (i < L.length && L[i].trim() && !/^\\s{0,3}#{1,6}\\s|^\\s*[-*+•]\\s+|^\\s*\\d{1,3}[.)]\\s+|^\\s*>|^\\s*\\|.*\\|\\s*$/.test(L[i])) { p.push(mdInline(L[i])); i++; }\n    if (p.length) out.push(`<p>${p.join('<br>')}</p>`); else { out.push(`<p>${mdInline(l)}</p>`); i++; }\n  }\n  return out.join('');\n}\n// Canal WhatsApp: texto tal cual, con el formato que WhatsApp aplica (*negrita*, _cursiva_, ~tachado~).\nfunction whatsapp(texto) {\n  let h = esc(String(texto || '').replace(/\\r\\n?/g, '\\n'));\n  h = h.replace(/```([\\s\\S]+?)```/g, '<code>$1</code>');\n  h = h.replace(/(^|[\\s(>])\\*(\\S(?:[^\\n]*?\\S)?)\\*(?=[\\s).,;:!?<]|$)/gm, '$1<b>$2</b>');\n  h = h.replace(/(^|[\\s(>])_(\\S(?:[^\\n_]*?\\S)?)_(?=[\\s).,;:!?<]|$)/gm, '$1<i>$2</i>');\n  h = h.replace(/(^|[\\s(>])~(\\S(?:[^\\n~]*?\\S)?)~(?=[\\s).,;:!?<]|$)/gm, '$1<s>$2</s>');\n  return `<div class=\"wa\">${linksInline(h)}</div>`;\n}\nconst respostaHtml = (texto, canal) => (canal === 'whatsapp' ? whatsapp(texto) : `<div class=\"md\">${markdown(texto)}</div>`);\n\n// ------------------------------------------------------------------ estado no reactivo (borradores)\nlet rascunho = {};          // item_id -> comentario (no re-renderiza a cada tecla)\nlet carregando = null;\nlet idxVisto = -1;\n\nfunction usuario(d2) {\n  try { const a = d2.auth('auth'); return (a && a.user) || null; } catch (err) { return null; }\n}\nfunction emailDe(d2) { const u = usuario(d2); return u && u.email ? String(u.email).trim() : ''; }\nfunction paisDe(d2) { const u = usuario(d2); return u && u.pais ? String(u.pais).trim().toUpperCase() : ''; }\nconst nomePais = (c) => (PAISES.find((p) => p[0] === c) || [c, c])[1];\nfunction ehTeste(d2) {\n  try { if (d2.context && d2.context.isBuilder) return true; } catch (e) { /* */ }\n  const h = (typeof location !== 'undefined' && location.hostname) || '';\n  return /data2\\.qa$|data2\\.builders$|localhost|^$/.test(h);\n}\nfunction posicoes(email, it) {\n  const troca = fnv(email + '|' + it.item_id) % 2 === 1;\n  return troca ? { A: { id: it.r2_id, texto: it.r2_texto }, B: { id: it.r1_id, texto: it.r1_texto } }\n               : { A: { id: it.r1_id, texto: it.r1_texto }, B: { id: it.r2_id, texto: it.r2_texto } };\n}\nconst chaveLS = (email, pais) => `avi-ab-${VERSAO}|${email}|${pais}`;\n\nasync function carregarPais(d2, pais) {\n  if (carregando === pais) return;\n  carregando = pais;\n  const st = d2.state;\n  d2.setState({ pais, itens: null, erro: '', idx: 0, enviado: null, verTudo: false, comecou: true });\n  try {\n    const ds = d2.data('itens');\n    if (!ds) throw new Error('sin acceso a los escenarios');\n    try { ds.filter({ pais }); } catch (e) { /* filtro opcional */ }\n    const todos = await ds.loadAll();\n    const brutos = (Array.isArray(todos) && todos.length ? todos : ds.records()).filter((it) => it && it.pais === pais);\n    const lista = brutos.map((it) => ({\n      item_id: it.item_id, tipo: it.tipo, canal: it.canal, ordem: it.ordem,\n      faq_titulo: lerJson(it.faq_titulo, ''), faq_texto: lerJson(it.faq_texto, ''), pergunta: lerJson(it.pergunta, ''),\n      historico: lerJson(it.historico, []), r1_id: it.r1_id, r1_texto: lerJson(it.r1_texto, ''), r2_id: it.r2_id, r2_texto: lerJson(it.r2_texto, ''),\n    })).sort((a, b) => a.ordem - b.ordem);\n    if (!lista.length) throw new Error('sin escenarios');\n    const salvo = cargar(chaveLS(emailDe(d2), pais), { votos: {}, enviados: {} });\n    rascunho = {};\n    Object.keys(salvo.votos || {}).forEach((k) => { rascunho[k] = salvo.votos[k].comentario || ''; });\n    const primeiro = Math.max(0, lista.findIndex((it) => !(salvo.votos || {})[it.item_id]));\n    d2.setState({ itens: lista, votos: salvo.votos || {}, enviados: salvo.enviados || {}, idx: primeiro === -1 ? 0 : primeiro });\n  } catch (e) {\n    d2.setState({ erro: 'No se pudieron cargar los escenarios (' + (e && e.message ? e.message : 'error') + '). Intenta de nuevo.' });\n  } finally { carregando = null; }\n}\n\nfunction persistir(d2) {\n  const st = d2.state;\n  const email = emailDe(d2);\n  if (!st.pais || !email) return;\n  const votos = {};\n  Object.keys(st.votos || {}).forEach((k) => { votos[k] = { escolha: st.votos[k].escolha, comentario: rascunho[k] || '' }; });\n  guardar(chaveLS(email, st.pais), { votos, enviados: st.enviados || {} });\n}\n\nasync function enviar(d2) {\n  const st = d2.state;\n  if (st.enviando) return;\n  const itens = st.itens || [];\n  if (d2.param('previa', false)) { d2.setState({ enviado: { previa: true, ok: itens.filter((it) => (st.votos || {})[it.item_id]).length } }); return; }\n  const assin = (k) => (st.votos[k] ? st.votos[k].escolha + '|' + (rascunho[k] || '') : '');\n  const pend = itens.filter((it) => st.votos[it.item_id] && (st.enviados || {})[it.item_id] !== assin(it.item_id));\n  if (!pend.length) { d2.setState({ enviado: { ok: 0, falha: 0, nada: true } }); return; }\n  d2.setState({ enviando: true, enviado: null });\n  const teste = ehTeste(d2);\n  const email = emailDe(d2);\n  if (!email) { d2.setState({ enviando: false, enviado: { ok: 0, falha: pend.length } }); return; }\n  let ok = 0, falha = 0; const env = Object.assign({}, st.enviados || {});\n  const fila = pend.slice();\n  const trabalhador = async () => {\n    while (fila.length) {\n      const it = fila.shift();\n      const pos = posicoes(email, it);\n      const esc0 = st.votos[it.item_id].escolha;\n      const corpo = { email, item_id: it.item_id, pais: st.pais, escolha: esc0, rid_a: pos.A.id, rid_b: pos.B.id,\n        rid_escolhido: esc0 === 'A' ? pos.A.id : (esc0 === 'B' ? pos.B.id : ''), origem: (typeof location !== 'undefined' && location.origin) || '',\n        comentario: (rascunho[it.item_id] || '').trim() ? asciiJson((rascunho[it.item_id] || '').trim().slice(0, 2000)) : '',\n        versao: VERSAO, teste, cliente_em: new Date().toISOString() };\n      let feito = false;\n      for (let t = 0; t < 3 && !feito; t++) {\n        try {\n          const ds = d2.data('votos');\n          if (!ds) throw new Error('sin acceso');\n          await ds.create(corpo);\n          feito = true;\n        } catch (e) { feito = false; }\n        if (!feito) await new Promise((res) => setTimeout(res, 800 * (t + 1)));\n      }\n      if (feito) { ok++; env[it.item_id] = assin(it.item_id); } else falha++;\n    }\n  };\n  await Promise.all([trabalhador(), trabalhador(), trabalhador()]);\n  d2.setState({ enviando: false, enviados: env, enviado: { ok, falha } });\n  persistir(d2);\n}\n\n// ------------------------------------------------------------------ vistas\nfunction vistaInicio(st) {\n  return `<section class=\"intro\">\n    <h2>¿Cómo funciona?</h2>\n    <p>Vas a ver conversaciones reales de consultoras con AVI. <b>Los datos personales fueron reemplazados por datos ficticios.</b>\n    En cada escenario verás la conversación, la pregunta, la información que AVI tenía disponible y <b>dos respuestas posibles: A y B</b>.</p>\n    <p><b>Elige la que mejor atiende a la consultora</b>, considerando:</p>\n    <ul>\n      <li><b>Correcta:</b> usa solo la información disponible, sin inventar datos (links, teléfonos, plazos, pasos, contactos).</li>\n      <li><b>Útil:</b> responde lo que se preguntó, tomando en cuenta la conversación.</li>\n      <li><b>Clara:</b> fácil de leer y con el formato adecuado al canal (WhatsApp o web).</li>\n      <li><b>Tono:</b> amable y profesional.</li>\n    </ul>\n    <p>Si las dos son igual de buenas, marca <b>Empate</b>. Si ninguna sirve, marca <b>Ambas malas</b>. El comentario es opcional.\n    Tu avance se guarda en este navegador; al final, presiona <b>Enviar mis respuestas</b>.</p>\n    <p><button class=\"pri\" data-act=\"comecar\">Comenzar</button></p>\n  </section>`;\n}\n\nfunction vistaHistorico(it, verTudo) {\n  const h = Array.isArray(it.historico) ? it.historico : [];\n  if (!h.length) return '<p class=\"vazio\">Sin mensajes anteriores.</p>';\n  const N = 8;\n  const mostra = verTudo ? h : h.slice(-N);\n  const msgs = mostra.map((m) => {\n    const cls = m.de === 'Consultora' ? 'c' : (m.de === 'Agente' ? 'g' : (m.de === 'AVI' ? 'b' : 's'));\n    const hora = m.hora ? String(m.hora).slice(11, 16) : '';\n    return `<div class=\"msg ${cls}\"><div class=\"quem\">${esc(m.de)}${hora ? ` · ${esc(hora)}` : ''}</div><div class=\"tx\">${linksInline(esc(m.texto))}</div></div>`;\n  }).join('');\n  const mais = h.length > N ? `<button class=\"lnk\" data-act=\"vertudo\">${verTudo ? 'Ver solo los últimos mensajes' : `Ver toda la conversación (${h.length} mensajes)`}</button>` : '';\n  return `${mais}<div class=\"msgs\">${msgs}</div>`;\n}\n\nfunction vistaItem(st, email) {\n  const it = st.itens[st.idx];\n  const pos = posicoes(email, it);\n  const v = (st.votos || {})[it.item_id];\n  const nf = it.tipo === 'NOT_FOUND';\n  const canal = it.canal === 'whatsapp' ? 'WhatsApp' : 'Web';\n  const bot = (lado) => {\n    const sel = v && v.escolha === lado;\n    return `<div class=\"resp ${sel ? 'sel' : ''}\"><div class=\"rh\"><span class=\"letra\">${lado}</span><span class=\"cn\">Respuesta ${lado} · ${canal}</span></div>${respostaHtml(pos[lado].texto, it.canal)}</div>`;\n  };\n  const botoes = ESCOLHAS.map(([k, t]) => `<button class=\"vt ${v && v.escolha === k ? 'on' : ''} k-${k}\" data-voto=\"${k}\">${t}</button>`).join('');\n  return `<article class=\"item\">\n    <div class=\"ihead\">\n      <span class=\"num\">Escenario ${st.idx + 1} de ${st.itens.length}</span>\n      <span class=\"chip ${nf ? 'nf' : 'faq'}\">${nf ? 'Sin FAQ encontrada' : 'FAQ'}</span>\n      <span class=\"chip canal\">${canal}</span>\n      ${v ? '<span class=\"chip ok\">Respondido</span>' : ''}\n    </div>\n    <details class=\"bloco\" open><summary>Conversación previa</summary>${vistaHistorico(it, st.verTudo)}</details>\n    <div class=\"pergunta\"><div class=\"lbl\">Pregunta de la consultora</div><div class=\"tx\">${linksInline(esc(it.pergunta))}</div></div>\n    <details class=\"bloco faqb\"><summary>Información disponible para AVI${nf ? ' (instrucción para cuando no hay FAQ)' : ''}: <b>${esc(it.faq_titulo)}</b></summary><div class=\"faqtx\">${linksInline(esc(it.faq_texto))}</div></details>\n    <div class=\"duas\">${bot('A')}${bot('B')}</div>\n    <div class=\"voto\">\n      <div class=\"lbl\">¿Cuál respuesta es mejor para la consultora?</div>\n      <div class=\"vts\">${botoes}</div>\n      <textarea data-com=\"${esc(it.item_id)}\" maxlength=\"2000\" placeholder=\"Comentario (opcional): ¿qué te hizo decidir? ¿algún dato inventado o error?\">${esc(rascunho[it.item_id] || '')}</textarea>\n    </div>\n    <div class=\"nav\">\n      <button data-act=\"ant\" ${st.idx === 0 ? 'disabled' : ''}>← Anterior</button>\n      <span class=\"sp\"></span>\n      <button data-act=\"prox\" class=\"pri\" ${st.idx >= st.itens.length - 1 ? 'disabled' : ''}>Siguiente →</button>\n    </div>\n  </article>`;\n}\n\nfunction vistaRodape(st) {\n  const tot = st.itens.length;\n  const resp = st.itens.filter((it) => (st.votos || {})[it.item_id]).length;\n  const env = Object.keys(st.enviados || {}).filter((k) => st.itens.some((it) => it.item_id === k)).length;\n  const pct = Math.round((resp / tot) * 100);\n  const msg = st.enviado ? (st.enviado.previa ? `<span class=\"aviso\">Vista previa: en este archivo las respuestas no se envían (${st.enviado.ok} marcadas).</span>` : st.enviado.nada ? '<span class=\"aviso\">No hay respuestas nuevas para enviar.</span>'\n    : (st.enviado.falha ? `<span class=\"erro\">Se enviaron ${st.enviado.ok}; fallaron ${st.enviado.falha}. Presiona enviar de nuevo.</span>`\n      : `<span class=\"okmsg\">¡Gracias! Se enviaron ${st.enviado.ok} respuestas.</span>`)) : '';\n  const pontos = st.itens.map((it, i) => `<button class=\"pt ${(st.votos || {})[it.item_id] ? 'r' : ''} ${i === st.idx ? 'at' : ''}\" data-ir=\"${i}\" title=\"Escenario ${i + 1}\">${i + 1}</button>`).join('');\n  return `<footer>\n    <div class=\"prog\"><div class=\"barra\"><i style=\"width:${pct}%\"></i></div><span>${resp} de ${tot} respondidos · ${env} enviados</span></div>\n    <div class=\"pts\">${pontos}</div>\n    <div class=\"env\"><button class=\"pri\" data-act=\"enviar\" ${st.enviando || !resp ? 'disabled' : ''}>${st.enviando ? 'Enviando…' : `Enviar mis respuestas (${resp})`}</button>${msg}</div>\n  </footer>`;\n}\n\nexport default {\n  css: `\n:host{all:initial;display:block;width:100%;height:100%}\n*{box-sizing:border-box}\n.app{font:15px/1.5 system-ui,-apple-system,\"Segoe UI\",Roboto,Arial,sans-serif;color:#1d2125;background:#f5f6f8;height:100%;overflow:auto}\nheader{position:sticky;top:0;z-index:2;display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;padding:12px 16px;background:#fff;border-bottom:1px solid #e2e5e9}\nheader h1{font-size:18px;margin:0}\n.quem-sou{font-size:12px;color:#5a636e}\n.sp{flex:1}\n.paises{display:flex;flex-wrap:wrap;gap:6px}\n.paises button{border:1px solid #cdd3da;background:#fff;border-radius:999px;padding:5px 12px;font:inherit;font-size:13px;cursor:pointer;color:#1d2125}\n.paises button.on{background:#1d2125;color:#fff;border-color:#1d2125}\nmain{max-width:1180px;margin:0 auto;padding:16px}\n.intro{background:#fff;border:1px solid #e2e5e9;border-radius:10px;padding:16px 20px}\n.intro h2{margin:0 0 8px;font-size:17px}.intro li{margin:4px 0}.nota{color:#5a636e}\n.item{background:#fff;border:1px solid #e2e5e9;border-radius:10px;padding:14px 16px}\n.ihead{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;margin-bottom:10px}\n.num{font-weight:700}\n.chip{font-size:12px;border-radius:999px;padding:2px 10px;background:#eef1f4;color:#3b434c}\n.chip.nf{background:#fff1d6;color:#7a4b00}.chip.faq{background:#e6f0ff;color:#1f4fb0}.chip.ok{background:#e3f6ea;color:#17653a}\n.bloco{border:1px solid #e8ebee;border-radius:8px;padding:8px 12px;margin:8px 0;background:#fbfbfc}\n.bloco summary{cursor:pointer;font-size:13px;color:#3b434c}\n.msgs{display:flex;flex-direction:column;gap:6px;margin-top:8px;max-height:420px;overflow:auto}\n.msg{max-width:82%;border-radius:10px;padding:6px 10px;border:1px solid #e3e6ea;background:#fff}\n.msg.c{align-self:flex-end;background:#e7f6ec;border-color:#cde8d6}.msg.g{background:#f1ecff;border-color:#ddd3fb}.msg.s{align-self:center;background:#f1f3f5;font-size:13px}\n.quem{font-size:11px;color:#6a737d;margin-bottom:2px}\n.tx,.faqtx{white-space:pre-wrap;word-break:break-word}\n.faqtx{font-size:13px;color:#2f363d;max-height:340px;overflow:auto;margin-top:6px}\n.pergunta{margin:10px 0;padding:10px 12px;border-left:4px solid #1d6b3f;background:#f0faf3;border-radius:6px}\n.lbl{font-size:12px;color:#5a636e;font-weight:600;margin-bottom:4px}\n.duas{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:12px 0}\n.resp{border:1px solid #d9dee4;border-radius:10px;padding:10px 12px;background:#fff;min-width:0}\n.resp.sel{border-color:#1f4fb0;box-shadow:0 0 0 2px #cfe0ff}\n.rh{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n.letra{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:#1d2125;color:#fff;font-weight:700}\n.cn{font-size:12px;color:#5a636e}\n.wa{white-space:pre-wrap;word-break:break-word}\n.md p{margin:6px 0}.md ul,.md ol{margin:6px 0;padding-left:22px}.md li{margin:2px 0}.md blockquote{margin:6px 0;padding:4px 10px;border-left:3px solid #d0d5db;color:#3b434c}\n.md .h{font-weight:700;margin:10px 0 4px}.md .h1{font-size:17px}.md .h2{font-size:16px}.md .h3,.md .h4{font-size:15px}\n.md table{border-collapse:collapse;font-size:13px;margin:6px 0}.md td,.md th{border:1px solid #e1e4e8;padding:3px 6px}\n.md hr{border:0;border-top:1px solid #e1e4e8;margin:8px 0}\ncode{background:#f1f3f5;border-radius:4px;padding:0 4px;font-size:13px}\na{color:#1f4fb0}\n.voto{border-top:1px solid #eceff2;padding-top:10px}\n.vts{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0 8px}\n.vt{border:1px solid #cdd3da;background:#fff;border-radius:8px;padding:8px 14px;font:inherit;cursor:pointer}\n.vt.on{background:#1f4fb0;color:#fff;border-color:#1f4fb0}.vt.k-ambas_malas.on{background:#a1261d;border-color:#a1261d}.vt.k-empate.on{background:#4b5563;border-color:#4b5563}\ntextarea{width:100%;min-height:58px;border:1px solid #cdd3da;border-radius:8px;padding:8px;font:inherit;font-size:14px;resize:vertical}\n.nav{display:flex;align-items:center;gap:8px;margin-top:10px}\nbutton{font:inherit}\n.nav button,.env button{border:1px solid #cdd3da;background:#fff;border-radius:8px;padding:8px 14px;cursor:pointer}\nbutton.pri{background:#1d6b3f;color:#fff;border-color:#1d6b3f}\nbutton:disabled{opacity:.45;cursor:default}\n.lnk{border:0;background:none;color:#1f4fb0;cursor:pointer;padding:4px 0;font-size:13px}\nfooter{position:sticky;bottom:0;background:#fff;border-top:1px solid #e2e5e9;padding:10px 16px;display:flex;flex-direction:column;gap:8px}\n.prog{display:flex;align-items:center;gap:10px;font-size:13px;color:#3b434c}\n.barra{flex:0 0 180px;height:8px;background:#eef1f4;border-radius:4px;overflow:hidden}.barra i{display:block;height:100%;background:#1d6b3f}\n.pts{display:flex;flex-wrap:nowrap;gap:4px;overflow-x:auto;padding-bottom:4px;scrollbar-width:thin}\n.pt{flex:0 0 30px;width:30px;height:26px;border:1px solid #d5dae0;background:#fff;border-radius:6px;font-size:12px;cursor:pointer;padding:0}\n.pt.r{background:#e3f6ea;border-color:#b9e2c7}.pt.at{outline:2px solid #1f4fb0}\n.env{display:flex;flex-wrap:wrap;align-items:center;gap:10px}\n.okmsg{color:#17653a}.erro{color:#a1261d}.aviso{color:#7a4b00}\n.vazio{color:#6a737d;font-size:13px}\n.box{background:#fff;border:1px solid #e2e5e9;border-radius:10px;padding:18px;text-align:center}\n@media (max-width:860px){.duas{grid-template-columns:1fr}.msg{max-width:94%}.barra{flex-basis:110px}main{padding:10px}footer{padding:8px 10px;gap:6px}.item{padding:12px}}\n`,\n  render(d2) {\n    const st = d2.state;\n    const email = emailDe(d2);\n    const pais = paisDe(d2);\n    let corpo;\n    if (!email) corpo = `<div class=\"box\">Para participar necesitas iniciar sesión.</div>`;\n    else if (!pais) corpo = `<div class=\"box erro\">Tu usuario no tiene un país asignado. Avisa al equipo de AVI.</div>`;\n    else if (!st.pais || !st.comecou) corpo = vistaInicio(st);\n    else if (st.erro) corpo = `<div class=\"box erro\">${esc(st.erro)}<br><br><button class=\"pri\" data-pais=\"${esc(st.pais)}\">Reintentar</button></div>`;\n    else if (!st.itens) corpo = `<div class=\"box\">Cargando escenarios…</div>`;\n    else corpo = vistaItem(st, email);\n    return `<div class=\"app\">\n      <header><h1>Evaluación de respuestas de AVI</h1><span class=\"quem-sou\">${email ? esc(email) + (pais ? ' · ' + esc(nomePais(pais)) : '') : ''}</span><span class=\"sp\"></span>\n        ${email ? '<button class=\"lnk\" data-act=\"sair\">Cerrar sesión</button>' : ''}</header>\n      <main>${corpo}</main>\n      ${email && st.comecou && st.pais && st.itens && !st.erro ? vistaRodape(st) : ''}\n    </div>`;\n  },\n  mounted(d2, root) {\n    d2.on('[data-pais]', 'click', (e, el) => { const p = paisDe(d2); if (p) carregarPais(d2, p); });\n    d2.on('[data-voto]', 'click', (e, el) => {\n      const st = d2.state; const it = st.itens && st.itens[st.idx]; if (!it) return;\n      const votos = Object.assign({}, st.votos || {}); votos[it.item_id] = { escolha: el.getAttribute('data-voto') };\n      d2.setState({ votos, enviado: null }); persistir(d2);\n    });\n    d2.on('textarea[data-com]', 'input', (e, el) => { rascunho[el.getAttribute('data-com')] = el.value; persistir(d2); });\n    d2.on('[data-act]', 'click', (e, el) => {\n      const a = el.getAttribute('data-act'); const st = d2.state;\n      if (a === 'ant' && st.idx > 0) d2.setState({ idx: st.idx - 1, verTudo: false });\n      else if (a === 'prox' && st.itens && st.idx < st.itens.length - 1) d2.setState({ idx: st.idx + 1, verTudo: false });\n      else if (a === 'vertudo') d2.state.verTudo = !st.verTudo;\n      else if (a === 'enviar') enviar(d2);\n      else if (a === 'comecar') { const p = paisDe(d2); if (p) { d2.state.comecou = true; if (!st.itens || st.pais !== p) carregarPais(d2, p); } }\n      else if (a === 'sair') { persistir(d2); try { const au = d2.auth('auth'); if (au) au.logout(); } catch (err) { /* */ } }\n    });\n    d2.on('[data-ir]', 'click', (e, el) => { d2.setState({ idx: Number(el.getAttribute('data-ir')) || 0, verTudo: false }); });\n  },\n  updated(d2, root) {\n    const st = d2.state;\n    if (root && idxVisto !== st.idx) {\n      idxVisto = st.idx;\n      const app = root.querySelector('.app'); if (app) app.scrollTop = 0;\n      const box = root.querySelector('.msgs'); if (box) box.scrollTop = box.scrollHeight;\n    }\n  },\n};\n"}},"auth":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"auth"}}}},"itens":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"itens"}}}},"votos":{"component":"data2-core-types@Reference","params":{"reference":{"component":"data2-core-primitives@String","params":{"content":"votos"}}}}}}}}}},{"_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":"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":"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"},{"_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":"6a8381f975a221e62113625c","component":"data2-rush@HTTPListener","reference":"sLSYzN3EtX3kpGdQzYwov","system":"67850da249571825a6d31832","params":{},"versioning":"enabled","bundleId":null}]}},{"reference":"ComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6af20f83e88217babb72f97c","component":"data2-core-primitives@Router","system":"67850da249571825a6d31832","displayName":"router-evaluacion-avi","params":{"routerType":{"component":"data2-core-primitives@String","params":{"content":"browser"}},"routes":[{"component":"data2-core-primitives@Route","params":{"path":"/*","name":"evaluacion","content":{"component":"data2-core-types@ArtboardReference","params":{"reference":"-m1T4Y5NE5M0edl66ONGi"}},"policies":[{"component":"data2-core-auth@Policy","params":{"authenticationStrategy":{"disabled":false,"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"eval-avi-auth"}}}}]}}],"login":{"component":"data2-core-types@ArtboardReference","params":{"reference":"7dmsyo3t7QhHRDgzxaCuh"}}},"reference":"Qts9ezEy8Q7jqwrvn-U8L"},{"_id":"6af20f6fafdd5c8553dfdd15","component":"data2-core-primitives@Artboard","system":"67850da249571825a6d31832","displayName":"Login Evaluacion AVI","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Login Evaluacion AVI"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":800}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"7dmsyo3t7QhHRDgzxaCuh"},{"_id":"6af20f6da27a74fb164d3633","component":"data2-core-primitives@Artboard","system":"67850da249571825a6d31832","displayName":"Evaluacion AVI","params":{"title":{"component":"data2-core-primitives@String","params":{"content":"Evaluacion AVI"}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":800}},"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0}}},"reference":"-m1T4Y5NE5M0edl66ONGi"},{"_id":"591f11b79a1e1e3642eea742","parent":"6af20f6fafdd5c8553dfdd15","component":"lucas-playground@LoginEvaluacionAVI","system":"67850da249571825a6d31832","displayName":"Login (code)","params":{"auth":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"eval-avi-auth"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":800}}}},"reference":"CqMseRdoiS4Nsb_5L5TSQ"},{"_id":"b51dbe2167709f4c12f596c1","parent":"6af20f6da27a74fb164d3633","component":"lucas-playground@EvaluacionABAVI_pg","system":"67850da249571825a6d31832","displayName":"Evaluacion AB (code)","params":{"auth":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"eval-avi-auth"}},"itens":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ab-itens-tabDataSet"}},"votos":{"component":"data2-core-types@InteractiveModuleReference","params":{"reference":"ab-votos-tabDataSet"}},"super":{"position":{"component":"data2-core-types@Position","params":{"x":0,"y":0,"left":true,"right":true,"top":true,"bottom":true}},"dimensions":{"component":"data2-core-types@Dimensions","params":{"width":1280,"height":800}}}},"reference":"X9o5am69ZqgKbW3dCA8j0"}]}},{"_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":"6af20f87afdd5c8553dfdd71","displayName":"Evaluacion AVI","reference":"iFsuE5uqQ2wYB07TUuu0p","params":{"domain":{"component":"data2-core-primitives@String","params":{"content":"evaluacion-avi"}},"entryPoint":{"component":"data2-core-types@ComponentInstanceReference","params":{"component":"data2-core-primitives@Router","reference":"Qts9ezEy8Q7jqwrvn-U8L"}}},"system":"67850da249571825a6d31832","component":"data2-core-primitives@Application"}]}},{"reference":"DataComponentInstancesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[{"_id":"6ab3c7a42086c2e43413fb97","component":"data2-core@Data2TableDataSource","system":"67850da249571825a6d31832","params":{"table":"faqs","schema":[{"component":"data2-core-types@Field","params":{"key":"faq_id","title":"faq_id","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"standard_question","title":"standard_question","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"intent_variations","title":"intent_variations","component":"data2-core-primitives@Array"}},{"component":"data2-core-types@Field","params":{"key":"approved_answer_es_uy","title":"approved_answer_es_uy","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"country","title":"country","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"active","title":"active","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"created_at","title":"created_at","component":"data2-core-primitives@Date"}},{"component":"data2-core-types@Field","params":{"key":"updated_at","title":"updated_at","component":"data2-core-primitives@Date"}}]},"reference":"faqs"},{"_id":"6af20e866486ec24c80ea375","component":"data2-core@AuthenticationStrategy","system":"67850da249571825a6d31832","params":{"credentials":[{"key":"email"},{"key":"password"}],"verifier":{"component":"data2-core-types@DataSetReference","params":{"reference":"eval-avi-analistasDataSet"}},"email":"email","password":"password"},"reference":"eval-avi-auth"},{"_id":"6af20e8293f1ff555d58a800","component":"data2-core@DataSet","system":"67850da249571825a6d31832","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"eval-avi-analistas"}}},"reference":"eval-avi-analistasDataSet"},{"_id":"6af20e82c77e0b1483484521","component":"data2-core@Data2TableDataSource","system":"67850da249571825a6d31832","params":{"table":"eval-avi-analistas","schema":[{"component":"data2-core-types@Field","params":{"key":"email","title":"Usuario","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"password","title":"Contraseña","component":"data2-core-types@Password","required":true}},{"component":"data2-core-types@Field","params":{"key":"pais","title":"País","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"nome","title":"Nombre","component":"data2-core-primitives@String"}}]},"reference":"eval-avi-analistas"},{"_id":"6aedd428709259665ad07996","component":"data2-core@DataSet","system":"67850da249571825a6d31832","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ab-votos-tab"}}},"reference":"ab-votos-tabDataSet"},{"_id":"6aedd4219479fae9bfec984b","component":"data2-core@DataSet","system":"67850da249571825a6d31832","params":{"datasource":{"component":"data2-core-types@DataSourceReference","params":{"reference":"ab-itens-tab"}}},"reference":"ab-itens-tabDataSet"},{"_id":"6aedd427de03aa2fc60ad33a","component":"data2-core@Data2TableDataSource","system":"67850da249571825a6d31832","params":{"table":"ab-votos-tab","schema":[{"component":"data2-core-types@Field","params":{"key":"email","title":"E-mail do analista","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"item_id","title":"Item","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"pais","title":"País","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"escolha","title":"Escolha (A | B | empate | ambas_malas)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"rid_escolhido","title":"Id opaco escolhido","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"rid_a","title":"Id opaco mostrado como A","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"rid_b","title":"Id opaco mostrado como B","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"comentario","title":"Comentário","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"versao","title":"Versão da tela","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"teste","title":"Voto de teste","component":"data2-core-primitives@Boolean"}},{"component":"data2-core-types@Field","params":{"key":"origem","title":"Origin da requisição","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"cliente_em","title":"Hora no navegador","component":"data2-core-primitives@String"}}]},"reference":"ab-votos-tab"},{"_id":"6aedd421709259665ad0798b","component":"data2-core@Data2TableDataSource","system":"67850da249571825a6d31832","params":{"table":"ab-itens-tab","schema":[{"component":"data2-core-types@Field","params":{"key":"item_id","title":"Item","component":"data2-core-primitives@String","required":true}},{"component":"data2-core-types@Field","params":{"key":"pais","title":"País","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"tipo","title":"Tipo (FAQ | NOT_FOUND)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"canal","title":"Canal (whatsapp | web)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"ordem","title":"Ordem no país","component":"data2-core-primitives@Number"}},{"component":"data2-core-types@Field","params":{"key":"faq_titulo","title":"Título da FAQ","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"faq_texto","title":"Texto da FAQ (instructions)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"pergunta","title":"Pergunta (mascarada)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"historico","title":"Histórico legível (JSON, mascarado)","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"r1_id","title":"Resposta 1: id opaco","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"r1_texto","title":"Resposta 1: texto","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"r2_id","title":"Resposta 2: id opaco","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"r2_texto","title":"Resposta 2: texto","component":"data2-core-primitives@String"}},{"component":"data2-core-types@Field","params":{"key":"turno_data","title":"Data do turno real","component":"data2-core-primitives@String"}}]},"reference":"ab-itens-tab"}]}},{"reference":"BundlesTableDataSource","component":"data2-core@MemoryDataSource","params":{"initialData":[]}}],"processesParams":{}}}}