Skip to content

feat: add Logs page to POS Settings#511

Merged
kilbot merged 14 commits intomainfrom
feature/logs-page
Feb 11, 2026
Merged

feat: add Logs page to POS Settings#511
kilbot merged 14 commits intomainfrom
feature/logs-page

Conversation

@kilbot
Copy link
Copy Markdown
Contributor

@kilbot kilbot commented Feb 11, 2026

Summary

Closes #504

  • Adds a new Logs tab under a Tools group in the POS Settings sidebar
  • Moves License to an Account group, restructuring the sidebar into Settings / Tools / Account
  • New REST controller (GET /wcpos/v1/logs, POST /wcpos/v1/logs/mark-read) that reads log entries from both file-based and database WooCommerce log handlers
  • Severity-specific nav badges (red for errors, amber for warnings) showing unread counts since last viewed
  • Log viewer with level filtering (All/Errors/Warnings), expandable entries for long messages and context, and pagination
  • Fatal errors banner linking to WooCommerce's built-in log viewer
  • 14 new PHPUnit tests covering route registration, auth, file parsing, DB parsing, level filtering, pagination, mark-read, and unread count calculation

Test plan

  • Load POS Settings — sidebar should show three groups: Settings, Tools, Account
  • Logs appears under Tools with severity badges if there are unread errors/warnings
  • Click Logs — entries display in reverse chronological order
  • Filter buttons (All/Errors/Warnings) filter the list correctly
  • Long messages truncate at ~100 chars; clicking expands to show full message + context
  • Revisiting the Logs page after viewing clears the badges
  • If fatal-errors log files exist, a warning banner appears with a link to WC logs
  • Works with both file-based (default) and database WC log handlers
  • PHPUnit: vendor/bin/phpunit --filter Test_Logs_Controller passes (14 tests)

Summary by CodeRabbit

  • New Features

    • Added a Logs page in Settings with filtering (all, errors, warnings), pagination, expandable entries, fatal-error notices with link, and a mark-as-read action.
    • Unread error/warning counts displayed in Settings navigation and synchronized in real time.
  • Translations

    • Added localization strings for Tools, Logs, and related UI labels.
  • Tests

    • Added REST API tests covering routes, permissions, parsing, pagination, file/DB sources, and read-tracking.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 11, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds a Logs feature: new Logs REST controller (file/DB sources) with listing, filtering, pagination, fatal-error detection, and mark-read/unread tracking; registers controller in API; exposes unread counts to settings bootstrap; adds a React Logs screen, nav badge, routing, translations, and comprehensive tests and plan docs.

Changes

Cohort / File(s) Summary
Backend Logs REST API
includes/API.php, includes/API/Logs.php
New API\Logs controller registered; routes for GET /wcpos/v1/logs and POST /wcpos/v1/logs/mark-read; supports file and DB handlers, parse logic, level filtering, pagination, fatal-error detection, permissions, and Logs::get_unread_counts() static method.
Backend Settings Integration
includes/Admin/Settings.php
Injects unreadLogCounts into the settings inline bootstrap payload by calling Logs::get_unread_counts() for the current user.
Frontend Log Viewer & Hooks
packages/settings/src/screens/logs/index.tsx, packages/settings/src/screens/logs/use-unread-log-counts.ts
New Logs screen component (fetching/paginating/filtering entries, fatal-error notice, expandable entries) and useUnreadLogCounts() hook with markLogsRead() that syncs unread counts via API.
Frontend Navigation & Routing
packages/settings/src/layouts/nav-item.tsx, packages/settings/src/layouts/nav-sidebar.tsx, packages/settings/src/router.tsx, packages/settings/src/layouts/root-layout.tsx
Nav badge API extended to support severity badges; Logs nav item added under Tools with severity/unread badge; /logs route registered; page title mapping for /logs added.
Translations
packages/settings/src/translations/locales/en/wp-admin-settings.json
Added translation keys for common/tools/account and logs UI (logs, errors, warnings, fatal_errors_detected, view_in_wc, no_entries).
Tests
tests/includes/API/Test_Logs_Controller.php
New REST unit tests covering route registration, permissions, file/DB parsing, filtering, pagination/headers, mark-read behavior, unread-counts, and helpers for log files and DB table setup.
Documentation / Plan
docs/plans/2026-02-11-logs-page-plan.md
Detailed implementation plan and rollout checklist for Logs page feature.

Sequence Diagram

sequenceDiagram
    participant User as User (Browser)
    participant Frontend as Logs Component
    participant API as REST API<br/>wcpos/v1/logs
    participant Handler as Handler<br/>(File / DB)
    participant DB as Database
    participant FS as File System

    User->>Frontend: Load Logs Page
    Frontend->>API: GET /wcpos/v1/logs?level=&page=
    API->>Handler: get_handler_type()
    alt File handler
        Handler->>FS: read log files
        FS-->>Handler: log lines
        Handler->>Handler: parse_log_line()
        Handler-->>API: entries
    else DB handler
        Handler->>DB: SELECT rows from woocommerce_log
        DB-->>Handler: rows
        Handler->>Handler: transform rows → entries
        Handler-->>API: entries
    end
    API-->>Frontend: entries + has_fatal_errors + headers
    Frontend->>Frontend: render list, filters, pagination
    User->>Frontend: Click "Mark as Read"
    Frontend->>API: POST /wcpos/v1/logs/mark-read
    API->>DB: update user_meta (last viewed)
    DB-->>API: success
    API-->>Frontend: success
    Frontend->>Frontend: reset unread counts (hook)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I hopped through logs both file and DB,
Counting warnings, errors, and mystery.
A badge on the sidebar, a page to explore,
Click "mark read" — the counts fall to floor.
Carrots for devs, and bugs no more.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Logs page to POS Settings' accurately describes the main change, which introduces a new Logs viewer feature in the POS Settings interface.
Linked Issues check ✅ Passed The PR comprehensively implements all major backend and frontend objectives from #504: REST endpoints for logs retrieval and mark-read, unread count tracking, dual log source support (file/DB), frontend logs screen, sidebar nav item with severity badges, filtering, pagination, and fatal-errors banner.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the Logs feature scope: REST API controller, frontend logs screen, sidebar navigation updates, translations, and tests. No unrelated or extraneous modifications were detected.
Docstring Coverage ✅ Passed Docstring coverage is 85.37% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/logs-page

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 11, 2026

E2E API Test Results

35 tests   35 ✅  3s ⏱️
17 suites   0 💤
 1 files     0 ❌

Results for commit 6eadd2b.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@docs/plans/2026-02-11-logs-page-plan.md`:
- Around line 11-13: Remove the machine-specific absolute worktree path after
the "Worktree:" label and replace it with a generic placeholder (e.g.,
"Worktree: /path/to/worktree" or "Worktree: ./worktree") so the plan is
portable; leave the "Test command:" line unchanged.

In `@includes/API/Logs.php`:
- Around line 197-218: get_db_entries currently loads the entire log table into
PHP; add optional $limit and $offset parameters to get_db_entries($level = null,
?int $limit = null, ?int $offset = null) and incorporate them into the SQL as
prepared LIMIT/OFFSET clauses (use $wpdb->prepare with %d placeholders) so the
query returns only the requested page; then change get_unread_counts to avoid
calling get_db_entries for counting and instead run a single prepared COUNT(*)
query on the same $wpdb->prefix . 'woocommerce_log' table with the same source =
'woocommerce-pos' and level severity condition to compute unread counts directly
in SQL.
- Around line 81-106: The pagination variables $per_page and $page must be
clamped to positive integers before computing $total_pages and $offset to
prevent negative/zero values; update the assignment logic in the same block
(where $per_page and $page are set and before $total_pages/$offset calculations)
to cast to int and enforce $per_page = max(1, (int)...) and $page = max(1,
(int)...), optionally also bounding $per_page to a sane maximum, so subsequent
math in array_slice, ceil and offset is always valid.
- Around line 351-377: get_unread_counts() is comparing $last_viewed (ISO 8601
stored via gmdate('c')) to $entry['timestamp'] (MySQL DATETIME from
get_db_entries()), which fails due to different formats; normalize both values
to Unix timestamps before the comparison. In the loop inside
get_unread_counts(), convert $last_viewed and $entry['timestamp'] to integers
using strtotime (or DateTime->getTimestamp) when they are non-numeric (e.g.
$last_viewed_ts = is_numeric($last_viewed) ? (int)$last_viewed :
strtotime($last_viewed); $entry_ts = is_numeric($entry['timestamp']) ?
(int)$entry['timestamp'] : strtotime($entry['timestamp'])), then use those
timestamps for the conditional that skips read entries and for any other
ordering/compare logic; reference get_unread_counts(), LAST_VIEWED_META_KEY,
get_db_entries()/get_file_entries(), $last_viewed, and $entry['timestamp'] when
making the change.

In `@packages/settings/src/screens/logs/use-unread-log-counts.ts`:
- Around line 44-51: markLogsRead currently zeroes the shared counts and calls
emitChange before the POST completes; capture the previous counts (the variable
referenced as counts) into a local variable, perform the optimistic update (set
counts = { error:0, warning:0 } and emitChange), then wrap the apiFetch call in
a try/catch; on failure restore counts = previousCounts, call emitChange to roll
back the UI, and rethrow or handle the error as appropriate. Ensure you
reference the markLogsRead function, the counts variable, emitChange, and
apiFetch so the rollback is applied only on request failure.
🧹 Nitpick comments (4)
includes/Admin/Settings.php (1)

117-117: Consider performance impact of loading unread counts on every settings page load.

Logs::get_unread_counts() instantiates the Logs controller and reads all log entries (from file or database) to calculate counts. If there are many log entries, this could add latency to every settings page load.

Consider caching the unread counts in a transient or computing them lazily only when the user navigates to a page that displays the badge.

packages/settings/src/screens/logs/index.tsx (3)

48-58: Type safety improvement for pagination metadata.

The _totalPages property is added dynamically but accessed via type assertion (data as any)?._totalPages. Consider extending the response type to include the pagination metadata for better type safety.

Proposed type improvement
 interface LogsResponse {
 	entries: LogEntry[];
 	has_fatal_errors: boolean;
 	fatal_errors_url: string;
+	_totalPages?: number;
 }

 // ...

-const totalPages = (data as any)?._totalPages ?? 1;
+const totalPages = data?._totalPages ?? 1;

153-169: Pagination button labels should be internationalized.

"Previous" and "Next" are hardcoded strings. For consistency with the rest of the UI, these should use the translation function.

Proposed fix
 <button
 	onClick={() => setPage((p) => Math.max(1, p - 1))}
 	disabled={page <= 1}
 	className="wcpos:px-3 wcpos:py-1 wcpos:rounded wcpos:text-sm wcpos:border wcpos:border-gray-300 disabled:wcpos:opacity-50"
 >
-	Previous
+	{t('common.previous', 'Previous')}
 </button>
 // ...
 <button
 	onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
 	disabled={page >= totalPages}
 	className="wcpos:px-3 wcpos:py-1 wcpos:rounded wcpos:text-sm wcpos:border wcpos:border-gray-300 disabled:wcpos:opacity-50"
 >
-	Next
+	{t('common.next', 'Next')}
 </button>

60-62: Logs marked as read immediately on mount.

markLogsRead() is called unconditionally when the component mounts. This means even a brief navigation to the Logs page will clear all unread badges, regardless of whether the user actually reviewed the entries.

This may be intentional UX, but consider whether marking as read should be deferred (e.g., after a short delay or on scroll) to ensure the user had a chance to view the entries.

Comment thread docs/plans/2026-02-11-logs-page-plan.md Outdated
Comment on lines +11 to +13
**Worktree:** `/Users/kilbot/Projects/woocommerce-pos/.worktrees/feature-logs-page`

**Test command:** `npx wp-env run --env-cwd='wp-content/plugins/feature-logs-page' tests-cli -- vendor/bin/phpunit -c .phpunit.xml.dist --verbose`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the machine-specific worktree path from the plan.

The absolute path is local to one machine and will stale quickly; replace with a generic placeholder.

🤖 Prompt for AI Agents
In `@docs/plans/2026-02-11-logs-page-plan.md` around lines 11 - 13, Remove the
machine-specific absolute worktree path after the "Worktree:" label and replace
it with a generic placeholder (e.g., "Worktree: /path/to/worktree" or "Worktree:
./worktree") so the plan is portable; leave the "Test command:" line unchanged.

Comment thread includes/API/Logs.php
Comment thread includes/API/Logs.php
Comment on lines +197 to +218
private function get_db_entries( ?string $level = null ): array {
global $wpdb;

$table = $wpdb->prefix . 'woocommerce_log';

// Check table exists.
$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
if ( ! $table_exists ) {
return array();
}

$where = $wpdb->prepare( 'WHERE source = %s', 'woocommerce-pos' );

if ( $level ) {
$severity = \WC_Log_Levels::get_level_severity( $level );
$where .= $wpdb->prepare( ' AND level = %d', $severity );
}

// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table is safe prefix, $where is prepared above.
$sql = "SELECT timestamp, level, message FROM {$table} {$where} ORDER BY timestamp DESC";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL is safely constructed above with $wpdb->prepare().
$results = $wpdb->get_results( $sql, ARRAY_A );
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full implementation of get_db_entries() in Logs.php
rg -A 30 "function get_db_entries" includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 1086


🏁 Script executed:

# Search for all calls to get_db_entries() to understand how results are used
rg "get_db_entries" includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 211


🏁 Script executed:

# Check for pagination/slicing operations in Logs.php
rg "array_slice|LIMIT|OFFSET|paginate" includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 126


🏁 Script executed:

# Get context around the array_slice call to understand the full function/method
rg -B 10 -A 10 "array_slice" includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 613


🏁 Script executed:

# Look for unread count related code
rg "unread|count" includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 384


🏁 Script executed:

# Get the full Logs.php file to understand the overall structure
wc -l includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 90


🏁 Script executed:

# Get the full get_unread_counts function to see if it uses get_db_entries
rg -A 30 "public static function get_unread_counts" includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 918


🏁 Script executed:

# Check if get_unread_counts calls get_db_entries
rg -B 5 -A 40 "function get_unread_counts" includes/API/Logs.php

Repository: wcpos/woocommerce-pos

Length of output: 1220


Implement server-side pagination in get_db_entries() to avoid loading entire log table.

get_db_entries() loads all rows without LIMIT/OFFSET, then pagination is applied via PHP array_slice(). This is inefficient for large log tables and impacts both page loads and the get_unread_counts() method, which calls get_db_entries() to count unread entries by iterating through every row in PHP.

Pass $limit and $offset parameters to get_db_entries() and apply them in the SQL query. Similarly, optimize get_unread_counts() to use a COUNT query with appropriate WHERE conditions instead of loading all rows into memory.

🤖 Prompt for AI Agents
In `@includes/API/Logs.php` around lines 197 - 218, get_db_entries currently loads
the entire log table into PHP; add optional $limit and $offset parameters to
get_db_entries($level = null, ?int $limit = null, ?int $offset = null) and
incorporate them into the SQL as prepared LIMIT/OFFSET clauses (use
$wpdb->prepare with %d placeholders) so the query returns only the requested
page; then change get_unread_counts to avoid calling get_db_entries for counting
and instead run a single prepared COUNT(*) query on the same $wpdb->prefix .
'woocommerce_log' table with the same source = 'woocommerce-pos' and level
severity condition to compute unread counts directly in SQL.

Comment thread includes/API/Logs.php Outdated
Comment thread packages/settings/src/screens/logs/use-unread-log-counts.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 11, 2026

E2E UI Test Results

18 tests   10 ✅  5m 24s ⏱️
 6 suites   0 💤
 1 files     8 ❌

For more details on these failures, see this check.

Results for commit 9b8094e.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 11, 2026

PHPUnit Test Results

720 tests   712 ✅  54s ⏱️
 40 suites    8 💤
  1 files      0 ❌

Results for commit 9b8094e.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/settings/src/screens/logs/index.tsx`:
- Around line 150-169: The expanded row index (expandedIndex) can persist across
pages and expand the wrong entry when the page changes; update the pagination
handlers or lifecycle to clear it whenever page changes—e.g., when calling
setPage (used in the Previous and Next button onClick handlers) also call the
setter for expandedIndex (setExpandedIndex) to reset to null/undefined/-1, or
add a useEffect that watches page and resets expandedIndex; ensure you reference
and update expandedIndex and setExpandedIndex in the component that renders the
pagination.
- Around line 121-138: The log entry container currently uses a non-focusable
div with an onClick to toggle expansion; replace it with a semantic interactive
element (preferably a <button>) so keyboard users can focus and activate it,
move the onClick handler (onClick={() => isLong && setExpandedIndex(isExpanded ?
null : index)}) to that button, add aria-expanded={isExpanded} and ensure the
disabled state or pointer-events are applied when isLong is false (so
non-expandable entries aren’t interactive), and preserve existing classes
(including LEVEL_STYLES, entry.timestamp, displayMessage) so visual styling
remains unchanged.

Comment thread packages/settings/src/screens/logs/index.tsx Outdated
Comment thread packages/settings/src/screens/logs/index.tsx
kilbot and others added 2 commits February 11, 2026 21:40
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/settings/src/screens/logs/index.tsx`:
- Around line 109-149: The expandedIndex state can point to the wrong row after
entries are refreshed and reordered; add logic to reset expandedIndex when the
entries array changes by using an effect that watches entries and calls
setExpandedIndex(null) (or appropriate default) to clear expansion; locate the
state and setter for expandedIndex and add a useEffect referencing entries to
clear it whenever entries updates to avoid stale expanded rows in the list
rendering code that maps entries.

Comment thread packages/settings/src/screens/logs/index.tsx
@kilbot kilbot merged commit 36d345c into main Feb 11, 2026
2 of 3 checks passed
@kilbot kilbot deleted the feature/logs-page branch February 11, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Logs page in POS Settings

1 participant