Bug Fix Inbox (Auto-Collect + Triage + Fix Checklist)
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:
- Create:
/tools/bugfix/ - Create writable folder:
/tools/bugfix/_inbox/ - Save the PHP file below as:
/tools/bugfix/index.php - Edit the token at the top.
- Embed the JS snippet on your site (included at bottom of the PHP output).
- 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.