aboutsummaryrefslogtreecommitdiff
path: root/api/debug
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-18 17:01:52 +0200
committerAgando <roduccomm@gmail.com>2026-10-18 17:01:52 +0200
commitd7a1198aa7ac77cbe9d3f0c844245dd08a8ccd12 (patch)
tree0e6851e95a62d05f51966fe29eb784108df0fe4b /api/debug
parent824d6cd3c61488c120c0226d1929324f2741fafc (diff)
newOS4 v4.6.37: bug-report pipeline (report.php, bug command, HTML generator, telemetry-loop fix)
Diffstat (limited to 'api/debug')
-rw-r--r--api/debug/dashboard.php2
-rw-r--r--api/debug/ingest.php10
-rw-r--r--api/debug/report.php99
3 files changed, 108 insertions, 3 deletions
diff --git a/api/debug/dashboard.php b/api/debug/dashboard.php
index 7a46661..c235ec0 100644
--- a/api/debug/dashboard.php
+++ b/api/debug/dashboard.php
@@ -151,7 +151,7 @@ if ($lastSync): ?>
<td><?= $i++ ?></td>
<td class="max-w-300"><a href="error.php?hash=<?= urlencode($e['hash'] ?? '') ?>" style="color:var(--text);text-decoration:none;" title="View details"><?= htmlspecialchars(substr($e['message'] ?? '', 0, 80)) ?></a></td>
<td><span class="tag"><?= htmlspecialchars($e['type'] ?? '?') ?></span></td>
- <td><?= $e['count'] ?></td>
+ <td><?= (int)($e['count'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($topErrors)): ?>
diff --git a/api/debug/ingest.php b/api/debug/ingest.php
index 363dd09..b513593 100644
--- a/api/debug/ingest.php
+++ b/api/debug/ingest.php
@@ -17,6 +17,12 @@ $errors = $payload['errors'];
$session = $payload['session'] ?? [];
$clientVersion = $payload['version'] ?? 'unknown';
+// Client-supplied counts are untrusted: clamp so a bad client cannot store 38,495× rows.
+function clamp_count($v) {
+ $n = (int)$v;
+ return $n < 1 ? 1 : ($n > 1000 ? 1000 : $n);
+}
+
// Validate batch size
if (count($errors) > MAX_ERRORS_PER_BATCH) {
$errors = array_slice($errors, 0, MAX_ERRORS_PER_BATCH);
@@ -65,7 +71,7 @@ foreach ($errors as $e) {
if (isset($existingHashes[$hash])) {
// Update existing entry
$idx = $existingHashes[$hash];
- $allErrors[$idx]['count'] += $e['count'] ?? 1;
+ $allErrors[$idx]['count'] += clamp_count($e['count'] ?? 1);
$allErrors[$idx]['last_seen'] = $e['lastSeen'] ?? $now;
$allErrors[$idx]['ingested_at'] = $now;
if (!empty($e['stack'])) {
@@ -85,7 +91,7 @@ foreach ($errors as $e) {
'module' => $e['module'] ?? null,
'stack' => isset($e['stack']) ? substr($e['stack'], 0, 2000) : null,
'session_id' => $sessionId,
- 'count' => $e['count'] ?? 1,
+ 'count' => clamp_count($e['count'] ?? 1),
'first_seen' => $e['firstSeen'] ?? $now,
'last_seen' => $e['lastSeen'] ?? $now,
'ingested_at' => $now,
diff --git a/api/debug/report.php b/api/debug/report.php
new file mode 100644
index 0000000..a4cf9f1
--- /dev/null
+++ b/api/debug/report.php
@@ -0,0 +1,99 @@
+<?php
+// api/debug/report.php — structured bug-report intake (Part A)
+// POST { title, body?, diagnostics{...} } -> { success, id, duplicate, url }
+require_once __DIR__ . '/config.php';
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ error_response('Method not allowed', 405);
+}
+
+define('REPORTS_DIR', __DIR__ . '/../../data/reports');
+define('RATE_FILE', DEBUG_DATA_DIR . '/report_rate.json');
+define('MAX_REPORT_KB', 512);
+define('RATE_MAX', 5); // reports per IP …
+define('RATE_WINDOW', 600); // … per 10 minutes
+
+$raw = file_get_contents('php://input');
+if (!$raw) error_response('Empty request body');
+if (strlen($raw) > MAX_REPORT_KB * 1024) {
+ error_response('Payload too large (max ' . MAX_REPORT_KB . 'KB)', 413);
+}
+
+$payload = json_decode($raw, true);
+if (!is_array($payload)) error_response('Invalid JSON');
+
+$title = trim((string)($payload['title'] ?? ''));
+if ($title === '') error_response('title is required');
+if (mb_strlen($title) > 160) $title = mb_substr($title, 0, 160);
+$body = (string)($payload['body'] ?? '');
+if (mb_strlen($body) > 4000) $body = mb_substr($body, 0, 4000);
+$diag = (isset($payload['diagnostics']) && is_array($payload['diagnostics'])) ? $payload['diagnostics'] : [];
+
+// Storage guard: data/ is not writable by php-fpm, so never pretend we wrote a report.
+if (!is_dir(REPORTS_DIR)) @mkdir(REPORTS_DIR, 0775, true);
+if (!is_dir(REPORTS_DIR) || !is_writable(REPORTS_DIR)) {
+ error_response('Report storage unavailable', 503);
+}
+
+// Per-IP rate limit
+$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
+$now = time();
+$rate = load_json(RATE_FILE);
+$hits = array_values(array_filter($rate[$ip] ?? [], function ($t) use ($now) {
+ return $t > $now - RATE_WINDOW;
+}));
+if (count($hits) >= RATE_MAX) {
+ header('Retry-After: ' . RATE_WINDOW);
+ error_response('Rate limit exceeded (max ' . RATE_MAX . ' reports per 10 minutes)', 429);
+}
+$hits[] = $now;
+$rate[$ip] = $hits;
+foreach ($rate as $k => $v) {
+ $v = array_values(array_filter($v, function ($t) use ($now) { return $t > $now - 86400; }));
+ if ($v) $rate[$k] = $v; else unset($rate[$k]);
+}
+save_json(RATE_FILE, $rate);
+
+// Dedupe: same title + error set + version within 24h collapses onto the existing report.
+$fingerprint = sha1($title . '|' . json_encode($diag['errors'] ?? []) . '|' . (string)($diag['version'] ?? ''));
+$existing = glob(REPORTS_DIR . '/RPT-*.json') ?: [];
+foreach (array_slice(array_reverse($existing), 0, 100) as $f) {
+ $d = json_decode(@file_get_contents($f), true);
+ if (!is_array($d)) continue;
+ if (($d['fingerprint'] ?? '') === $fingerprint && strtotime($d['created_at'] ?? '') > $now - 86400) {
+ json_response([
+ 'success' => true,
+ 'id' => $d['id'],
+ 'duplicate' => true,
+ 'url' => '/spaces/newos4/downloads/bugs/BUG-REPORT.html',
+ ]);
+ }
+}
+
+$id = 'RPT-' . gmdate('Ymd-His') . '-' . substr(bin2hex(random_bytes(4)), 0, 8);
+$record = [
+ 'id' => $id,
+ 'created_at' => gmdate('c'),
+ 'fingerprint' => $fingerprint,
+ 'title' => $title,
+ 'body' => $body,
+ 'diagnostics' => $diag,
+ 'meta' => [
+ 'ip' => $ip,
+ 'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
+ ],
+];
+
+$file = REPORTS_DIR . '/' . $id . '.json';
+$json = json_encode($record, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+if ($json === false || file_put_contents($file, $json) === false) {
+ error_response('Failed to persist report', 500);
+}
+@chmod($file, 0664);
+
+json_response([
+ 'success' => true,
+ 'id' => $id,
+ 'duplicate' => false,
+ 'url' => '/spaces/newos4/downloads/bugs/BUG-REPORT.html',
+], 201);