migrado de js para go

This commit is contained in:
Luiz Silva 2026-01-01 19:32:29 -03:00
parent 663a8d5bf2
commit 6f78511946
7 changed files with 964 additions and 83 deletions

View file

@ -1,4 +1,14 @@
(function(){
// Widget NPS (arquivo único).
//
// Regras do projeto (.agent):
// - sem dependências externas
// - fail-closed
// - contratos públicos estáveis
//
// Evolução: regras de negócio do cliente foram movidas para WASM (Go)
// sempre que possível. O backend continua sendo a autoridade.
const DEFAULTS = {
apiBase: '',
cooldownHours: 24,
@ -8,51 +18,55 @@
data_minima_abertura: '',
};
function parseDataMinima(s){
// Aceita somente ISO (data) YYYY-MM-DD.
// Retorna um Date no início do dia (00:00) no horário local.
const v = String(s || '').trim();
if(!v) return null;
const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(v);
if(!m) return null;
const ano = Number(m[1]);
const mes = Number(m[2]);
const dia = Number(m[3]);
if(!ano || mes < 1 || mes > 12 || dia < 1 || dia > 31) return null;
return new Date(ano, mes-1, dia, 0, 0, 0, 0);
}
function antesDaDataMinima(cfg){
const d = parseDataMinima(cfg.data_minima_abertura);
if(!d) return false;
return new Date() < d;
}
function normalizeEmail(email){
return String(email || '').trim().toLowerCase();
}
function cooldownKey(produto, inquilino, usuarioCodigo){
// Prefixo de storage atualizado para o novo nome do projeto.
return `eli-nps:cooldown:${produto}:${inquilino}:${usuarioCodigo}`;
}
function nowMs(){ return Date.now(); }
// ------------------------------------------------------------------
// WASM (Go)
// ------------------------------------------------------------------
function withinCooldown(key){
try{
const v = localStorage.getItem(key);
if(!v) return false;
const obj = JSON.parse(v);
return obj && obj.until && nowMs() < obj.until;
}catch(e){ return false; }
async function carregarWasm(apiBase){
// fail-closed: se o WASM não carregar, o widget não abre.
if(window.__eli_nps_wasm_ready) return true;
if(window.__eli_nps_wasm_loading) return window.__eli_nps_wasm_loading;
window.__eli_nps_wasm_loading = (async function(){
try{
// wasm_exec.js expõe global `Go`.
if(!window.Go){
await carregarScript(`${apiBase}/static/wasm_exec.js`);
}
const go = new Go();
const res = await fetch(`${apiBase}/static/e-li.nps.wasm`, {cache: 'no-cache'});
if(!res.ok) return false;
const bytes = await res.arrayBuffer();
const {instance} = await WebAssembly.instantiate(bytes, go.importObject);
go.run(instance);
return !!window.__eli_nps_wasm_ready;
}catch(e){
return false;
}
})();
return window.__eli_nps_wasm_loading;
}
function setCooldown(key, hours){
try{
const until = nowMs() + hours*3600*1000;
localStorage.setItem(key, JSON.stringify({until}));
}catch(e){}
function carregarScript(src){
return new Promise(function(resolve, reject){
try{
const s = document.createElement('script');
s.src = src;
s.async = true;
s.onload = function(){ resolve(); };
s.onerror = function(){ reject(new Error('script_fail')); };
document.head.appendChild(s);
}catch(e){
reject(e);
}
});
}
function createModal(){
@ -133,71 +147,43 @@
init: async function(opts){
const cfg = Object.assign({}, DEFAULTS, opts || {});
// Bloqueio por data mínima (feature flag simples).
// Ex.: não abrir modal antes de 2026-01-01.
if(antesDaDataMinima(cfg)){
return;
}
// Carrega WASM (Go). Sem WASM, não abrimos o widget (fail-closed).
const okWasm = await carregarWasm(cfg.apiBase);
if(!okWasm) return;
// produto_nome pode ser qualquer string (ex.: "e-licencie", "Cachaça & Churras").
// Regra do projeto: o tratamento/normalização de caracteres deve ser feito
// apenas no backend, exclusivamente para nome de tabela/rotas.
const produtoNome = String(cfg.produto_nome || '').trim();
const inquilino = String(cfg.inquilino_codigo || '').trim();
const usuarioCodigo = String(cfg.usuario_codigo || '').trim();
const email = normalizeEmail(cfg.usuario_email);
// Pré-validação e preparação do payload no WASM.
const pre = window.__eli_nps_wasm_preflight(cfg);
if(!pre || !pre.ok) return;
// controle de exibição: produto + inquilino_codigo + usuario_codigo
if(!produtoNome || !inquilino || !usuarioCodigo){
return; // missing required context
}
// A chave do cooldown é “best-effort” e não participa de nenhuma regra
// de segurança. Mantemos o produto como foi informado.
const chaveCooldown = cooldownKey(produtoNome, inquilino, usuarioCodigo);
if(withinCooldown(chaveCooldown)) return;
// Enviamos exatamente o produto_nome informado.
const payload = {
produto_nome: produtoNome,
inquilino_codigo: inquilino,
inquilino_nome: String(cfg.inquilino_nome || '').trim(),
usuario_codigo: usuarioCodigo,
usuario_nome: String(cfg.usuario_nome || '').trim(),
usuario_telefone: String(cfg.usuario_telefone || '').trim(),
usuario_email: email,
};
// Cooldown visual no browser (WASM faz storage best-effort).
// A chave do cooldown é best-effort e não participa de regra de segurança.
if(window.__eli_nps_wasm_cooldown_ativo(pre.chave_cooldown)) return;
let data;
try{
const res = await postJSON(`${cfg.apiBase}/api/e-li.nps/pedido`, payload);
const res = await postJSON(`${cfg.apiBase}/api/e-li.nps/pedido`, pre.payload);
if(!res.ok) return; // fail-closed
data = await res.json();
}catch(e){
return; // fail-closed
}
if(!data || !data.pode_abrir || !data.id){
// small cooldown to avoid flicker if backend keeps rejecting
setCooldown(chaveCooldown, cfg.cooldownHours);
return;
}
// Backend can return normalized product; use it for building iframe URL.
const produtoRota = data.produto;
if(!produtoRota){
// fail-closed (não dá pra montar URL segura)
setCooldown(chaveCooldown, cfg.cooldownHours);
const dec = window.__eli_nps_wasm_decidir(cfg, data);
if(!dec || !dec.abrir){
// cooldown para evitar flicker se o backend seguir rejeitando.
if(dec && dec.aplicar_cooldown){
window.__eli_nps_wasm_set_cooldown(pre.chave_cooldown, dec.cooldown_ate_ms);
}
return;
}
const modal = createModal();
const iframe = document.createElement('iframe');
iframe.src = `${cfg.apiBase}/e-li.nps/${produtoRota}/${data.id}/form`;
iframe.src = `${cfg.apiBase}/e-li.nps/${dec.produto_rota}/${dec.id}/form`;
modal.panel.appendChild(iframe);
// Visual cooldown so it doesn't keep popping (even if user closes).
setCooldown(chaveCooldown, cfg.cooldownHours);
window.__eli_nps_wasm_set_cooldown(pre.chave_cooldown, dec.cooldown_ate_ms);
}
};
})();