瀏覽代碼

feat(ui): show last activity table on consumer edit page

Adds a "Last activity" section at the bottom of the consumer edit
page showing the 10 most recent audit log entries scoped to this
consumer (entity_type=consumer, 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 consumer.

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

Note: blocklist fetches are not currently audited, so this surfaces
admin actions on the consumer record (created / updated / deleted)
rather than per-fetch consumption events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa 1 周之前
父節點
當前提交
7929ed87db
共有 2 個文件被更改,包括 107 次插入0 次删除
  1. 92 0
      ui/resources/views/pages/consumers/edit.twig
  2. 15 0
      ui/src/Controllers/ConsumersController.php

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

@@ -42,5 +42,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">Last activity</h2>
+                <p class="text-xs text-slate-500 dark:text-slate-400">10 most recent audit entries for this consumer.</p>
+            </div>
+            <a href="/app/audit?entity_type=consumer&amp;entity_id={{ consumer.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-cyan-100 px-2 py-0.5 font-mono text-[0.7rem] uppercase tracking-tight text-cyan-900 dark:bg-cyan-900 dark:text-cyan-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/ConsumersController.php

@@ -96,11 +96,26 @@ final class ConsumersController
             return $response->withStatus(303)->withHeader('Location', '/app/consumers');
         }
 
+        $auditItems = [];
+        try {
+            $auditPage = $this->admin->listAuditLog(
+                $user->userId,
+                ['entity_type' => 'consumer', '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/consumers/edit.twig', [
             'active_section' => 'consumers',
             'consumer' => $consumer,
             'policies' => $policiesRaw['items'] ?? [],
             'can_write' => $this->userIs($user, 'admin'),
+            'audit_items' => $auditItems,
         ]);
     }