dtf-site.html held commercial rules, the nesting engine, PDF analysis, the cart and every handler in a single inline script, 42% of the runtime code in one file, and the money logic lived in the middle of it. It is now nine files under local/static, cut at the section markers the original author left, so no function was split across a boundary: config, product modes, upload, sheet analysis, PDF, quality, packing, cart, flow. They load as classic scripts in the original order and share one global scope, so evaluation is exactly what it was; the extraction was checked byte-identical against the original before the tags replaced it. dtf-site.html is 1,394 lines of markup and style. With no inline script left anywhere, the policy no longer needs a hash allowlist: script-src is now 'self' alone, which is stronger than what it replaced and cannot drift as the page changes. Three things depended on the old shape and were updated rather than worked around. The pricing parity test read the ladder out of the HTML and now reads it from site-config.js, still proving the server agrees with what the customer is shown. The isolated artwork test served four hardcoded script paths and now serves any script that resolves inside local/static, so the next file added does not silently 404. The CSP assertion checked the whole policy for 'unsafe-inline' and now checks the script-src directive alone, since style-src legitimately carries it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
9.4 KiB
JavaScript
187 lines
9.4 KiB
JavaScript
/* Site DTF — The grade shown to the customer, and the reservations attached to it.
|
||
Extracted verbatim from the single inline script in dtf-site.html.
|
||
Loaded as classic scripts in the order listed there: they share one global
|
||
scope and run top to bottom, exactly as the original did. */
|
||
// ── enviar
|
||
// Avalia sozinho conforme o cliente sobe e preenche. Devolve o que ainda falta,
|
||
// para a fila dizer em texto em vez de travar num botão.
|
||
function avaliar(){
|
||
if(ehFolha()){
|
||
if(!folhas.length) return {pronto:false, falta:'Arraste sua folha montada para começar.'};
|
||
const larga=folhas.find(x=>x.med && x.med.larg > larguraFilme()+0.5);
|
||
if(larga) return {pronto:false, falta:'A folha '+larga.f.name+' tem '+n1(larga.med.larg)+
|
||
' cm e não cabe no filme de '+n1(larguraFilme())+' cm.'};
|
||
const semMedida=folhas.filter(x=>!(x.m>0)).length;
|
||
if(semMedida) return {pronto:false, falta:'Informe o comprimento de '+semMedida+
|
||
' folha'+(semMedida>1?'s':'')+'.'};
|
||
metros=folhaTotalM();
|
||
reencaixado=false;
|
||
const ans=folhas.map(x=>x.an).filter(Boolean);
|
||
if(!ans.length){
|
||
// sem análise possível (PDF, CDR, PSD): não inventa nota
|
||
const exts=[...new Set(folhas.map(f=>extDe(f.f.name)))].join(', ');
|
||
mostraNota([
|
||
['av','Sem nota · '+exts+' não abre no navegador',
|
||
'não temos como conferir a arte antes de imprimir'],
|
||
['er','Metro pela tabela · '+rs(TABELA[modo]),
|
||
'exportando em PNG ou PDF, cai até '+rs(pisoEscada())],
|
||
['fix','Alguém abre seu arquivo','a conferência é feita na mão, na sala']
|
||
], 0, folhas.length>1? folhas.length+' folhas' : folhas[0].f.name,
|
||
larguraFilme()+' cm · '+fmtM(metros)+' m · preço de tabela');
|
||
return {pronto:true};
|
||
}
|
||
const dpi=Math.min(...ans.map(a=>a.dpi));
|
||
const n=Math.max(6,Math.min(100,Math.round(dpi/DPI_AVISA*100)));
|
||
const soma=(k)=>ans.reduce((t,a)=>t+a[k],0);
|
||
const itens=[
|
||
ans.every(a=>a.vetor)
|
||
? ['ok','Arte vetorial','sem imagem embutida · imprime em qualquer tamanho']
|
||
: [dpi>=DPI_AVISA?'ok':dpi>=DPI_RECUSA?'av':'er','Resolução '+dpi+' DPI',
|
||
ans.some(a=>a.fonteDpi==='imagens')
|
||
? 'média por área das imagens dentro do PDF · pior em '+
|
||
Math.min(...ans.map(a=>a.res? a.res.pior : a.dpi))+' DPI'
|
||
: 'resolução do arquivo · arte ampliada antes de exportar não aparece aqui'],
|
||
['ok', soma('artes')+' peças na folha',
|
||
Math.round(soma('aproveitamento')/ans.length)+'% da folha virou arte'],
|
||
soma('residuos') ? ['er','Resíduo de recorte',
|
||
soma('residuos')+' ponto'+(soma('residuos')>1?'s':'')+' abaixo de 2 mm · a impressora imprime']
|
||
: ['ok','Sem resíduo de recorte','nada solto na folha'],
|
||
soma('encostadas') ? ['fix', soma('encostadas')+' peças a menos de 5 mm',
|
||
'se forem artes diferentes, separamos com 5 mm sem custo']
|
||
: ['ok','Espaço entre peças','nenhuma abaixo de 6 mm'],
|
||
ans.some(a=>a.fundoChapado) ? ['er','Fundo chapado','a folha está sem transparência']
|
||
: ['ok','Fundo transparente','canal alfa conferido'],
|
||
null
|
||
].filter(Boolean);
|
||
mostraNota(itens, n, folhas.length>1? folhas.length+' folhas' : folhas[0].f.name,
|
||
larguraFilme()+' cm · '+fmtM(metros)+' m');
|
||
return {pronto:true};
|
||
}
|
||
if(!artes.length) return {pronto:false, falta:'Arraste suas artes para começar.'};
|
||
const sem=artes.filter(a=>!a.cm).length;
|
||
if(sem) return {pronto:false, falta:'Informe a largura de '+sem+' arte'+(sem>1?'s':'')+
|
||
' para ver a nota e o preço.'};
|
||
// metragem = altura da folha depois de montada, com os 5 mm entre peças
|
||
metros=+(montagemCm/100).toFixed(4);
|
||
if(!(metros>0)) return {pronto:false, falta:'Montando a folha…'};
|
||
const notas=artes.map(a=>Math.max(6,Math.min(100,Math.round((a.px/(a.cm/2.54))/300*100))));
|
||
const media=Math.round(notas.reduce((s,n)=>s+n,0)/notas.length), pior=Math.min(...notas);
|
||
mostraNota([
|
||
['ok','Montagem por nossa conta','5 mm garantidos'],
|
||
[cls(pior),'Pior arte em '+pior+'%',pior>=85?'todas em boa resolução':'reenviar maior sai mais barato']
|
||
],media,artes.length+' arte'+(artes.length>1?'s':''),
|
||
'folha montada por nós · '+fmtM(metros)+' m');
|
||
return {pronto:true};
|
||
}
|
||
|
||
// redesenha 220 ms depois da última mexida, para não avaliar a cada tecla
|
||
let tAval=null;
|
||
function agendaAvaliacao(){
|
||
clearTimeout(tAval);
|
||
tAval=setTimeout(()=>{
|
||
const r=avaliar();
|
||
const el=$('aoVivo');
|
||
if(!el) return;
|
||
if(r.pronto){
|
||
el.className='aoVivo pronto';
|
||
el.innerHTML='<span class="pt2"></span><span><b>Nota e preço calculados.</b> '+
|
||
'Confira abaixo e feche o pedido.</span>';
|
||
}else{
|
||
el.className='aoVivo';
|
||
el.innerHTML='<span class="pt2"></span><span>'+escapeHTML(r.falta)+'</span>';
|
||
limpaPaineis(); // o carrinho só some se o pedido estiver vazio
|
||
}
|
||
},220);
|
||
}
|
||
function mostraNota(itens,n,titulo,sub){
|
||
nota=n;
|
||
$('qual').classList.add('on');
|
||
$('qNome').textContent=titulo; $('qSub').textContent=sub;
|
||
const semNota = n===0;
|
||
$('pct').innerHTML = semNota? '—<small>/100</small>' : n+'<small>/100</small>';
|
||
const c = semNota? 'var(--fraco)' : n>=85?'var(--verde)':n>=50?'var(--laranja)':'var(--vermelho)';
|
||
$('pct').style.color=c;
|
||
$('qbi').style.width=(semNota?0:n)+'%'; $('qbi').style.background=c;
|
||
$('qitens').innerHTML=itens.map(i=>
|
||
'<div class="qi '+i[0]+'"><span class="c">'+
|
||
(i[0]==='ok'?'✓':i[0]==='av'?'!':i[0]==='fix'?'↻':'×')+'</span>'+
|
||
'<div><b>'+escapeHTML(i[1])+'</b><span>'+escapeHTML(i[2])+'</span></div></div>').join('');
|
||
$('qmsg').style.display='none';
|
||
ressalvaResolucao();
|
||
carrinho();
|
||
}
|
||
|
||
// A resolução do arquivo não é a das artes dentro dele: um PDF em 300 DPI
|
||
// pode ter uma imagem inserida em 180, e é essa que sai serrilhada.
|
||
function ressalvaResolucao(){
|
||
const box=$('ciente'), chk=$('cienteOk');
|
||
let baixas=[], geral=300, recusadas=[];
|
||
if(ehFolha()){
|
||
// Uma régua só: o número que precifica é o número que bloqueia.
|
||
// Recusa quando a média por área fica abaixo de 96 DPI — a folha inteira não
|
||
// vale o filme. Se a média passa mas há peças ruins, é ressalva com aceite.
|
||
folhas.forEach((x,k)=>{
|
||
if(!x.an) return;
|
||
const r=x.an.res, cm=x.med? x.med.larg : larguraFilme();
|
||
const nome=(folhas.length>1? (k+1)+'. ':'')+x.f.name;
|
||
const extra = r ? {pior:r.pior, de:r.total,
|
||
quantas:r.quantasBaixas, parte:Math.round(r.fracBaixa*100),
|
||
quantasAviso:r.quantasAviso, parteAviso:Math.round(r.fracAviso*100)} : {};
|
||
if(x.an.dpi<DPI_RECUSA) recusadas.push(Object.assign({nome, dpi:x.an.dpi, cm}, extra));
|
||
else if(x.an.dpi<DPI_AVISA || (r&&r.quantasAviso))
|
||
baixas.push(Object.assign({nome, dpi:x.an.dpi, cm}, extra));
|
||
});
|
||
geral = folhas.some(x=>x.an) ? Math.max(...folhas.filter(x=>x.an).map(x=>x.an.dpi)) : 300;
|
||
}else{
|
||
const comTamanho=artes.filter(a=>a.cm>0);
|
||
recusadas=comTamanho.filter(a=>dpiDe(a)<DPI_RECUSA)
|
||
.map(a=>({nome:a.f.name, dpi:Math.round(dpiDe(a)), cm:a.cm}));
|
||
baixas=comTamanho.filter(a=>dpiDe(a)>=DPI_RECUSA && dpiDe(a)<DPI_AVISA)
|
||
.map(a=>({nome:a.f.name, dpi:Math.round(dpiDe(a)), cm:a.cm}));
|
||
const todas=comTamanho.map(a=>dpiDe(a));
|
||
geral = todas.length? Math.round(Math.max(...todas)) : 300;
|
||
}
|
||
// Abaixo de 96 DPI a sala não imprime. Não é desconto, é recusa —
|
||
// e não existe aceite que destrave.
|
||
if(recusadas.length){
|
||
$('cienteTxt').innerHTML=
|
||
'<p><b>A média da folha ficou abaixo de '+DPI_RECUSA+' DPI.</b> Nesse ponto a peça '+
|
||
'sai serrilhada a ponto de não valer o filme. Reenvie maior ou reduza a largura.</p>'+
|
||
'<div class="lista">'+recusadas.map(b=>'<div><b>'+escapeHTML(b.nome)+'</b> — '+
|
||
(b.quantas
|
||
? 'média de '+b.dpi+' DPI · pior imagem em '+b.pior+' DPI · '+b.quantas+
|
||
' de '+b.de+' imagens abaixo de '+DPI_RECUSA+' DPI, cobrindo '+b.parte+'% da arte'
|
||
: b.dpi+' DPI em '+n1(b.cm)+' cm · precisa de '+
|
||
Math.ceil(DPI_AVISA*b.cm/2.54)+' px de largura')+'</div>')
|
||
.join('')+'</div>';
|
||
$('cienteTit').textContent='Resolução abaixo do que imprimimos';
|
||
box.className='ciente on recusa'; chk.checked=false; $('qOk').disabled=true;
|
||
return;
|
||
}
|
||
$('cienteTit').textContent='Ressalva de resolução';
|
||
box.className='ciente';
|
||
if(!baixas.length){
|
||
box.classList.remove('on'); chk.checked=false; $('qOk').disabled=false; return;
|
||
}
|
||
const menor=baixas.reduce((p,b)=>b.dpi<p.dpi?b:p);
|
||
const media=Math.round(baixas.reduce((s,b)=>s+b.dpi,0)/baixas.length);
|
||
$('cienteTxt').innerHTML=
|
||
'<p><b>Parte da arte está abaixo de '+DPI_AVISA+' DPI.</b> Essas peças podem sair '+
|
||
'com traço serrilhado, e isso não tem conserto depois de impresso. '+
|
||
'O resto da folha sai normal.</p>'+
|
||
'<div class="lista">'+baixas.map(b=>'<div><b>'+escapeHTML(b.nome)+'</b> — '+
|
||
(b.quantasAviso
|
||
? 'média de '+b.dpi+' DPI · '+b.quantasAviso+' de '+b.de+' imagens abaixo de '+
|
||
DPI_AVISA+' DPI, cobrindo '+b.parteAviso+'% da arte'+
|
||
(b.quantas? ' · dessas, '+b.quantas+' abaixo de '+DPI_RECUSA+' DPI ('+
|
||
b.parte+'% da arte)' : '')
|
||
: b.dpi+' DPI'+(b.cm? ' em '+n1(b.cm)+' cm':''))+
|
||
'</div>').join('')+'</div>';
|
||
box.className='ciente on'; chk.checked=false; $('qOk').disabled=true;
|
||
}
|
||
$('cienteOk').addEventListener('change',e=>{
|
||
if($('ciente').classList.contains('recusa')) return; // recusa não se aceita
|
||
$('qOk').disabled=!e.target.checked;
|
||
});
|
||
|