( ′∀`)σ≡σ☆))Д′)レ(゚∀゚;)ヘ=З=З=Зε≡(ノ´_ゝ`)ノ HEX
HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux mail.thebrand.ai 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 13 19:51:50 UTC 2026 x86_64
User: www-data (33)
PHP: 8.3.6
Disabled: NONE
Upload Files
File: /var/www/html/tmpr/../tmpr/../tmpr/../tmpr/../tmpr/..//tmpr/../customui/index.php
<?php
// index.php - Simple PHP frontend to drive the n8n workflow in custom-ui.json
// Assumptions:
// - n8n is reachable at http://localhost:5678
// - The initial webhook path is "test" (from custom-ui.json)
// - The workflow returns a resumeUrl that we will POST a base64 image to
// - The resume response returns { summary, products, resumeUrl } to render

// Config (override via environment variables if needed)
$N8N_BASE_URL = getenv('N8N_BASE_URL') ?: 'https://thebrandai.app.n8n.cloud';
$WEBHOOK_PATH = getenv('N8N_WEBHOOK_PATH') ?: 'test';
$N8N_AUTH_TOKEN = getenv('N8N_AUTH_TOKEN') ?: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1ZDcwMDk1Mi1hNjhiLTRhMDMtODFkZC02NTY3OWRmOGE4YWEiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzYxNTA3MzY2LCJleHAiOjE3NjQwMTgwMDB9.w-J43tTtQz_qeTf7CCLIeH9cqsbLKjPRnLyMp0Opn-M';

// Robust AJAX error handling: ensure JSON is returned even on fatal errors
error_reporting(E_ALL);
ini_set('display_errors', '0');
register_shutdown_function(function(){
    $err = error_get_last();
    if ($err && isset($_POST['ajax'])) {
        if (!headers_sent()) {
            header('Content-Type: application/json');
            http_response_code(500);
        }
        echo json_encode([
            'success' => false,
            'messages' => ['Server error: ' . ($err['message'] ?? 'unknown') . ' at ' . ($err['file'] ?? '?') . ':' . ($err['line'] ?? '?')],
            'result' => null,
        ]);
    }
});

function http_post_json(string $url, array $payload, int $timeout = 30): array {
    // Build headers with optional Bearer token
    $headers = [
        'Content-Type: application/json',
        'Accept: application/json'
    ];
    global $N8N_AUTH_TOKEN;
    if (!empty($N8N_AUTH_TOKEN)) {
        $headers[] = 'Authorization: Bearer ' . $N8N_AUTH_TOKEN;
    }

    $ch = curl_init($url);
    $json = json_encode($payload);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => $json,
        CURLOPT_TIMEOUT => $timeout,
    ]);
    $response = curl_exec($ch);
    $errno = curl_errno($ch);
    $error = $errno ? curl_error($ch) : null;
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $ctype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);

    if ($errno) {
        return ['_error' => "cURL error: $error", '_status' => $status, '_content_type' => $ctype];
    }

    $trimmed = is_string($response) ? trim($response) : '';
    $decoded = null;
    if ($trimmed !== '') {
        $decoded = json_decode($trimmed, true);
    }

    if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
        // If non-JSON but 2xx, return raw for caller to interpret
        if ($status >= 200 && $status < 300) {
            return ['_status' => $status, '_content_type' => $ctype, '_raw' => $response];
        }
        return ['_error' => 'Invalid JSON response', '_status' => $status, '_content_type' => $ctype, '_raw' => $response];
    }

    if (!is_array($decoded)) {
        $decoded = ['_value' => $decoded];
    }
    $decoded['_status'] = $status;
    $decoded['_content_type'] = $ctype;
    return $decoded;
}

function http_post_multipart(string $url, array $fields, string $filePath, string $fileName, string $mime = 'application/octet-stream', int $timeout = 30): array {
    $headers = [
        'Accept: application/json'
    ];
    global $N8N_AUTH_TOKEN;
    if (!empty($N8N_AUTH_TOKEN)) {
        $headers[] = 'Authorization: Bearer ' . $N8N_AUTH_TOKEN;
    }

    $data = $fields;
    $data['file'] = new CURLFile($filePath, $mime, $fileName);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => $data,
        CURLOPT_TIMEOUT => $timeout,
    ]);
    $response = curl_exec($ch);
    $errno = curl_errno($ch);
    $error = $errno ? curl_error($ch) : null;
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $ctype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);

    if ($errno) {
        return ['_error' => "cURL error: $error", '_status' => $status, '_content_type' => $ctype];
    }

    $trimmed = is_string($response) ? trim($response) : '';
    $decoded = null;
    if ($trimmed !== '') {
        $decoded = json_decode($trimmed, true);
    }

    if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
        if ($status >= 200 && $status < 300) {
            return ['_status' => $status, '_content_type' => $ctype, '_raw' => $response];
        }
        return ['_error' => 'Invalid JSON response', '_status' => $status, '_content_type' => $ctype, '_raw' => $response];
    }

    if (!is_array($decoded)) {
        $decoded = ['_value' => $decoded];
    }
    $decoded['_status'] = $status;
    $decoded['_content_type'] = $ctype;
    return $decoded;
}

// Simple router for actions
$action = $_POST['action'] ?? '';
$messages = [];
$resultData = null;
// Detect AJAX requests via explicit flag
$isAjax = isset($_POST['ajax']);

if ($action === 'upload') {
    // Validate file
    if (!isset($_FILES['invoice']) || $_FILES['invoice']['error'] !== UPLOAD_ERR_OK) {
        $messages[] = 'Please choose an invoice image file.';
    } else {
        $fileTmp = $_FILES['invoice']['tmp_name'];
        $fileContents = @file_get_contents($fileTmp);
        // Ensure filename and mime are safe strings
        $fileName = (isset($_FILES['invoice']['name']) && $_FILES['invoice']['name']) ? (string)$_FILES['invoice']['name'] : 'invoice';
        $mime = 'application/octet-stream';
        if (function_exists('finfo_open')) {
            $fi = @finfo_open(FILEINFO_MIME_TYPE);
            if ($fi) {
                $detected = @finfo_file($fi, $fileTmp);
                if (is_string($detected) && $detected !== '') { $mime = $detected; }
                @finfo_close($fi);
            }
        }
        if ($mime === 'application/octet-stream' && isset($_FILES['invoice']['type']) && $_FILES['invoice']['type']) {
            $mime = (string)$_FILES['invoice']['type'];
        }
        if ($fileContents === false) {
            $messages[] = 'Unable to read uploaded file.';
        } else {
            $base64 = base64_encode($fileContents);

            // Step 1: Call initial webhook to get resumeUrl
            $startUrl = rtrim($N8N_BASE_URL, '/') . '/webhook/' . $WEBHOOK_PATH;
            $startResp = http_post_json($startUrl, [ 'start' => true ]);

            // If production webhook is not registered, try test webhook
            if ((int)($startResp['_status'] ?? 0) === 404) {
                $messages[] = 'Production webhook not active. Trying test webhook...';
                $startUrlTest = rtrim($N8N_BASE_URL, '/') . '/webhook-test/' . $WEBHOOK_PATH;
                $startRespTest = http_post_json($startUrlTest, [ 'start' => true ]);
                // Prefer test response if it has resumeUrl
                if (isset($startRespTest['resumeUrl'])) {
                    $startResp = $startRespTest;
                } else {
                    // Keep both for diagnostics
                    $messages[] = 'Test webhook also failed.';
                    $messages[] = 'Prod response: ' . htmlspecialchars(json_encode($startResp));
                    $messages[] = 'Test response: ' . htmlspecialchars(json_encode($startRespTest));
                }
            }

            if (!empty($startResp['_error'])) {
                $messages[] = 'Start webhook error: ' . $startResp['_error'];
            } elseif (!isset($startResp['resumeUrl'])) {
                $messages[] = 'Start webhook did not return resumeUrl.';
                $messages[] = 'Response: ' . htmlspecialchars(json_encode($startResp));
            } else {
                $resumeUrl = $startResp['resumeUrl'];
                $startResumeUrl = $resumeUrl;

                // Step 2: POST the image as base64 JSON to the resumeUrl (per n8n custom-ui.json)
                $resumeResp = http_post_json($resumeUrl, [
                    'file' => $base64,
                    'filename' => ($fileName ?: 'invoice'),
                    'mime' => ($mime ?: 'application/octet-stream')
                ]);

                // Treat HTTP errors explicitly
                if ((int)($resumeResp['_status'] ?? 0) >= 400) {
                    $messages[] = 'Resume webhook HTTP error ' . (int)$resumeResp['_status'];
                    if (!empty($resumeResp['_raw'])) {
                        $messages[] = 'Raw response: ' . htmlspecialchars(substr((string)$resumeResp['_raw'], 0, 500));
                    }
                } elseif (!empty($resumeResp['_error'])) {
                    $messages[] = 'Resume webhook error: ' . $resumeResp['_error'];
                } else {
                    // Attempt to extract summary/products from common n8n shapes
                    $summary = $resumeResp['summary'] ?? '';
                    $products = $resumeResp['products'] ?? '';
                    $finalResumeUrl = $resumeResp['resumeUrl'] ?? null;
                    $rawForUi = $resumeResp;

                    if (!$summary && !$products && isset($resumeResp['_raw']) && is_string($resumeResp['_raw'])) {
                        $maybe = json_decode(trim($resumeResp['_raw']), true);
                        if (is_array($maybe)) {
                            if (isset($maybe[0]['json'])) {
                                $j = $maybe[0]['json'];
                                $summary = $j['summary'] ?? $summary;
                                $products = $j['products'] ?? $products;
                                $finalResumeUrl = $j['resumeUrl'] ?? $finalResumeUrl;
                            } elseif (isset($maybe['json'])) {
                                $j = $maybe['json'];
                                $summary = $j['summary'] ?? $summary;
                                $products = $j['products'] ?? $products;
                                $finalResumeUrl = $j['resumeUrl'] ?? $finalResumeUrl;
                            }
                            $rawForUi = $maybe;
                        }
                    }

                    $resultData = [
                        'summary' => $summary,
                        'products' => $products,
                        'startResumeUrl' => $startResumeUrl ?? null,
                        'finalResumeUrl' => $finalResumeUrl,
                        'raw' => $rawForUi,
                    ];

                    if (empty($finalResumeUrl)) {
                        $messages[] = 'Warning: final resumeUrl missing in response.';
                        if (!empty($resumeResp['_raw'])) {
                            $messages[] = 'Note: showing raw response from resume webhook.';
                        }
                    }
                }
            }
        }
    }
} elseif ($action === 'finalize') {
    $finalResumeUrl = $_POST['finalResumeUrl'] ?? '';
    if (!$finalResumeUrl) {
        $messages[] = 'Missing final resumeUrl.';
    } else {
        $finalResp = http_post_json($finalResumeUrl, [ 'confirm' => true ]);
        if (!empty($finalResp['_error'])) {
            $messages[] = 'Finalize webhook error: ' . $finalResp['_error'];
            $resultData = [ 'finalizeRaw' => $finalResp ];
        } else {
            $resultData = [ 'finalizeRaw' => $finalResp ];
        }
    }
}

// If AJAX, return JSON response and exit before rendering HTML
if ($isAjax) {
    header('Content-Type: application/json');
    echo json_encode([
        'success' => empty($messages),
        'messages' => $messages,
        'result' => $resultData,
    ]);
    exit;
}
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Custom UI - Invoice Analyzer</title>
  <style>
    body { font-family: system-ui, Arial, sans-serif; margin: 2rem; color: #222; }
    header { margin-bottom: 1.5rem; }
    .card { border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
    .row { display: flex; gap: 1rem; }
    .row > .col { flex: 1; }
    label { display: block; margin-bottom: 0.5rem; }
    input[type=file] { margin-bottom: 1rem; }
    button { padding: 0.5rem 1rem; border: 1px solid #888; background: #f5f5f5; border-radius: 6px; cursor: pointer; }
    button:hover { background: #eee; }
    pre { background: #f9f9f9; padding: 1rem; border-radius: 6px; overflow: auto; }
    .messages { color: #b00020; }
    .meta { color: #555; font-size: 0.9rem; }
  </style>
</head>
<body>
  <header>
    <h1>Invoice Analyzer (n8n workflow)</h1>
    <p class="meta">n8n base: <?php echo htmlspecialchars($N8N_BASE_URL); ?>, webhook path: <?php echo htmlspecialchars($WEBHOOK_PATH); ?></p>
  </header>

  <div class="card">
    <h2>Upload Invoice Image</h2>
    <form id="uploadForm" method="post" enctype="multipart/form-data">
      <input type="hidden" name="action" value="upload">
      <label for="invoice">Choose invoice image (PNG/JPG/PDF as image):</label>
      <input type="file" name="invoice" id="invoice" accept="image/*,.png,.jpg,.jpeg">
      <div>
        <button id="analyzeBtn" type="submit">Analyze</button>
      </div>
    </form>
  </div>

  <div id="progressCard" class="card" style="display:none">
    <h2>Progress</h2>
    <p id="statusText"></p>
  </div>

  <div id="messagesCard" class="card messages" style="display:none">
    <h2>Messages</h2>
    <ul id="messagesList"></ul>
  </div>

  <div id="resultCard" class="card" style="display:none">
    <h2>Result</h2>
    <div class="row">
      <div class="col">
        <h3>Summary</h3>
        <p id="summaryText"></p>
      </div>
      <div class="col">
        <h3>Products</h3>
        <p id="productsText"></p>
      </div>
    </div>
    <div class="row">
      <div class="col">
        <h3>Start Resume URL</h3>
        <p id="startResumeUrlText"></p>
      </div>
      <div class="col">
        <h3>Final Resume URL</h3>
        <p id="finalResumeUrlText"></p>
      </div>
    </div>
    <form id="finalizeForm" style="display:none; margin-top: 1rem;">
      <button type="submit">Finalize</button>
    </form>
  </div>

  <div id="finalizeCard" class="card" style="display:none">
    <h2>Finalize Response</h2>
    <pre id="finalizeRaw"></pre>
  </div>

  <?php if (!empty($messages)): ?>
    <div class="card messages">
      <h2>Messages</h2>
      <ul>
        <?php foreach ($messages as $m): ?>
          <li><?php echo htmlspecialchars($m); ?></li>
        <?php endforeach; ?>
      </ul>
    </div>
  <?php endif; ?>

  <?php if ($resultData && isset($resultData['summary'])): ?>
    <div class="card">
      <h2>Result</h2>
      <div class="row">
        <div class="col">
          <h3>Summary</h3>
          <p><?php echo nl2br(htmlspecialchars($resultData['summary'])); ?></p>
        </div>
        <div class="col">
          <h3>Products</h3>
          <p><?php echo nl2br(htmlspecialchars($resultData['products'])); ?></p>
        </div>
      </div>

      <?php if (!empty($resultData['finalResumeUrl'])): ?>
        <form method="post" style="margin-top: 1rem;">
          <input type="hidden" name="action" value="finalize">
          <input type="hidden" name="finalResumeUrl" value="<?php echo htmlspecialchars($resultData['finalResumeUrl']); ?>">
          <button type="submit">Finalize</button>
        </form>
      <?php endif; ?>

      <details style="margin-top: 1rem;">
        <summary>Raw response</summary>
        <pre><?php echo htmlspecialchars(json_encode($resultData['raw'], JSON_PRETTY_PRINT)); ?></pre>
      </details>
    </div>
  <?php endif; ?>

  <?php if ($resultData && isset($resultData['finalizeRaw'])): ?>
    <div class="card">
      <h2>Finalize Response</h2>
      <pre><?php echo htmlspecialchars(json_encode($resultData['finalizeRaw'], JSON_PRETTY_PRINT)); ?></pre>
    </div>
  <?php endif; ?>

  <footer class="meta">
    <p>Tip: If your n8n instance is not running on localhost:5678, set N8N_BASE_URL in your environment or update the config variables at the top of this file.</p>
  </footer>
</body>
</html>

<script>
  (function(){
    function initCore(){
      const uploadForm = document.getElementById('uploadForm');
      const messagesCard = document.getElementById('messagesCard');
      const messagesList = document.getElementById('messagesList');
      const progressCard = document.getElementById('progressCard');
      const statusText = document.getElementById('statusText');
      const resultCard = document.getElementById('resultCard');
      const summaryText = document.getElementById('summaryText');
      const productsText = document.getElementById('productsText');
      const startResumeUrlText = document.getElementById('startResumeUrlText');
      const finalResumeUrlText = document.getElementById('finalResumeUrlText');
      const finalizeForm = document.getElementById('finalizeForm');
      const finalizeCard = document.getElementById('finalizeCard');
      const finalizeRaw = document.getElementById('finalizeRaw');
      let finalResumeUrl = null;

      function setLink(el, url){
        if (!el) return;
        if (url) {
          const safe = document.createElement('a');
          safe.href = url;
          safe.target = '_blank';
          safe.rel = 'noopener';
          safe.textContent = url;
          el.innerHTML = '';
          el.appendChild(safe);
        } else {
          el.textContent = '';
        }
      }

      function showMessages(msgs){
        if (!messagesCard || !messagesList) return;
        messagesList.innerHTML = '';
        (msgs || []).forEach(m => {
          const li = document.createElement('li');
          li.textContent = m;
          messagesList.appendChild(li);
        });
        messagesCard.style.display = (msgs && msgs.length) ? 'block' : 'none';
      }
      function showProgress(text){
        if (!progressCard || !statusText) return;
        statusText.textContent = text || '';
        progressCard.style.display = text ? 'block' : 'none';
      }
      function clearUI(){
        showMessages([]);
        showProgress('');
        if (resultCard) resultCard.style.display = 'none';
        if (finalizeCard) finalizeCard.style.display = 'none';
        if (finalizeForm) finalizeForm.style.display = 'none';
        setLink(startResumeUrlText, '');
        setLink(finalResumeUrlText, '');
      }

      if (uploadForm) {
        uploadForm.addEventListener('submit', async function(e){
          e.preventDefault();
          clearUI();
          showProgress('Starting webhook...');
          const fd = new FormData(uploadForm);
          fd.set('action','upload');
          fd.set('ajax','1');
          try {
            const res = await fetch('index.php', { method:'POST', body: fd, headers: { 'Accept': 'application/json' } });
            showProgress('Waiting for analysis...');
            const status = res.status;
            const text = await res.text();
            let data;
            try {
              data = text ? JSON.parse(text) : null;
            } catch(parseErr) {
              showProgress('');
              showMessages([`HTTP ${status}: invalid JSON`, text ? `Raw: ${text.slice(0,500)}` : 'Empty response']);
              return;
            }
            showProgress('');
            if (!data) {
              showMessages([`HTTP ${status}: Empty response`]);
              return;
            }
            showMessages(data.messages || []);
            if (data.result) {
              if (summaryText) summaryText.textContent = data.result.summary || '';
              if (productsText) productsText.textContent = data.result.products || '';
              setLink(startResumeUrlText, data.result.startResumeUrl || '');
              setLink(finalResumeUrlText, data.result.finalResumeUrl || '');
              if (resultCard) resultCard.style.display = 'block';
              finalResumeUrl = data.result.finalResumeUrl || null;
              if (finalizeForm) finalizeForm.style.display = finalResumeUrl ? 'block' : 'none';
            }
          } catch(err){
            showProgress('');
            showMessages(['Network error: ' + err]);
          }
        });
      }

      if (finalizeForm) {
        finalizeForm.addEventListener('submit', async function(e){
          e.preventDefault();
          if (!finalResumeUrl) {
            showMessages(['Missing final resume URL.']);
            return;
          }
          showProgress('Finalizing...');
          const fd = new FormData();
          fd.set('action','finalize');
          fd.set('ajax','1');
          fd.set('finalResumeUrl', finalResumeUrl);
          try {
            const res = await fetch('index.php', { method:'POST', body: fd, headers: { 'Accept': 'application/json' } });
            const status = res.status;
            const text = await res.text();
            let data;
            try {
              data = text ? JSON.parse(text) : null;
            } catch(parseErr) {
              showProgress('');
              showMessages([`HTTP ${status}: invalid JSON`, text ? `Raw: ${text.slice(0,500)}` : 'Empty response']);
              return;
            }
            showProgress('');
            showMessages(data.messages || []);
            if (finalizeCard && finalizeRaw) {
              finalizeCard.style.display = 'block';
              finalizeRaw.textContent = JSON.stringify((data.result && data.result.finalizeRaw) ? data.result.finalizeRaw : data, null, 2);
            }
          } catch(err){
            showProgress('');
            showMessages(['Network error: ' + err]);
          }
        });
      }
    }
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', initCore);
    } else {
      initCore();
    }
  })();
</script>