1
0
Эх сурвалжийг харах

feat(ui): show recent reports table on reporter edit page

Adds a "Recent reports" section at the bottom of the reporter edit
page showing the 10 most recent audit log entries scoped to this
reporter (entity_type=reporter, entity_id=<id>). Each row can expand
to reveal the change diff or raw payload, and a "View all in audit
log" link jumps to the audit page pre-filtered by the same reporter.

The audit lookup is best-effort — the edit form still renders if the
audit endpoint is unavailable.

Note: actual abuse reports submitted via POST /api/v1/report are
stored in the `reports` table, not in `audit_log`. This section
surfaces admin actions on the reporter record (created / updated /
deleted), matching the same pattern used on the consumer edit page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa 1 долоо хоног өмнө
parent
commit
20a2a8dace

+ 92 - 0
ui/resources/views/pages/reporters/edit.twig

@@ -39,5 +39,97 @@
             </div>
         {% endif %}
     </form>
+
+    <section class="mt-8 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
+        <header class="flex items-center justify-between gap-3 border-b border-slate-200 px-5 py-3 dark:border-slate-800">
+            <div>
+                <h2 class="text-sm font-semibold tracking-tight">Recent reports</h2>
+                <p class="text-xs text-slate-500 dark:text-slate-400">10 most recent audit entries for this reporter.</p>
+            </div>
+            <a href="/app/audit?entity_type=reporter&amp;entity_id={{ reporter.id }}" class="whitespace-nowrap text-xs font-medium text-indigo-600 hover:underline dark:text-indigo-400">View all in audit log →</a>
+        </header>
+        {% if audit_items|default([])|length > 0 %}
+            <div x-data="{ open: null }">
+                <table class="w-full text-sm">
+                    <thead class="border-b border-slate-200 bg-slate-50 text-left text-xs uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400">
+                        <tr>
+                            <th class="px-5 py-2 font-medium">When</th>
+                            <th class="px-5 py-2 font-medium">Actor</th>
+                            <th class="px-5 py-2 font-medium">Action</th>
+                            <th class="px-5 py-2 text-right font-medium">Payload</th>
+                        </tr>
+                    </thead>
+                    <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
+                        {% for ev in audit_items %}
+                            <tr>
+                                <td class="px-5 py-2 align-top"><time class="irdb-dt font-mono text-xs text-slate-600 dark:text-slate-300" datetime="{{ ev.occurred_at }}" title="{{ ev.occurred_at }}">{{ ev.occurred_at }}</time></td>
+                                <td class="px-5 py-2 align-top text-xs">
+                                    <span class="rounded bg-slate-100 px-1.5 py-0.5 font-mono uppercase tracking-tight text-slate-700 dark:bg-slate-800 dark:text-slate-300">{{ ev.actor_kind }}</span>
+                                    {% if ev.actor_id %}<span class="ml-1 font-mono text-slate-500">#{{ ev.actor_id }}</span>{% endif %}
+                                </td>
+                                <td class="px-5 py-2 align-top">
+                                    <span class="inline-block rounded bg-blue-100 px-2 py-0.5 font-mono text-[0.7rem] uppercase tracking-tight text-blue-900 dark:bg-blue-900 dark:text-blue-100">{{ ev.action }}</span>
+                                </td>
+                                <td class="px-5 py-2 align-top text-right">
+                                    {% if ev.details %}
+                                        <button type="button" x-on:click="open = (open === {{ ev.id }} ? null : {{ ev.id }})" class="rounded border border-slate-300 px-2 py-0.5 text-xs hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800">View</button>
+                                    {% else %}
+                                        <span class="text-xs text-slate-400">—</span>
+                                    {% endif %}
+                                </td>
+                            </tr>
+                            {% if ev.details %}
+                                <tr x-show="open === {{ ev.id }}" x-cloak>
+                                    <td colspan="4" class="bg-slate-50 px-5 py-3 dark:bg-slate-950">
+                                        {% if ev.details.changes is defined and ev.details.changes is iterable and ev.details.changes|length > 0 %}
+                                            <div class="mb-2 text-[0.7rem] font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Changes</div>
+                                            <table class="w-full text-xs">
+                                                <thead class="text-left text-[0.7rem] uppercase text-slate-500 dark:text-slate-400">
+                                                    <tr>
+                                                        <th class="px-2 py-1 font-medium">Field</th>
+                                                        <th class="px-2 py-1 font-medium">Before</th>
+                                                        <th class="px-2 py-1 font-medium">After</th>
+                                                    </tr>
+                                                </thead>
+                                                <tbody class="divide-y divide-slate-200 bg-white dark:divide-slate-800 dark:bg-slate-900">
+                                                    {% for field, change in ev.details.changes %}
+                                                        <tr>
+                                                            <td class="px-2 py-1 font-mono text-slate-700 dark:text-slate-200">{{ field }}</td>
+                                                            <td class="px-2 py-1 align-top">
+                                                                {% if change.from is null %}
+                                                                    <span class="text-slate-400">∅</span>
+                                                                {% elseif change.from is iterable %}
+                                                                    <pre class="overflow-x-auto rounded bg-rose-50 px-2 py-1 font-mono text-[0.7rem] text-rose-900 dark:bg-rose-950 dark:text-rose-200">{{ change.from|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
+                                                                {% else %}
+                                                                    <span class="rounded bg-rose-50 px-1.5 py-0.5 font-mono text-rose-900 dark:bg-rose-950 dark:text-rose-200">{{ change.from }}</span>
+                                                                {% endif %}
+                                                            </td>
+                                                            <td class="px-2 py-1 align-top">
+                                                                {% if change.to is null %}
+                                                                    <span class="text-slate-400">∅</span>
+                                                                {% elseif change.to is iterable %}
+                                                                    <pre class="overflow-x-auto rounded bg-emerald-50 px-2 py-1 font-mono text-[0.7rem] text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">{{ change.to|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
+                                                                {% else %}
+                                                                    <span class="rounded bg-emerald-50 px-1.5 py-0.5 font-mono text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">{{ change.to }}</span>
+                                                                {% endif %}
+                                                            </td>
+                                                        </tr>
+                                                    {% endfor %}
+                                                </tbody>
+                                            </table>
+                                        {% else %}
+                                            <pre class="overflow-x-auto rounded bg-white p-3 text-xs dark:bg-slate-900">{{ ev.details|json_encode(constant('JSON_PRETTY_PRINT')) }}</pre>
+                                        {% endif %}
+                                    </td>
+                                </tr>
+                            {% endif %}
+                        {% endfor %}
+                    </tbody>
+                </table>
+            </div>
+        {% else %}
+            <p class="px-5 py-6 text-center text-sm text-slate-400">No activity yet.</p>
+        {% endif %}
+    </section>
 </div>
 {% endblock %}

+ 15 - 0
ui/src/Controllers/ReportersController.php

@@ -91,10 +91,25 @@ final class ReportersController
             return $response->withStatus(303)->withHeader('Location', '/app/reporters');
         }
 
+        $auditItems = [];
+        try {
+            $auditPage = $this->admin->listAuditLog(
+                $user->userId,
+                ['entity_type' => 'reporter', 'entity_id' => (string) $id],
+                1,
+                10,
+            );
+            $auditItems = is_array($auditPage['items'] ?? null) ? $auditPage['items'] : [];
+        } catch (ApiException) {
+            // Audit lookup is best-effort; the edit form must still render
+            // if the audit endpoint is unavailable.
+        }
+
         return $this->twigEngine->render($response, 'pages/reporters/edit.twig', [
             'active_section' => 'reporters',
             'reporter' => $reporter,
             'can_write' => $this->userIs($user, 'admin'),
+            'audit_items' => $auditItems,
         ]);
     }