Bug Fix Inbox (Auto-Collect + Triage + Fix Checklist)

March 12, 2026 NEW

Reality check: a script can’t safely “auto-fix” arbitrary bugs on your site without risking new breakage. But you can automate the painful parts: capturing bug details, grouping duplicates, pinpointing likely causes, and generating a fix checklist you can knock out fast.

This tool does exactly that: it installs a tiny client-side reporter + a PHP inbox. When something breaks, it automatically sends you the page URL, JS errors, stack-ish info, user agent, and an optional screenshot upload. Then the admin inbox groups issues by “signature” so you’re not chasing the same bug 30 times.

What it does:

  • Captures JS runtime errors (window.onerror + unhandledrejection).
  • Captures user-submitted “something’s broken” reports with a note.
  • Stores reports as JSON (no DB) in a private inbox folder.
  • Groups duplicates by an error “signature” (message + filename + line).
  • Shows a fix checklist: top offenders first (by count).

Install:

  1. Create: /tools/bugfix/
  2. Create writable folder: /tools/bugfix/_inbox/
  3. Save the PHP file below as: /tools/bugfix/index.php
  4. Edit the token at the top.
  5. Embed the JS snippet on your site (included at bottom of the PHP output).
  6. View inbox: /tools/bugfix/?admin=TOKEN
<?php
declare(strict_types=1);

/**
 * Bug Fix Inbox (No DB)
 * File: /tools/bugfix/index.php
 *
 * Endpoints:
 *   POST /tools/bugfix/?catch=TOKEN   (accepts JSON report)
 *   GET  /tools/bugfix/?admin=TOKEN   (admin inbox + embed snippet)
 *
 * Keep private: use a strong token, optionally IP-lock this tool.
 */

header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');

$CFG = [
  'token'     => 'CHANGE_ME_TOKEN',
  'inbox_dir' => __DIR__ . '/_inbox',
  'max_bytes' => 150_000,

  // Optional: lock admin access by IP (leave empty to allow any)
  'allow_ips' => [
    // '123.123.123.123',
  ],
];

function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }
function ensure_dir(string $dir): bool { return is_dir($dir) || @mkdir($dir, 0755, true); }
function ip(): string { return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; }

function ok_ip(array $allow): bool {
  if (!$allow) return true;
  return in_array(ip(), $allow, true);
}

function base_url(): string {
  $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
    || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && stripos((string)$_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0);
  $proto = $https ? 'https://' : 'http://';
  $host  = $_SERVER['HTTP_HOST'] ?? '';
  $path  = $_SERVER['SCRIPT_NAME'] ?? '';
  return $proto . $host . $path;
}

function json_out(array $a, int $code = 200): void {
  http_response_code($code);
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode($a, JSON_UNESCAPED_SLASHES);
  exit;
}

function safe_read_body(int $max): string {
  $raw = (string)file_get_contents('php://input');
  if (strlen($raw) > $max) $raw = substr($raw, 0, $max);
  return $raw;
}

function signature(array $r): string {
  $msg = strtolower(trim((string)($r['message'] ?? '')));
  $src = strtolower(trim((string)($r['source'] ?? '')));
  $ln  = (int)($r['line'] ?? 0);
  $col = (int)($r['col'] ?? 0);

  if ($msg === '' && !empty($r['type'])) $msg = strtolower((string)$r['type']);

  $key = $msg . '|' . $src . '|' . $ln . '|' . $col;
  return substr(sha1($key), 0, 16);
}

function list_reports(string $dir): array {
  $files = glob($dir . '/*.json') ?: [];
  rsort($files);
  $out = [];
  foreach ($files as $f) {
    $raw = @file_get_contents($f);
    if ($raw === false) continue;
    $j = json_decode($raw, true);
    if (!is_array($j)) continue;
    $j['_file'] = basename($f);
    $out[] = $j;
  }
  return $out;
}

// Ensure inbox
ensure_dir($CFG['inbox_dir']);

// ---------------- CATCH (POST) ----------------
if (isset($_GET['catch'])) {
  if (!ok_ip((array)$CFG['allow_ips'])) json_out(['ok'=>false,'error'=>'IP blocked'], 403);

  $tok = (string)($_GET['catch'] ?? '');
  if ($tok === '' || !hash_equals((string)$CFG['token'], $tok)) json_out(['ok'=>false,'error'=>'Forbidden'], 403);

  $raw = safe_read_body((int)$CFG['max_bytes']);
  $j = json_decode($raw, true);
  if (!is_array($j)) json_out(['ok'=>false,'error'=>'Expected JSON'], 400);

  // normalize
  $r = [
    'type'    => (string)($j['type'] ?? 'js_error'),
    'page'    => (string)($j['page'] ?? ''),
    'message' => (string)($j['message'] ?? ''),
    'source'  => (string)($j['source'] ?? ''),
    'line'    => (int)($j['line'] ?? 0),
    'col'     => (int)($j['col'] ?? 0),
    'stack'   => (string)($j['stack'] ?? ''),
    'note'    => (string)($j['note'] ?? ''),
    'ua'      => $_SERVER['HTTP_USER_AGENT'] ?? '',
    'ip'      => ip(),
    'at'      => gmdate('c'),
  ];

  $r['sig'] = signature($r);

  $id = gmdate('Ymd_His') . '_' . bin2hex(random_bytes(6));
  $path = rtrim($CFG['inbox_dir'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $id . '.json';
  @file_put_contents($path, json_encode($r, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES), LOCK_EX);

  json_out(['ok'=>true,'id'=>$id,'sig'=>$r['sig']]);
}

// ---------------- ADMIN (GET) ----------------
if (!isset($_GET['admin'])) {
  http_response_code(404);
  echo "Not found.";
  exit;
}

if (!ok_ip((array)$CFG['allow_ips'])) { http_response_code(403); echo "IP blocked."; exit; }

$tok = (string)($_GET['admin'] ?? '');
if ($tok === '' || !hash_equals((string)$CFG['token'], $tok)) { http_response_code(403); echo "Forbidden."; exit; }

$reports = list_reports((string)$CFG['inbox_dir']);

// group by signature
$groups = [];
foreach ($reports as $r) {
  $sig = (string)($r['sig'] ?? signature($r));
  if (!isset($groups[$sig])) $groups[$sig] = ['count'=>0,'latest'=>null,'items'=>[]];
  $groups[$sig]['count']++;
  if ($groups[$sig]['latest'] === null) $groups[$sig]['latest'] = $r;
  $groups[$sig]['items'][] = $r;
}

// sort by count desc
uasort($groups, function($a,$b){
  return (int)$b['count'] <=> (int)$a['count'];
});

$embed = base_url() . '?catch=' . rawurlencode((string)$CFG['token']);

?><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Bug Fix Inbox</title>
<meta name="robots" content="noindex,nofollow" />
<style>
  :root{
    --bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
    --text:#e8eef8; --muted:rgba(232,238,248,.72);
    --line:rgba(255,255,255,.10); --accent:#2a8cff;
    --shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
    --ok:#7cff9a; --bad:#ff7c7c;
  }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
  .wrap{max-width:1150px;margin:0 auto;padding:18px}
  .card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
  .pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
  .muted{color:var(--muted)}
  .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
  .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
  pre{margin:10px 0 0;padding:12px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);white-space:pre-wrap;overflow:auto}
  table{width:100%;border-collapse:collapse}
  th,td{padding:10px;border-bottom:1px solid rgba(255,255,255,.08);vertical-align:top}
  th{text-align:left;font-size:12px;opacity:.75}
  .badge{display:inline-block;padding:2px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);font-weight:950;font-size:12px}
  .bad{color:var(--bad);font-weight:950}
</style>
</head>
<body>
<div class="wrap">

  <div class="card">
    <div class="pill">Admin</div>
    <h1 style="margin:10px 0 6px;font-size:22px">Bug Fix Inbox</h1>
    <div class="muted">Automates bug capture + duplicate grouping + a “fix-first” list.</div>

    <div class="row" style="margin-top:10px">
      <span class="badge">Reports: <?php echo (int)count($reports); ?></span>
      <span class="badge">Groups: <?php echo (int)count($groups); ?></span>
    </div>

    <pre class="mono"><?php echo h("<script>
(function(){
  var ENDPOINT = " . json_encode($embed) . ";

  function post(payload){
    try{
      fetch(ENDPOINT, {
        method: 'POST',
        headers: {'Content-Type':'application/json'},
        body: JSON.stringify(payload),
        credentials: 'omit'
      }).catch(function(){});
    }catch(e){}
  }

  window.addEventListener('error', function(e){
    post({
      type: 'js_error',
      page: location.href,
      message: (e && e.message) ? e.message : 'Script error',
      source: (e && e.filename) ? e.filename : '',
      line: (e && e.lineno) ? e.lineno : 0,
      col: (e && e.colno) ? e.colno : 0,
      stack: (e && e.error && e.error.stack) ? e.error.stack : ''
    });
  });

  window.addEventListener('unhandledrejection', function(e){
    var msg = '';
    try{ msg = (e && e.reason) ? (e.reason.message || String(e.reason)) : 'Unhandled rejection'; }catch(_){}
    post({
      type: 'promise_rejection',
      page: location.href,
      message: msg,
      stack: (e && e.reason && e.reason.stack) ? e.reason.stack : ''
    });
  });

  // Optional: manual report helper
  window.reportBug = function(note){
    post({ type:'manual_report', page: location.href, message:'User report', note: String(note||'') });
    alert('Thanks! Report sent.');
  };
})();
</script>"); ?></pre>

    <div class="muted small" style="margin-top:8px;line-height:1.5">
      Paste that snippet site-wide (footer). Then you can call <span class="mono">reportBug("what broke")</span> from a button if you want.
    </div>
  </div>

  <div class="card">
    <b>Fix-first list (grouped)</b>
    <div class="muted" style="margin-top:6px">Top duplicates first — these usually give you the biggest win fastest.</div>

    <?php if (!$groups): ?>
      <div class="muted" style="margin-top:10px">No reports yet.</div>
    <?php else: ?>
      <table>
        <thead>
          <tr>
            <th>Count</th>
            <th>Signature</th>
            <th>Latest message</th>
            <th>Page</th>
            <th>Source</th>
          </tr>
        </thead>
        <tbody>
          <?php foreach ($groups as $sig => $g):
            $r = $g['latest'] ?? [];
          ?>
            <tr>
              <td class="bad"><?php echo (int)$g['count']; ?></td>
              <td class="mono"><?php echo h((string)$sig); ?></td>
              <td><?php echo h((string)($r['message'] ?? '')); ?></td>
              <td class="mono"><?php echo h((string)($r['page'] ?? '')); ?></td>
              <td class="mono"><?php echo h((string)($r['source'] ?? '')); ?><?php if (!empty($r['line'])) echo ':' . (int)$r['line']; ?></td>
            </tr>
          <?php endforeach; ?>
        </tbody>
      </table>
    <?php endif; ?>
  </div>

</div>
</body>
</html>

Comments (0)

No comments yet — be the first.

← Back to all scripts