Compare commits

..

5 Commits

Author SHA1 Message Date
admtracksteel ab31a3e39c Trigger deployment webhook update 2026-03-12 10:54:00 +00:00
admtracksteel 7ce05fd240 Update Oxicorte process option label from 5mm to 6mm. 2026-03-12 10:28:56 +00:00
admtracksteel eb5babb4f3 update index.html - teste de envio automatico 2026-03-12 10:12:52 +00:00
root 655ee9cba1 Upload oficial do TSCUT 2d 2026-03-10 12:00:53 +00:00
root d8d8a3fbce Upload inicial do TSCUT 2026-03-10 11:03:54 +00:00
10 changed files with 2541 additions and 1645 deletions
-22
View File
@@ -1,22 +0,0 @@
---
trigger: always_on
---
# GEMINI.md - Antigravity Kit (Auto-Generated)
## TIER 0: UNIVERSAL RULES (Always Active)
### 🌍 Global User Rules (MANDATORY)
1. **Idioma**: Conversar e desenvolver sempre em **Português do Brasil**.
2. **Automação Terminais**: Executar comandos de terminal, leitura de arquivos e edições de forma automática. Comandos de desenvolvimento (build, install, lint, git, etc.) são sempre 'SafeToAutoRun'.
3. **Memória & Monitoramento**: Após cada evolução ou atualização no app, é OBRIGATÓRIO:
- Atualizar a memória do app em '/root/Antigravity_Memory/knowledge/{AppName}.md'.
- Atualizar o status e credenciais no 'app.py' do Cronos Dashboard.
- Realizar 'git commit', 'git push' e executar o 'update.sh' do Cronos para deploy automático.
### 🌐 Language Handling
---
-10
View File
@@ -1,10 +0,0 @@
VITE_LOGTO_ENDPOINT=https://logto-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io
VITE_LOGTO_APP_ID=3whpc0jru4ka5nerinnuc
VITE_LOGTO_ORG_ID=4j7uhxlmia9e
VITE_SUPABASE_SCHEMA=TSCUT
# --- LOGTO_SUPA UPGRADE ---
VITE_LOGTO_ENDPOINT=https://logto-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io
VITE_LOGTO_APP_ID=3whpc0jru4ka5nerinnuc
VITE_LOGTO_ORG_ID=4j7uhxlmia9e
VITE_SUPABASE_SCHEMA=TSCUT
-1
View File
@@ -4,4 +4,3 @@ dist
.vscode .vscode
.DS_Store .DS_Store
*.local *.local
package-lock.json
+4 -5
View File
@@ -5,14 +5,14 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/logo.png" type="image/png"> <link rel="icon" href="/logo.png" type="image/png">
<title>Otimizador de Corte - TSCUT</title> <title>Otimizador de Corte 2D- TSCUT</title>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<header> <header>
<h1><img src="/logo.png" alt="Logo" class="app-logo">Otimizador de Plano de Corte</h1> <h1><img src="/logo.png" alt="Logo" class="app-logo">Otimizador de Plano de Corte 2D</h1>
<p>Barras Lineares | Algoritmo FFD Avançado | Otimização com Múltiplas Barras</p> <p>Barras Lineares | Otimização com Múltiplas Barras</p>
<button id="themeToggle" class="theme-toggle-btn" onclick="toggleTheme()" title="Alternar Tema">🌙</button> <button id="themeToggle" class="theme-toggle-btn" onclick="toggleTheme()" title="Alternar Tema">🌙</button>
</header> </header>
@@ -48,7 +48,7 @@
</label> </label>
<label class="process-option"> <label class="process-option">
<input type="radio" name="cuttingProcess" value="oxy" onchange="updateProcess()"> <input type="radio" name="cuttingProcess" value="oxy" onchange="updateProcess()">
<span class="process-name">Oxicorte (5mm)</span> <span class="process-name">Oxicorte (6mm)</span>
</label> </label>
</div> </div>
</div> </div>
@@ -157,7 +157,6 @@
<div class="export-buttons"> <div class="export-buttons">
<button class="btn-primary" onclick="exportHTML()">📄 Exportar</button> <button class="btn-primary" onclick="exportHTML()">📄 Exportar</button>
<button class="btn-primary" onclick="printReport()">🖨️ Imprimir</button> <button class="btn-primary" onclick="printReport()">🖨️ Imprimir</button>
<button class="btn-primary" onclick="printCompactReport()" style="background-color: #27ae60;">🖨️ Rel.Compacto</button>
</div> </div>
</div> </div>
</div> </div>
-2
View File
@@ -1,2 +0,0 @@
[phases.setup]
nixPkgs = ["nodejs_23"]
+1153
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -18,8 +18,6 @@
"license": "ISC", "license": "ISC",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@logto/browser": "^3.0.13",
"@logto/react": "^4.0.14",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
+46 -265
View File
@@ -1,76 +1,3 @@
// --- LOGTO AUTH GUARD ---
import LogtoClient from '@logto/browser';
// Escondemos o app imediatamente
const style = document.createElement('style');
style.innerHTML = 'body { display: none !important; }';
document.head.appendChild(style);
// Implementação de Storage Persistente para evitar perda de sessão em cross-domain
const persistentStorage = {
getItem: (key) => localStorage.getItem(key),
setItem: (key, value) => {
try { localStorage.setItem(key, value); } catch (e) { console.error('Storage error', e); }
},
removeItem: (key) => localStorage.removeItem(key),
};
const logtoClient = new LogtoClient({
endpoint: import.meta.env.VITE_LOGTO_ENDPOINT,
appId: import.meta.env.VITE_LOGTO_APP_ID,
scopes: ['openid', 'offline_access', 'profile', 'email', 'organizations'],
storage: persistentStorage // CRÍTICO: localStorage forçado
});
async function protectPage() {
const urlParams = new URLSearchParams(window.location.search);
const isCallback = urlParams.has('code') && urlParams.has('state');
// A URI exata para a qual o Logto vai devolver o usuário
const redirectUri = window.location.origin + '/';
if (isCallback) {
try {
// Logto DEVE receber a URL completa com parâmetros para extrair o state
await logtoClient.handleSignInCallback(window.location.href);
console.log('TSCUT Auth: Login bem sucedido. Recarregando app...');
window.location.assign('/');
} catch (error) {
style.remove();
document.body.innerHTML = `
<div style="padding: 20px; color: #ff5555; background: #1a1a1a; min-height: 100vh; font-family: sans-serif;">
<h1>Falha na Autenticação Industrial</h1>
<p style="font-size: 1.2em;">Erro: ${error.message}</p>
<button onclick="window.location.assign('/')" style="margin-top: 20px; padding: 12px 25px; cursor: pointer;">
Tentar Novamente
</button>
</div>`;
console.error('Falha no callback do Logto:', error);
}
return;
}
try {
const isAuthenticated = await logtoClient.isAuthenticated();
if (!isAuthenticated) {
console.log('TSCUT Auth: Redirecionando para login Logto...');
await logtoClient.signIn(redirectUri);
return;
}
// Acesso permitido
style.remove();
} catch (err) {
console.error('Erro de autenticação:', err);
await logtoClient.signIn(redirectUri);
}
}
// Execução imediata
protectPage();
// -------------------------
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import Papa from 'papaparse'; import Papa from 'papaparse';
import './style.css'; import './style.css';
@@ -495,13 +422,14 @@ function showFeedback(message, type) {
showToast(message, type); showToast(message, type);
} }
// ===== ALGORITMO OTIMIZADO AVANÇADO (Programação Dinâmica / Knapsack) ===== // ===== ALGORITMO FFD AVANÇADO =====
function optimizeCutting() { function optimizeCutting() {
if (availableBars.length === 0 || demandPieces.length === 0) { if (availableBars.length === 0 || demandPieces.length === 0) {
showToast("Adicione barras e peças", "warning"); showToast('Adicione barras e peças', 'warning');
return; return;
} }
// Expandir peças com ID único global
const expandedPieces = []; const expandedPieces = [];
let uniqueIdCounter = 0; let uniqueIdCounter = 0;
demandPieces.forEach(p => { demandPieces.forEach(p => {
@@ -510,12 +438,14 @@ function optimizeCutting() {
} }
}); });
// FFD em cada barra disponível
const usedBars = []; const usedBars = [];
let unusedPieces = [...expandedPieces]; const unusedPieces = [...expandedPieces];
const sortedBarTypes = [...availableBars].sort((a, b) => b.length - a.length); // Ordenar barras por comprimento (maior para menor) para tentar usar as maiores primeiro?
// O código original não ordenava, seguia a ordem de inserção. Manteremos assim por enquanto.
for (let barType of sortedBarTypes) { for (let barType of availableBars) {
for (let barCopy = 0; barCopy < barType.qty; barCopy++) { for (let barCopy = 0; barCopy < barType.qty; barCopy++) {
if (unusedPieces.length === 0) break; if (unusedPieces.length === 0) break;
@@ -528,47 +458,48 @@ function optimizeCutting() {
isSimulated: barType.isSimulated || false isSimulated: barType.isSimulated || false
}; };
unusedPieces.sort((a, b) => b.length - a.length); // Tentar encaixar peças (FFD)
// Ordenar peças restantes por tamanho decrescente
const N = unusedPieces.length; const sorted = [...unusedPieces].sort((a, b) => b.length - a.length);
const capacity = bar.remaining;
const dp = new Int32Array(capacity + 1);
const keep = new Array(N);
for (let i = 0; i < N; i++) {
keep[i] = new Uint8Array(capacity + 1);
const w = unusedPieces[i].length + kerfSize;
for (let j = capacity; j >= 0; j--) {
if (j >= w && dp[j - w] + w > dp[j]) {
dp[j] = dp[j - w] + w;
keep[i][j] = 1;
}
}
}
let bestW = 0;
for(let j = 0; j <= capacity; j++) {
if(dp[j] > dp[bestW]) bestW = j;
}
let currW = bestW;
const toRemoveIds = []; const toRemoveIds = [];
for (let i = N - 1; i >= 0; i--) {
if (keep[i][currW] === 1) { for (let piece of sorted) {
const selectedPiece = unusedPieces[i]; // Check if piece fits considering kerf loss for this cut
bar.pieces.push(selectedPiece); // We assume each piece consumes its length + kerf
const w = selectedPiece.length + kerfSize; // Exception: The very last piece in a bar might not strictly need a kerf if it's the end,
bar.remaining -= w; // but usually in cutting processes, you cut the piece out, so kerf is consumed.
currW -= w; // User requirement: "consumo adicional de cada corte... sera de mais 2mm para cada corte"
toRemoveIds.push(selectedPiece.uniqueId);
const requiredSpace = piece.length + kerfSize;
// However, we need to be careful. If remaining is EXACTLY piece.length, can we cut it?
// If we cut it, we lose the kerf. So we need remaining >= piece.length + kerf?
// Or does the kerf come from the "waste"?
// Usually: Bar 6000. Piece 1000. Kerf 3.
// Cut 1: Consumes 1003. Remaining 4997.
// So yes, we treat piece length as (length + kerf).
if (bar.remaining >= requiredSpace) {
bar.pieces.push(piece);
bar.remaining -= requiredSpace;
toRemoveIds.push(piece.uniqueId);
} else if (bar.remaining >= piece.length && bar.remaining < requiredSpace) {
// Edge case: Fits exactly or with less than kerf remaining?
// If I have 1000mm remaining and need 1000mm piece.
// If I cut, I destroy 3mm. So I need 1003mm to get a 1000mm piece?
// Yes, usually. Unless it's the raw end of the bar, but we can't assume that.
// Let's stick to the rule: consumption = length + kerf.
// If bar.remaining < length + kerf, we can't cut it.
} }
} }
if (toRemoveIds.length > 0) { // Remover peças colocadas usando ID único
const removeSet = new Set(toRemoveIds); for (let uid of toRemoveIds) {
unusedPieces = unusedPieces.filter(p => !removeSet.has(p.uniqueId)); const idx = unusedPieces.findIndex(p => p.uniqueId === uid);
if (idx !== -1) unusedPieces.splice(idx, 1);
}
if (bar.pieces.length > 0) {
usedBars.push(bar); usedBars.push(bar);
} }
} }
@@ -1207,158 +1138,8 @@ window.calculateOptimization = calculateOptimization;
window.clearAll = clearAll; window.clearAll = clearAll;
window.exportHTML = exportHTML; window.exportHTML = exportHTML;
window.printReport = printReport; window.printReport = printReport;
window.printCompactReport = printCompactReport;
window.saveJob = saveJob; window.saveJob = saveJob;
function printCompactReport() {
const html = generateCompactReportHTML();
if (!html) {
showToast('Calcule a otimização primeiro', 'warning');
return;
}
const win = window.open('', '_blank');
win.document.write(html);
win.document.close();
setTimeout(() => {
win.print();
}, 500);
}
function generateCompactReportHTML() {
if (!lastResults) return null;
const { usedBars, totalWeight } = lastResults;
const dateStr = new Date().toLocaleDateString('pt-BR');
const timeStr = new Date().toLocaleTimeString('pt-BR');
const totalPieces = demandPieces.reduce((s, p) => s + p.qty, 0);
const jobTitle = document.getElementById('jobTitle').value.trim() || 'Relatório de Corte - Compacto';
const renderPieceGroups = (pieces) => {
const groups = {};
pieces.forEach(p => {
if (!groups[p.tag]) groups[p.tag] = 0;
groups[p.tag]++;
});
return Object.entries(groups).map(([tag, qt]) => `${tag} (${qt}x)`).join(', ');
};
let tableRows = usedBars.map((bar, idx) => {
const composition = renderPieceGroups(bar.pieces);
return `
<tr>
<td>${idx + 1}</td>
<td>${bar.barType}</td>
<td style="text-align: center;">${bar.count}</td>
<td style="text-align: right;">${bar.length} mm</td>
<td style="text-align: right; color: var(--danger);">${bar.remaining} mm</td>
<td>${composition}</td>
</tr>
`;
}).join('');
return `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Relatório Compacto - ${dateStr}</title>
<style>
:root { --primary: #1a6b8f; --danger: #e74c3c; --gray: #bdc3c7; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #555; font-family: 'Segoe UI', Arial, sans-serif; font-size: 10px; }
.page {
background: white;
width: 210mm;
min-height: 297mm;
margin: 20px auto;
padding: 10mm;
position: relative;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
}
@media print {
@page { size: A4 portrait; margin: 0; }
body { background: white; margin: 0; padding: 0; }
.page { margin: 0; box-shadow: none; border: none; min-height: 100vh; }
}
header {
border-bottom: 2px solid var(--primary);
padding-bottom: 10px;
margin-bottom: 10px;
color: var(--primary);
}
header h1 { font-size: 14pt; margin-bottom: 5px; }
header p { font-size: 8pt; color: #666; }
.content { flex: 1; }
.summary-box {
display: flex; justify-content: space-between;
background: #f4f4f4; padding: 8px; margin-bottom: 15px;
border: 1px solid #ddd; border-radius: 4px;
}
.summary-item { font-size: 9pt; }
.summary-item strong { display: block; font-size: 11pt; color: var(--primary); margin-top: 2px; }
table { width: 100%; border-collapse: collapse; font-size: 8pt; margin-bottom: 10px; }
th { background: var(--primary); color: white; text-align: left; padding: 6px; border: 1px solid #ddd; }
td { border: 1px solid #ddd; padding: 5px; }
tr:nth-child(even) { background: #f9f9f9; }
footer {
margin-top: 15px; border-top: 1px solid var(--gray);
padding-top: 10px; text-align: center; font-size: 8pt; color: #888;
}
</style>
</head>
<body>
<div class="page">
<header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h1>📊 ${jobTitle}</h1>
<p>Gerado em: ${dateStr} às ${timeStr}</p>
</div>
</div>
</header>
<div class="content">
<div class="summary-box">
<div class="summary-item">Total de Peças<strong>${totalPieces}</strong></div>
<div class="summary-item">Barras (Padrões)<strong>${usedBars.length}</strong></div>
<div class="summary-item">Peso Total (kg)<strong>${totalWeight.toFixed(2)}</strong></div>
<div class="summary-item">Aproveitamento Global<strong>${((1 - (lastResults.usedBars.reduce((s, b) => s + b.remaining, 0) / lastResults.usedBars.reduce((s, b) => s + b.length, 0))) * 100).toFixed(2)}%</strong></div>
</div>
<table>
<thead>
<tr>
<th style="width: 5%;">#</th>
<th style="width: 20%;">Perfil/Barra</th>
<th style="width: 8%; text-align: center;">Qtd</th>
<th style="width: 12%; text-align: right;">Comp. (mm)</th>
<th style="width: 12%; text-align: right;">Sobra (mm)</th>
<th style="width: 43%;">Composição (Tag x Qtd)</th>
</tr>
</thead>
<tbody>
${tableRows}
</tbody>
</table>
</div>
<footer>
Otimizador de Corte (Compacto) | ${totalPieces} Peças Processadas | Página 1 de 1
</footer>
</div>
</body>
</html>`;
}
window.loadJob = loadJob; window.loadJob = loadJob;
window.closeModal = closeModal; window.closeModal = closeModal;