From a27889b8ae0067dfe461b96579d62135734f53b5 Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Fri, 23 Oct 2026 17:42:47 +0200 Subject: [PATCH 1/2] fix: feedback page lists every repeated error, no count badge on the tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The widget folds identical consecutive console entries and failed requests into one with a count; the page now lists every occurrence (at most 500 per log), each with its own copy button and in "Copy all" and "Copy report", instead of one row with ×N. - Technical tab without the error/entry count badge. Fixes #44 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- .../Controllers/FeedbackPageController.php | 24 ++++++++++++------- resources/views/feedback/show.blade.php | 14 ++++------- tests/Feature/FeedbackLinkTest.php | 10 +++++--- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae930c..0fc0fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to the API and the widget are documented here. The format fo ### Added -- Feedback page redesign: screenshot first with the sender's message as a sticky, hideable card; technical details on a second tab with copy buttons for every value and log entry and "Copy report" as plain text; console errors highlighted and counted on the tab. +- Feedback page redesign: screenshot first with the sender's message as a sticky, hideable card; technical details on a second tab with copy buttons for every value and log entry and "Copy report" as plain text; console errors highlighted. Repeated console entries and failed requests are listed one by one (at most 500 per log), each with its own copy button and in "Copy all", instead of a single row with ×N; no count badge on the tab. - Feedback pages behind signed, expiring links with all details, console and network log and the screenshot (inline and download). - `feedback:link {id}` creates team links that can complete and reopen the entry (`--by` is logged, `--read-only`, `--expires`). - The API returns a read-only share link after sending (`FEEDBACK_SHARE_LINK_MINUTES`, `0` turns it off); the widget shows it on the thank you screen with a copy button, `after_send` receives it as `url`. diff --git a/app/Http/Controllers/FeedbackPageController.php b/app/Http/Controllers/FeedbackPageController.php index b6069f3..97b2942 100644 --- a/app/Http/Controllers/FeedbackPageController.php +++ b/app/Http/Controllers/FeedbackPageController.php @@ -19,6 +19,9 @@ */ final class FeedbackPageController { + /** Upper bound of rows per log once repeated entries are spread out. */ + private const MAX_ROWS = 500; + public function __invoke(Request $request, FeedbackEntry $entry): Response { $expires = Carbon::createFromTimestamp($request->integer('expires')); @@ -27,8 +30,8 @@ public function __invoke(Request $request, FeedbackEntry $entry): Response $shot = $entry->screenshot()->select(['entry_id', 'mime', 'width', 'height', 'bytes'])->first(); $nonce = Str::random(32); $environment = self::environment($entry); - $console = self::rows($entry->console_log, ['type', 'message', 'count', 'source', 'line', 'column', 'stack']); - $network = self::rows($entry->network_log, ['method', 'url', 'status', 'duration', 'count']); + $console = self::rows($entry->console_log, ['type', 'message', 'source', 'line', 'column', 'stack']); + $network = self::rows($entry->network_log, ['method', 'url', 'status', 'duration']); $customData = $entry->custom_data === null || $entry->custom_data === [] ? null : (string) json_encode($entry->custom_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); return response()->view('feedback.show', [ @@ -133,21 +136,20 @@ private static function report(FeedbackEntry $entry, array $environment, array $ public static function consoleLine(array $row): string { $where = $row['source'] === '' ? '' : ' ('.$row['source'].($row['line'] === '' ? '' : ':'.$row['line']).($row['column'] === '' ? '' : ':'.$row['column']).')'; - $times = (int) $row['count'] > 1 ? " ×{$row['count']}" : ''; - return "[{$row['type']}] {$row['message']}{$where}{$times}".($row['stack'] === '' ? '' : "\n{$row['stack']}"); + return "[{$row['type']}] {$row['message']}{$where}".($row['stack'] === '' ? '' : "\n{$row['stack']}"); } /** @param array $row */ public static function networkLine(array $row): string { - $times = (int) $row['count'] > 1 ? " ×{$row['count']}" : ''; - - return trim("{$row['method']} {$row['url']} → {$row['status']}".($row['duration'] === '' ? '' : " ({$row['duration']} ms)").$times); + return trim("{$row['method']} {$row['url']} → {$row['status']}".($row['duration'] === '' ? '' : " ({$row['duration']} ms)")); } /** - * Logs come from the browser: every cell becomes a plain string. + * Logs come from the browser: every cell becomes a plain string. The + * widget folds identical consecutive entries into one with a count, the + * page lists every occurrence (at most MAX_ROWS per log). * * @param array|null $log * @param list $keys @@ -169,7 +171,11 @@ private static function rows(?array $log, array $keys): array $row[$key] = is_scalar($value) ? (string) $value : ''; } - $rows[] = $row; + $times = max(1, is_numeric($item['count'] ?? null) ? (int) $item['count'] : 1); + + for ($i = 0; $i < $times && count($rows) < self::MAX_ROWS; $i++) { + $rows[] = $row; + } } return $rows; diff --git a/resources/views/feedback/show.blade.php b/resources/views/feedback/show.blade.php index e8ebb5a..9b82d36 100644 --- a/resources/views/feedback/show.blade.php +++ b/resources/views/feedback/show.blade.php @@ -47,7 +47,6 @@ [role=tab][aria-selected=true] { color: var(--ink); } [role=tab][aria-selected=true]::after { content: ""; position: absolute; left: 10px; right: 10px; bottom: -1px; height: 2px; border-radius: 2px; background: var(--ink); } .count { font-size: 12px; padding: 1px 7px; border-radius: 999px; background: var(--sunken); color: var(--mute); } - .count.err { background: var(--err-bg); color: var(--err); } main { max-width: 1400px; margin: 0 auto; padding: 20px; } [role=tabpanel][hidden] { display: none; } @@ -179,11 +178,6 @@ @@ -268,8 +262,8 @@
  • {{ $row['type'] !== '' ? $row['type'] : 'log' }}
    {{ $row['message'] }} - @if ($row['source'] !== '' || (int) $row['count'] > 1) - + @if ($row['source'] !== '') + @endif @if ($row['stack'] !== '')
    Stack trace
    {{ $row['stack'] }}
    @@ -297,7 +291,9 @@
  • {{ $row['status'] !== '' && $row['status'] !== '0' ? $row['status'] : 'ERR' }}
    {{ $row['method'] }} {{ $row['url'] }} - + @if ($row['duration'] !== '') + + @endif
  • diff --git a/tests/Feature/FeedbackLinkTest.php b/tests/Feature/FeedbackLinkTest.php index b8c1912..f313c5d 100644 --- a/tests/Feature/FeedbackLinkTest.php +++ b/tests/Feature/FeedbackLinkTest.php @@ -94,9 +94,13 @@ function entryWithShot(FeedbackProject $project, array $attributes = []): Feedba ->assertSee('https://www.beispiel.ch/zwei') ->assertSee('
    Viewport
    ', false) ->assertSee('role="tab" id="tab-technical"', false) - ->assertSee('1 error') + ->assertDontSee('', false) + ->assertSee('2 console errors') + ->assertSee('Console 2', false) ->assertSee('Copy report') - ->assertSee('data-copy="[error] Boom ×2"', false) + ->assertSee('data-copy="[error] Boom"', false) + ->assertSee('data-copy="[error] Boom'."\n".'[error] Boom"', false) + ->assertDontSee('×2') ->assertSee('POST https://api.beispiel.ch/save → 500 (120 ms)') ->assertSee('1541 × 947') ->assertSee('Europe/Zurich') @@ -119,7 +123,7 @@ function entryWithShot(FeedbackProject $project, array $attributes = []): Feedba ->toContain("Feedback {$entry->id} (Alpha)") ->toContain('Die Schaltfläche «Speichern» reagiert nicht.') ->toContain('Viewport: 1541 × 947') - ->toContain("Console:\n[error] Boom ×2") + ->toContain("Console:\n[error] Boom\n[error] Boom\n") ->toContain("Network:\nPOST https://api.beispiel.ch/save → 500 (120 ms)") ->toContain('"plan": "pro"'); }); From 29a740a0dd748c334f4b12f0e3a7921a73002700 Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Fri, 23 Oct 2026 17:44:22 +0200 Subject: [PATCH 2/2] test: browser test expects every repeated console error counted Co-Authored-By: Claude Opus 5.5 --- tests/Browser/FeedbackPageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Browser/FeedbackPageTest.php b/tests/Browser/FeedbackPageTest.php index ac6b695..bf9ddf2 100644 --- a/tests/Browser/FeedbackPageTest.php +++ b/tests/Browser/FeedbackPageTest.php @@ -35,7 +35,7 @@ function sentFeedback(FeedbackProject $project): FeedbackEntry ->assertSee('Die Schaltfläche «Speichern» reagiert nicht.') ->assertSee('Anna Muster') ->assertSee('anna@beispiel.ch') - ->assertSee('1 console error') + ->assertSee('2 console errors') ->assertVisible('img[alt^="Screenshot of"]') ->assertNoJavaScriptErrors();