| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 17.14% covered (danger) | 17.14% | 24 / 140 | 25.00% covered (danger) | 25.00% | 1 / 4 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
ActivityLogPage | 17.14% covered (danger) | 17.14% | 24 / 140 | 25.00% covered (danger) | 25.00% | 1 / 4 | 247.54 | 0.00% covered (danger) | 0.00% | 0 / 1
register | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
addMenu | 100.00% covered (success) | 100.00% | 8 / 8 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
render | 13.56% covered (danger) | 13.56% | 16 / 118 | 0.00% covered (danger) | 0.00% | 0 / 1 | 122.15 |
renderPagination | 0.00% covered (danger) | 0.00% | 0 / 13 | 0.00% covered (danger) | 0.00% | 0 / 1 | 30 |

1 | <?php
2 | /**
3 | * Activity Log Admin Page (F230).
4 | *
5 | * Operator-facing view of their server-side audit log.
6 | *
7 | * Surfaces the wp_swapads_audit_log entries (which have been written
8 | * since F230) so the operator can see exactly what happened on their
9 | * account: when a credit was spent, when a backlink was approved /
10 | * rejected, when source-check ran and what it found.
11 | *
12 | * Audit log is critical for trust in a 1:1 barter exchange — operators
13 | * must be able to see "who did what" so a compromised admin cookie or
14 | * buggy automation can't drain credits without notice.
15 | *
16 | * @package SwapAds\Client\Admin
17 | * @since 1.5.2
18 | */
19 |
20 | declare(strict_types=1);
21 |
22 | namespace SwapAds\Client\Admin;
23 |
24 | use SwapAds\Client\Admin\OperatorDashboardHubPage;
25 | use SwapAds\Client\Api\RestClient;
26 | use SwapAds\Client\License\LicenseManager;
27 |
28 | /**
29 | * Class ActivityLogPage.
30 | *
31 | * @since 1.5.2
32 | */
33 | final class ActivityLogPage
34 | {
35 | public const MENU_SLUG = 'swapads-client-activity-log';
36 | public const ERROR_CONTEXT = 'swapads_client_activity_log';
37 | public const PER_PAGE = 50;
38 |
39 | /**
40 | * Register WP hooks.
41 | */
42 | public static function register(): void
43 | {
44 | add_action('admin_menu', [self::class, 'addMenu']);
45 | }
46 |
47 | /**
48 | * Register the admin page (sub-menu of SwapAds).
49 | */
50 | public static function addMenu(): void
51 | {
52 | add_submenu_page(
53 | OperatorDashboardHubPage::MENU_SLUG,
54 | 'Activity Log',
55 | 'Activity Log',
56 | 'manage_options',
57 | self::MENU_SLUG,
58 | [self::class, 'render']
59 | );
60 | }
61 |
62 | /**
63 | * Render the activity log page.
64 | */
65 | public static function render(): void
66 | {
67 | if (!current_user_can('manage_options')) {
68 | wp_die('Insufficient permissions', 'Forbidden', ['response' => 403]);
69 | }
70 | $licensed = LicenseManager::isLicensed();
71 | if (!$licensed) {
72 | echo Renderer::pageHeader(
73 | 'Activity Log',
74 | 'Every state-changing action on your account is recorded here.'
75 | );
76 | echo Renderer::card(
77 | 'License required',
78 | '<p>You need an active license before activity events are recorded.</p>'
79 | . '<p><a href="' . esc_url(admin_url('admin.php?page=' . OperatorDashboardHubPage::MENU_SLUG))
80 | . '" class="button">Activate license</a></p>',
81 | 'swapads-card-warning'
82 | );
83 | echo Renderer::pageFooter();
84 | return;
85 | }
86 |
87 | $rest = RestClient::fromOption();
88 | $action = trim((string) ($_GET['action_filter'] ?? ''));
89 | $type = trim((string) ($_GET['entity_filter'] ?? ''));
90 | $offset = max(0, (int) ($_GET['offset'] ?? 0));
91 | $resp = $rest->listAuditLog($action, $type, self::PER_PAGE, $offset);
92 | $items = isset($resp['items']) && is_array($resp['items']) ? $resp['items'] : [];
93 | $total = isset($resp['total']) ? (int) $resp['total'] : count($items);
94 | $error = $rest->lastError();
95 |
96 | echo Renderer::pageHeader(
97 | 'Activity Log',
98 | sprintf(
99 | '%s events recorded. Newest first.',
100 | number_format_i18n($total)
101 | )
102 | );
103 |
104 | // Surface any inline error.
105 | if ($error !== null) {
106 | echo ClientErrorRenderer::render(self::ERROR_CONTEXT, [
107 | 'code' => 'BACKEND_UNREACHABLE',
108 | 'message' => (string) wp_json_encode($error),
109 | ], 'load activity log');
110 | }
111 |
112 | // Filters row
113 | echo '<form method="get" class="swapads-filter-bar" style="margin-bottom:16px;">';
114 | echo '<input type="hidden" name="page" value="' . esc_attr(self::MENU_SLUG) . '">';
115 | echo '<label style="margin-right:12px;">Action: ';
116 | echo '<select name="action_filter">';
117 | $opts = [
118 | '' => 'All actions',
119 | 'backlink.offer' => 'backlink.offer',
120 | 'backlink.remove' => 'backlink.remove',
121 | 'backlink.update' => 'backlink.update',
122 | 'approval.pending' => 'approval.pending',
123 | 'approval.reject' => 'approval.reject',
124 | 'source_check.ok' => 'source_check.ok',
125 | 'source_check.404' => 'source_check.404',
126 | 'source_check.error' => 'source_check.error',
127 | 'source_check.refund' => 'source_check.refund',
128 | ];
129 | foreach ($opts as $val => $label) {
130 | printf(
131 | '<option value="%s"%s>%s</option>',
132 | esc_attr($val),
133 | selected($action, $val, false),
134 | esc_html($label)
135 | );
136 | }
137 | echo '</select></label>';
138 | echo '<label style="margin-right:12px;">Entity: ';
139 | echo '<select name="entity_filter">';
140 | $eopts = [
141 | '' => 'All entities',
142 | 'backlink' => 'backlink',
143 | 'approval' => 'approval',
144 | 'source_check' => 'source_check',
145 | 'license' => 'license',
146 | ];
147 | foreach ($eopts as $val => $label) {
148 | printf(
149 | '<option value="%s"%s>%s</option>',
150 | esc_attr($val),
151 | selected($type, $val, false),
152 | esc_html($label)
153 | );
154 | }
155 | echo '</select></label>';
156 | echo '<button type="submit" class="button">Filter</button>';
157 | echo '</form>';
158 |
159 | if (empty($items)) {
160 | echo Renderer::card(
161 | 'No activity yet',
162 | '<p>No matching events. Once you approve, reject, or run source-checks, they will appear here.</p>',
163 | 'swapads-card-info'
164 | );
165 | } else {
166 | echo '<table class="widefat striped" role="table" aria-label="Activity log">';
167 | echo '<thead><tr>';
168 | echo '<th scope="col">When</th>';
169 | echo '<th scope="col">Actor</th>';
170 | echo '<th scope="col">Action</th>';
171 | echo '<th scope="col">Entity</th>';
172 | echo '<th scope="col">Details</th>';
173 | echo '</tr></thead><tbody>';
174 | foreach ($items as $row) {
175 | $when = (string) ($row['created_at'] ?? '');
176 | $actor = (string) ($row['actor'] ?? 'system');
177 | $a = (string) ($row['action'] ?? '');
178 | $eT = (string) ($row['entity_type'] ?? '');
179 | $eI = (string) ($row['entity_id'] ?? '');
180 | $details = $row['details'] ?? null;
181 | echo '<tr>';
182 | echo '<td>' . esc_html(mysql2date('Y-m-d H:i:s', $when)) . '</td>';
183 | echo '<td><span class="swapads-actor swapads-actor--' . esc_attr($actor) . '">' . esc_html($actor) . '</span></td>';
184 | echo '<td><code>' . esc_html($a) . '</code></td>';
185 | echo '<td>' . esc_html($eT) . ($eI !== '' ? ' #' . esc_html($eI) : '') . '</td>';
186 | echo '<td>';
187 | if (is_array($details)) {
188 | $json = (string) wp_json_encode($details);
189 | echo '<pre style="margin:0; font-size:11px; max-width:480px; overflow-x:auto;">'
190 | . esc_html($json)
191 | . '</pre>';
192 | } else {
193 | echo '<span class="swapads-muted">—</span>';
194 | }
195 | echo '</td>';
196 | echo '</tr>';
197 | }
198 | echo '</tbody></table>';
199 |
200 | // Pagination links
201 | self::renderPagination($offset, count($items), $total, $action, $type);
202 | }
203 |
204 | echo Renderer::pageFooter();
205 | }
206 |
207 | /**
208 | * Render Next / Prev pagination links.
209 | */
210 | private static function renderPagination(int $offset, int $shown, int $total, string $action, string $type): void
211 | {
212 | if ($shown < self::PER_PAGE && $offset === 0) {
213 | return; // Only one page, no nav needed.
214 | }
215 | $baseUrl = admin_url('admin.php?page=' . self::MENU_SLUG);
216 | $params = ['action_filter' => $action, 'entity_filter' => $type];
217 |
218 | echo '<div class="tablenav" style="margin-top:12px;"><div class="tablenav-pages">';
219 | if ($offset > 0) {
220 | $prev = $offset - self::PER_PAGE;
221 | $params['offset'] = max(0, $prev);
222 | echo '<a class="button" href="' . esc_url(add_query_arg($params, $baseUrl)) . '">&laquo; Previous</a> ';
223 | }
224 | if ($offset + $shown < $total) {
225 | $params['offset'] = $offset + self::PER_PAGE;
226 | echo '<a class="button" href="' . esc_url(add_query_arg($params, $baseUrl)) . '">Next &raquo;</a>';
227 | }
228 | echo '</div></div>';
229 | }
230 | }