Strapi: Upload & Download & Webhook logs#115
Conversation
Adds a new Activity Logs feature to the plugin with: - Server-side: DB model (Strapi store), service, controller, and routes for storing and retrieving log sessions and entries - Upload/download services now create log sessions and record entries during transfers (webhook-triggered downloads logged as webhook type) - Admin-side: Activity Logs page with three tabs (Upload, Download, Webhooks), searchable sessions table, and detail view with console- style log entries - Navigation integration with icon in plugin sidebar Resolves LOC-3981 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🔍 Review Round 1 — PASS
|
- Add mutex around read-modify-write operations in activity logs service to prevent lost updates during concurrent upload + webhook sessions - Wrap LocalazyUploadService.upload() call in try/catch so sessions are properly marked as failed if the API call throws - Remove no-op try/catch wrappers in admin activity logs service - Extract shared formatDate/formatDuration/getStatusColor to utils file - Remove defaultRoute update from Activity Logs page (not appropriate for a logs page to become the landing page) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🔍 Review Round 2 — PASS ✅Previous Round Resolution
Must FixNone. Should Fix
🧪 Manual Testing ChecklistNo new scenarios — Round 1 checklist still applies. Summary3 should-fix items and 1 suggestion resolved. However, the `updatePluginSettings({ defaultRoute })` removal was incorrect — this is the standard last-visited-route pattern and should be restored. The mutex, upload error handling, try/catch cleanup, and DRY utils are all good. DECISION: PASS_WITH_SUGGESTIONS |
The defaultRoute tracks the last visited page, not a permanent landing page — Activity Logs should participate in this like other pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Click any column header to sort ascending/descending. Active sort column is highlighted with a caret icon. Supports sorting by status, date, duration, initiated by, and summary. Default sort is by date descending. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sort column and direction are saved to localStorage and restored on page load, so the user's preference survives navigation and refreshes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each tab (upload, download, webhook) now remembers its own sort column and direction. Preferences are stored in plugin settings (Strapi store) with a 1s debounce to avoid excessive API calls during rapid toggling. Loaded on page init so they persist across browsers and sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Box passes alignItems directly to the DOM element, triggering a React warning. Flex handles flex props correctly without leaking to the DOM. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reserve a fixed 8px slot for the sort caret icon in every header so columns don't resize when the active sort column changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds From/To date pickers alongside the search field. Sessions are filtered client-side by startedAt timestamp. The dateTo filter includes the full end-of-day. Uses Strapi DS DatePicker component with clear support. Date range persists across tab switches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strapi DatePicker returns dates at midnight UTC, which in positive UTC offset timezones (e.g. Europe/Warsaw) appears as the previous day. Normalize by constructing a new Date from UTC year/month/day components in local time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Store raw DatePicker Date as-is (for correct display), and normalize only in the filter by extracting UTC year/month/day to construct local start-of-day and end-of-day for timestamp comparison. This avoids the double-conversion that caused a 2-day offset. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace broken native date inputs with Strapi DS DatePicker. Fix timezone round-trip by converting onChange local Date to UTC midnight before storing, so the value prop stays consistent with DatePicker's internal UTC-based CalendarDate conversion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tab switches no longer clear search/date filters or show the full-page loader. Only the sessions table content reloads while filters stay intact. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace count-based FIFO eviction with time-based retention. Sessions older than 1 year are pruned when a new session starts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add generic exportAsJson(data, filename) in @common/utils for reuse across the plugin. Activity Logs page gets an "Export logs" button that fetches all sessions with full entries and downloads as JSON. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use Strapi DS Pagination component with 20 items per page, always visible. Page resets on filter/sort/tab changes. Add info note below page subtitle about 1-year log retention policy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The nested DesignSystemProvider (needed for button theming) injects a
GlobalStyle with h1-h6 { font: unset } that strips heading styles from
Strapi's own provider. Fix with a shared HeadingFixGlobalStyle that
restores correct Typography variant sizes using data-strapi-header
attribute selectors.
- Plugin pages: DesignSystemProvider + HeadingFixGlobalStyle + removed as='h2'
- Settings pages: DesignSystemProvider + HeadingFixGlobalStyle (as='h2' kept)
- Log detail: BackButton from @strapi/strapi/admin replaces custom ghost button
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🔍 Review Round 3 — PASS
|
| Issue | Round | Status |
|---|---|---|
| Restore `updatePluginSettings({ defaultRoute })` | #2 | ✅ Resolved |
| All Round 1 items (mutex, upload error, try/catch, DRY) | #1 | ✅ Confirmed no regressions |
Must Fix
None.
Should Fix
- Export triggers N+1 API requests: `exportLogsAsJson` fetches sessions per type (3 requests), then fetches each session's detail individually (N requests). With hundreds of sessions, this fires hundreds of sequential HTTP requests. Consider a server-side bulk export endpoint (`GET /activity-logs/export`) returning all sessions with entries in one response.
- Pagination renders all page links without truncation: `Array.from({ length: totalPages })` renders every page link. With 1-year retention at PAGE_SIZE=20, this could produce hundreds of `` elements. Add ellipsis/truncation (show first, last, and a window around current page).
Suggestions
- `emptyActivityLogs` is a shared mutable object — `getActivityLogs()` returns it directly when store is empty, then `startSession` mutates it via `.unshift()`. Low risk but cleaner to return a fresh object.
- Consider limiting exported log data — `entry.data` could contain translation values. Acceptable for admin-only export, but worth a product decision.
🧪 Manual Testing Checklist (delta — new features)
Sorting
- Sort by each column (status, startedAt, duration, initiatedBy, summary) → ascending/descending toggle works
- Switch tabs → sort preference persists per tab (upload/download/webhook stored independently)
- Reload page → sort preferences restored from plugin settings
Date range filter
- Set "From" and "To" dates → sessions outside range hidden
- Set "From" only (no "To") → only lower bound applies
- Set "To" only (no "From") → only upper bound applies
- Clear date filters → all sessions reappear
- Test timezone boundary: session at 23:59 local on selected date is included
Pagination
- Navigate through pages → correct 20 items per page
- Change filter/search while on page 3 → page resets to 1
- Test with 50+ sessions → verify performance and that all page links render (will be many without truncation)
Export
- Click "Export logs" → JSON file downloads with filename `localazy-activity-logs-YYYY-MM-DD.json`
- Verify exported JSON contains all session types with full entry detail
- Export with zero logs → graceful handling (empty or no download)
- Export with many sessions → verify it completes (note: N+1 requests, may be slow)
Retention
- Verify retention is now time-based (1 year) instead of count-based (100)
- Create a session, manually backdate `startedAt` to >1 year ago → trigger new session → old one evicted
Summary
18 new commits add well-structured client-side sorting, date filtering, pagination, and JSON export. Code is clean and correct. Two should-fix items: N+1 export pattern will degrade with many sessions, and pagination links need truncation at scale. No blocking issues.
DECISION: PASS_WITH_SUGGESTIONS
- Add PluginErrorBoundary with friendly error page and disconnect button for cases where the Localazy project is deleted or session is invalid. Uses functional ErrorFallback with hooks, wrapped by thin class boundary. - Add server-side bulk export endpoint (GET /activity-logs/export) to replace N+1 client-side fetching during log export. - Add pagination truncation with Dots — shows first, last, and a window of 2 around the current page instead of rendering all page links. - Fix mutable emptyActivityLogs shared reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🔍 Review Round 4 — PASS ✅Previous Round Resolution
Must FixNone. Should FixNone. 🧪 Manual Testing Checklist (delta)
SummaryAll Round 3 should-fix items resolved. Bulk export endpoint eliminates N+1 requests, pagination truncation with Dots keeps the paginator usable at scale, and the shared mutable object is fixed. Bonus: error boundary added for crash resilience. Clean across all 4 rounds. DECISION: PASS |
Highlight matching text in status, date, initiated by, and summary columns when searching. Use lodash debounce (300ms) to avoid re-rendering the table on every keystroke. Update search placeholder to clarify which fields are searchable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract from monolithic page files into focused modules: - models/activity-logs.ts — shared types (SessionItem, SessionDetail, etc.) - components/HighlightMatch — search term highlighting (both pages) - components/StatusLabel — status with color + optional highlight - components/SortableHeader — clickable column header with caret - components/SessionMetadata — status/date/duration display (detail page) - components/SessionsTable — full table with sort, filter, pagination - hooks/use-debounced-search — shared debounced search (both pages) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add @typescript-eslint/no-floating-promises with type-aware linting. Fix 32 violations across 17 files using void prefix for fire-and-forget promises, await for test assertions, and return for error interceptor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a webhook configuration section to Global Settings with:
- Server endpoints to check webhook status, register webhook, and get
Strapi URL (with localhost fallback from host/port config)
- Modal walkthrough with 3 steps: verify URL, confirm endpoint, register
- Webhook identified by customId ('strapi-plugin-localazy') so URL
changes in Localazy don't break the pairing
- Detects deleted webhooks and prompts for reconfiguration
- Triggers on 'project_published' event
- Includes ngrok hint for local development
- Link to Localazy webhook docs
- Webhook config section placed first in Download Settings as prerequisite
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use useTheme() from styled-components to access theme colors instead of hardcoded hex values. Fixes unreadable hover rows and borders in dark mode across Activity Logs, session detail, sortable headers, search highlight, and status column. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🔍 Review Round 5 — NEEDS WORK ❌Previous Round ResolutionAll prior items (rounds 1-4) remain resolved. No regressions. Must Fix
Should Fix
Suggestions
🧪 Manual Testing Checklist (delta)Search highlighting
Webhook setup (new feature)
Dark mode
Component refactor
SummaryThe webhook setup feature is well-structured but needs server-side URL validation (must-fix for security) and has a dead type variant. Search highlighting and debounce hook have minor issues. Dark mode and component refactor are clean. First NEEDS WORK in this PR's review history. DECISION: NEEDS_WORK |
Must-fix: - Server-side webhook URL validation (non-empty, http/https, parseable) - Remove dead 'deleted' state variant from WebhookState type Should-fix: - HighlightMatch now highlights all occurrences using regex split, with special characters escaped to prevent regex errors - useDebouncedSearch cancels pending debounce on unmount - getStrapiUrl returns isLocal flag for localhost/private IPs, modal shows warning when URL is non-public - Replace Alert with custom info/warning blocks (no close button) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
🔍 Review Round 6 — PASS ✅Previous Round Resolution
Must FixNone. Should FixNone. 🧪 Manual Testing ChecklistNo new scenarios — Round 5 checklist still applies, specifically:
SummaryAll 5 Round 5 items resolved cleanly in a single commit. URL validation covers empty, non-string, invalid URL, and non-HTTP protocols. Local address detection uses a comprehensive regex covering localhost, 127.0.0.1, 0.0.0.0, and RFC 1918 private ranges. Highlight and debounce fixes are correct. Clean resolution of the first NEEDS WORK round. DECISION: PASS |
Summary
Changes
Server (8 files)
server/src/db/model/activity-logs.ts(new) — Data model types and store keyserver/src/services/activity-logs-service.ts(new) — CRUD service for log sessions/entriesserver/src/controllers/activity-logs-controller.ts(new) — REST endpointsserver/src/routes/activity-logs-routes.ts(new) — Route definitionsserver/src/services/index.ts,controllers/index.ts,routes/index.ts,core/index.ts— Registrationserver/src/services/localazy-transfer-upload-service.ts— Log session creation during uploadsserver/src/services/localazy-transfer-download-service.ts— Log session for downloads + webhook distinctionAdmin (9 files)
admin/src/pages/ActivityLogs.tsx(new) — Main page with tabs, search, sessions tableadmin/src/pages/ActivityLogDetail.tsx(new) — Detail view with session metadata and log entriesadmin/src/modules/activity-logs/services/activity-logs-service.ts(new) — API serviceadmin/src/modules/activity-logs/locale/en.ts(new) — Translationsadmin/src/pages/App.tsx— Routes for activity logsTesting
Note
The task description mentions potential conflict with the multi-user support task (LOC-3982). Currently uses
'Strapi User'as placeholder forinitiatedBy— should be updated once user tracking is implemented.Fibery Task
LOC-3981
🤖 Generated with Claude Code