Skip to content

feat(app): overhaul audit log UX - #754

Merged
HugoRCD merged 3 commits into
mainfrom
feat/audit-log-ux-overhaul
May 30, 2026
Merged

feat(app): overhaul audit log UX#754
HugoRCD merged 3 commits into
mainfrom
feat/audit-log-ux-overhaul

Conversation

@HugoRCD

@HugoRCD HugoRCD commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Enrich the audit log API with human-readable labels (summary, actor, resource, client) via batch lookups instead of raw IDs
  • Replace the badge-heavy table with a timeline UI, responsive filters (action, actor, project), detail drawer, and visible pagination counter
  • Fix audit data quality: token actor ID, team-scoped token events, expanded logging on project/env/variable/team mutations
  • Resolve token scope tooltips to project/team/environment names instead of numeric IDs

Test plan

  • Open /{teamSlug}/team/audit-logs and verify entries show readable summaries (e.g. web-platform / production)
  • Click an entry and confirm the detail drawer shows structured sections + collapsible raw metadata
  • Filter by action, actor type, and project; confirm count updates and Load more works
  • Create/delete a token and confirm token.create appears in the team audit feed
  • On /user/tokens, hover scoped token chips and verify tooltip shows names not IDs
  • Run pnpm exec vitest run test/unit/audit-enrich.test.ts in apps/shelve

Summary by CodeRabbit

  • New Features
    • Redesigned audit log interface with an enhanced timeline feed and detailed event viewer
    • Added filtering by action, actor type, and project
    • Enriched metadata displays human-readable names (projects, environments, tokens) instead of IDs
    • Token scope labels now show actual team/project/environment names
    • Updated audit log documentation with expanded event coverage and improved filtering guidance

Review Change Stack

Replace raw ID-heavy table with human-readable timeline, structured detail drawer, and server-side name resolution for actors and resources.
@vercel

vercel Bot commented May 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
shelve-app Ready Ready Preview, Comment, Open in v0 May 30, 2026 8:46pm
shelve-lp Error Error May 30, 2026 8:46pm
shelve-vault Ready Ready Preview, Comment May 30, 2026 8:46pm

@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@HugoRCD, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 52 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 29c087b7-2b3b-4b17-b3a8-3f1080031713

📥 Commits

Reviewing files that changed from the base of the PR and between 58f5af1 and 0341e80.

📒 Files selected for processing (15)
  • apps/shelve/app/components/audit/LogDetail.vue
  • apps/shelve/app/components/audit/LogEntry.vue
  • apps/shelve/app/composables/useAuditLogs.ts
  • apps/shelve/app/composables/useScopeLabels.ts
  • apps/shelve/server/api/teams/[slug]/audit-logs/index.get.ts
  • apps/shelve/server/api/teams/[slug]/environments/[id].delete.ts
  • apps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/[variableId]/index.delete.ts
  • apps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/[variableId]/index.put.ts
  • apps/shelve/server/api/user/scope-labels.get.ts
  • apps/shelve/server/services/audit-enrich.ts
  • apps/shelve/server/services/audit.ts
  • apps/shelve/server/utils/audit-present.ts
  • apps/shelve/test/e2e/flows/audit-logs.ts
  • apps/shelve/test/unit/audit-enrich.test.ts
  • packages/types/src/Audit.ts
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive audit log UX overhaul, transforming the interface from a table-based view to a timeline-feed design with server-side enrichment. The backend gains an enrichment pipeline that resolves actors, resources, and client metadata; new audit logging is wired throughout all team, project, and token management endpoints; and the frontend refactors to use composables for data fetching and new components for timeline display with filtering and detail modals.

Changes

Audit Log UX Overhaul

Layer / File(s) Summary
Audit types, actions, and session contracts
packages/types/src/Audit.ts, apps/shelve/shared/types/auth.d.ts
AUDIT_ACTIONS constant and AuditLogEntry, AuditLogResponse types contract the enriched audit log shape. UserSession extended with optional token metadata fields (tokenId, tokenPrefix, tokenName).
Scope label resolution and presentation
apps/shelve/server/api/user/scope-labels.get.ts, apps/shelve/server/utils/audit-present.ts, apps/shelve/app/composables/useScopeLabels.ts
New /api/user/scope-labels endpoint returns maps of team/project/environment IDs to names. buildSummary and label helpers generate human-readable audit event descriptions. Client composable caches labels and provides helper functions for resolving IDs to names.
Server audit enrichment and user agent parsing
apps/shelve/server/services/audit-enrich.ts, apps/shelve/server/utils/userAgent.ts
enrichAuditLogs batch-fetches actor/resource records and produces enriched AuditLogEntry objects with actor, resource, and client fields. parseUserAgent extracts browser/OS or known client info from user agent strings.
Authentication and audit service enhancements
apps/shelve/server/services/user.ts, apps/shelve/server/services/audit.ts, apps/shelve/server/middleware/2.auth.ts
authenticateToken returns token metadata (id, prefix, name). Audit service captures token-derived metadata, tracks token-based actors, and adds logAuditForTeams and scope resolution helpers for multi-team token logging. Middleware stores token fields in session.
Audit logging throughout API endpoints
apps/shelve/server/api/teams/[slug]/audit-logs/index.get.ts, apps/shelve/server/api/teams/[slug]/environments/*, apps/shelve/server/api/teams/[slug]/invitations/index.post.ts, apps/shelve/server/api/teams/[slug]/members/[id].delete.ts, apps/shelve/server/api/teams/[slug]/projects/*, apps/shelve/server/api/tokens/*
Audit logs GET endpoint extended with actorType and projectId filters, calls enrichAuditLogs, and returns total count. Environment, invitation, member, project, and variable endpoints all emit audit events with enriched metadata (names, IDs). Token endpoints use team-scoped logging via logAuditForTeams.
Client composables and utilities
apps/shelve/app/composables/useAuditLogs.ts, apps/shelve/app/utils/userAgent.ts, apps/shelve/app/components/token/Scopes.vue
useAuditLogs manages paginated log fetching with debounced filter changes and refetch on team slug change. Scope component integrates label helpers to display human-readable team/project/environment lists in tooltips. Client-side user agent parsing mirrors server behavior.
Audit log timeline components
apps/shelve/app/components/audit/LogEntry.vue, apps/shelve/app/components/audit/LogDetail.vue, apps/shelve/app/components/audit/LogFeed.vue, apps/shelve/app/components/audit/LogFilters.vue
LogEntry renders clickable audit log summary with actor avatar/icon, resource, and date. LogDetail drawer shows full entry context with actor/resource/date/IP details and optional raw metadata. LogFeed handles loading/empty states and list rendering. LogFilters provides action/actor-type/project selectors.
Audit logs page refactor
apps/shelve/app/pages/[teamSlug]/team.vue, apps/shelve/app/pages/[teamSlug]/team/audit-logs.vue
Replaced table-based audit view with feed-based timeline using composables and new components. Page now derives section description from active tab. Filtering, loading, pagination, and detail modals wired via composable state and component selection.
Tests and documentation
apps/shelve/test/unit/audit-enrich.test.ts, apps/shelve/test/e2e/flows/audit-logs.ts, apps/lp/content/docs/2.core-features/audit-logs.md, .changeset/audit-log-ux-overhaul.md
Unit tests verify buildSummary handles variable/token events with metadata and map-derived labels. E2E tests updated to assert enriched response structure, new filter values, and total count. Documentation updated with enrichment details, new query filters, and updated event action names. Changeset documents minor version bump for @shelve/app.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat(app): overhaul audit log UX' directly and accurately reflects the main change: a comprehensive overhaul of the audit log user experience with enriched API responses, timeline UI, filters, and detail drawer.
Description check ✅ Passed The PR description provides a clear summary of changes (enriched API, timeline UI, audit data quality fixes, token scope improvements) and includes a detailed test plan, but does not explicitly reference a linked issue or confirm documentation updates as required by the template checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/audit-log-ux-overhaul

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Thank you for following the naming conventions! 🙏

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/[variableId]/index.put.ts (1)

27-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t derive audit project metadata solely from the URL.

Line 31 updates by variableId only, while Lines 40-49 log whatever projectId was passed in the route. If those IDs disagree, the update can be written against one variable and audited under a different project.

Suggested fix
+  const existing = await db.query.variables.findFirst({
+    where: and(
+      eq(schema.variables.id, variableId),
+      eq(schema.variables.projectId, projectId),
+    ),
+  })
+  if (!existing) throw createError({ statusCode: 404, statusMessage: 'Variable not found' })
+
-  const project = await new ProjectsService().getProject(projectId)
+  const project = await new ProjectsService().getProject(existing.projectId)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/shelve/server/api/teams/`[slug]/projects/[projectId]/variables/[variableId]/index.put.ts
around lines 27 - 49, The audit metadata is using the route projectId which can
diverge from the actual variable's project; fetch the variable's authoritative
project (e.g., call VariablesService.getVariable(variableId) or use the result
returned by VariablesService.updateVariable) and derive projectId/projectName
from that object instead of the route param, and additionally validate that the
variable's project matches the route projectId (or throw) before calling
logAudit so the audit always reflects the actual resource updated (refer to
VariablesService.updateVariable, getVariable, ProjectsService.getProject,
logAudit, variableId and projectId).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/shelve/app/components/audit/LogDetail.vue`:
- Around line 15-16: showRawMetadata is a component-level ref that currently
persists across log selections; add a watcher that resets showRawMetadata.value
= false whenever the selected log/entry changes (watch the selection prop or
emitted selection ref used by LogDetail, e.g., the prop named entry/selectedLog)
so new entries start collapsed; apply the same reset logic where selection can
change (same component areas referenced around lines 120-136) to ensure raw
metadata is cleared on every switch.

In `@apps/shelve/app/components/audit/LogEntry.vue`:
- Around line 14-55: The row is a native <button> (the element with
`@click`="$emit('select')") that currently wraps a nested <NuxtLink>, causing
invalid interactive nesting; change the outer <button> to a non-button container
(e.g., <div>) that preserves keyboard accessibility by adding role="button",
tabindex="0", and keyboard handlers (e.g., `@keydown.enter` and `@keydown.space`
calling $emit('select')) while keeping the existing `@click`="$emit('select')"
behavior and leaving the inner <NuxtLink> and its `@click.stop` unchanged so the
link can be focused and activated independently.

In `@apps/shelve/app/composables/useAuditLogs.ts`:
- Around line 24-47: The fetchLogs function can apply stale or duplicated
responses when multiple concurrent calls complete out-of-order; to fix, add a
per-call sequence token (e.g., incrementing requestId or an inFlightToken stored
alongside loading/loadingMore) at the start of fetchLogs, capture it in the
local scope before awaiting $fetch, and only update logs.value, cursor.value,
hasMore.value and total.value if the captured token matches the current token
(thus ignoring stale responses); also ensure loading/loadingMore flags and
cursor resets are set/cleared consistently (use finally) tied to that token so
only the active request clears the loading state.

In `@apps/shelve/app/composables/useScopeLabels.ts`:
- Around line 7-12: The module currently calls useState at import time (const
scopeLabels = useState(...)), which can run outside a Nuxt app/request context;
move the useState initialization inside the exported composable function
useScopeLabels so it runs per-call. Inside useScopeLabels, create scopeLabels
via useState<ScopeLabels | null>('scope-labels', () => null) and then keep the
existing async fetchScopeLabels logic to read/update scopeLabels.value and
return it; ensure any references to scopeLabels in fetchScopeLabels still
resolve to the locally created variable.

In `@apps/shelve/server/api/teams/`[slug]/audit-logs/index.get.ts:
- Line 21: The current code pushes lt(schema.auditLogs.id, cursor) into
conditions and then uses that same conditions array to compute total, causing
total to reflect “remaining rows” after the cursor; fix by separating the
pagination filter from the filter used to compute total: build a baseConditions
(copy of conditions before adding the cursor) and use that baseConditions when
calculating total, then only push lt(schema.auditLogs.id, cursor) into
conditions for the paginated query (affecting nextCursor/rows), so total remains
the count of rows matching active filters (not filtered by cursor); apply the
same change where total is computed (lines referenced around total and where
cursor is added) so total, cursor, conditions, and nextCursor behave correctly.

In `@apps/shelve/server/api/teams/`[slug]/environments/[id].delete.ts:
- Around line 15-26: The delete endpoint currently performs
db.delete(schema.environments)... then calls clearCache(...) and awaits
logAudit(...), but if logAudit throws the HTTP response fails after the delete
committed; make the audit write best-effort so the delete remains successful:
after awaiting db.delete(...) and clearCache('Environments', team.id), wrap the
await logAudit(event, {...}) call in a try/catch (or dispatch it without
awaiting) and on failure only record/log the audit error (e.g.,
processLogger.warn/error) and do not rethrow; keep the response for the deletion
successful. Ensure you reference the existing symbols
db.delete(schema.environments), clearCache('Environments', team.id), and
logAudit(event, {...}) when making the change.

In
`@apps/shelve/server/api/teams/`[slug]/projects/[projectId]/variables/[variableId]/index.delete.ts:
- Around line 10-29: The variable lookup is only by variableId which allows
deleting a variable from one project while auditing another; update the
existence check in db.query.variables.findFirst to also require the route
project (e.g., include eq(schema.variables.projectId, projectId)) or otherwise
verify existing.projectId === projectId, and then pass or enforce the project
context when calling VariablesService.deleteVariable (or adjust deleteVariable
to accept projectId) so the deletion and the logAudit
(ProjectsService.getProject, logAudit) refer to the same project.

In `@apps/shelve/server/api/user/scope-labels.get.ts`:
- Around line 1-13: The file imports only inArray from drizzle-orm but calls eq
in the DB query; update the import statement that currently reads "import {
inArray } from 'drizzle-orm'" to also include eq so eq is available for the call
in the default defineEventHandler (used in the db.query.members.findMany where
eq(schema.members.userId, user.id) is invoked); ensure other usages of
inArray/eq in this module still work after the import change and run a
build/test to verify no runtime ReferenceError.

In `@apps/shelve/server/services/audit-enrich.ts`:
- Around line 60-74: The project/environment enrichment queries
(db.query.projects.findMany and db.query.environments.findMany that use
inArray(schema.projects.id, [...ids.projectIds]) and
inArray(schema.environments.id, [...ids.environmentIds])) must also restrict
results to the current team to avoid cross-team label leakage; update both where
clauses to include a teamId equality predicate (e.g., teamId === currentTeamId
or schema.projects.teamId/ schema.environments.teamId equals the team id in
scope) so only rows matching the team are returned before mapping into
projects.set and environments.set.

In `@apps/shelve/server/services/audit.ts`:
- Around line 50-60: The current logAuditForTeams function performs sequential
awaits over uniqueTeamIds which adds latency; replace the for-loop with a
parallelized approach by using Promise.all over uniqueTeamIds.map to call
logAudit(event, { ...payload, teamId }) for each teamId, and preserve the
existing per-entry error swallowing/handling by attaching .catch(...) to each
mapped promise (or mapping to a wrapper that returns a resolved promise on
error) so one failed audit doesn't reject the whole Promise.all; keep the
function signature and use the same uniqueTeamIds de-duplication logic.

In `@apps/shelve/server/utils/audit-present.ts`:
- Around line 36-39: The fallback summaries are unreachable because `\`Updated
variable ${meta.key ?? ''}\`.trim()` still returns "Updated variable" when
meta.key is missing; update the switch cases for 'variables.update' and
'variables.delete' to choose between two explicit strings based on whether
meta.key is present (e.g., use a conditional on meta.key) so that when meta.key
is falsy you return the project-context fallback via projectLabel(meta.projectId
as number | undefined, maps, meta); apply the same pattern for the delete case.

In `@apps/shelve/test/e2e/flows/audit-logs.ts`:
- Around line 12-16: The test currently asserts the presence of the
'token.create' audit entry immediately after a fire-and-forget write (using
ctx.api to fetch AuditLogResponse.feed.logs), which can flake; modify the test
to perform bounded polling: repeatedly call
ctx.api(`/api/teams/${ctx.teamSlug}/audit-logs`) with a short delay (e.g.,
100–500ms) up to a configurable timeout or max attempts, check
feed.logs.some(entry => entry.action === 'token.create') on each attempt, and
only fail the test if the timeout/max attempts elapse without finding the entry;
keep the existing assertions for feed.logs being an array and the summary/actor
checks after the polling succeeds.

In `@packages/types/src/Audit.ts`:
- Around line 21-35: AUDIT_ACTIONS is missing the 'token.use' member present in
the AuditAction type; update the exported AUDIT_ACTIONS array to include
'token.use' so the list stays aligned with the AuditAction contract (locate the
AUDIT_ACTIONS constant in packages/types/src/Audit.ts and add the 'token.use'
entry).

---

Outside diff comments:
In
`@apps/shelve/server/api/teams/`[slug]/projects/[projectId]/variables/[variableId]/index.put.ts:
- Around line 27-49: The audit metadata is using the route projectId which can
diverge from the actual variable's project; fetch the variable's authoritative
project (e.g., call VariablesService.getVariable(variableId) or use the result
returned by VariablesService.updateVariable) and derive projectId/projectName
from that object instead of the route param, and additionally validate that the
variable's project matches the route projectId (or throw) before calling
logAudit so the audit always reflects the actual resource updated (refer to
VariablesService.updateVariable, getVariable, ProjectsService.getProject,
logAudit, variableId and projectId).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c2c71d61-2457-4881-bc35-25dddcb807a2

📥 Commits

Reviewing files that changed from the base of the PR and between 8b241b2 and 58f5af1.

📒 Files selected for processing (37)
  • .changeset/audit-log-ux-overhaul.md
  • apps/lp/content/docs/2.core-features/audit-logs.md
  • apps/shelve/app/components/audit/LogDetail.vue
  • apps/shelve/app/components/audit/LogEntry.vue
  • apps/shelve/app/components/audit/LogFeed.vue
  • apps/shelve/app/components/audit/LogFilters.vue
  • apps/shelve/app/components/token/Scopes.vue
  • apps/shelve/app/composables/useAuditLogs.ts
  • apps/shelve/app/composables/useScopeLabels.ts
  • apps/shelve/app/pages/[teamSlug]/team.vue
  • apps/shelve/app/pages/[teamSlug]/team/audit-logs.vue
  • apps/shelve/app/utils/userAgent.ts
  • apps/shelve/server/api/teams/[slug]/audit-logs/index.get.ts
  • apps/shelve/server/api/teams/[slug]/environments/[id].delete.ts
  • apps/shelve/server/api/teams/[slug]/environments/[id].put.ts
  • apps/shelve/server/api/teams/[slug]/environments/index.post.ts
  • apps/shelve/server/api/teams/[slug]/invitations/index.post.ts
  • apps/shelve/server/api/teams/[slug]/members/[id].delete.ts
  • apps/shelve/server/api/teams/[slug]/projects/[projectId]/index.delete.ts
  • apps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/[variableId]/index.delete.ts
  • apps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/[variableId]/index.put.ts
  • apps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/env/[envId].get.ts
  • apps/shelve/server/api/teams/[slug]/projects/[projectId]/variables/index.post.ts
  • apps/shelve/server/api/teams/[slug]/projects/index.post.ts
  • apps/shelve/server/api/tokens/[id].delete.ts
  • apps/shelve/server/api/tokens/index.post.ts
  • apps/shelve/server/api/user/scope-labels.get.ts
  • apps/shelve/server/middleware/2.auth.ts
  • apps/shelve/server/services/audit-enrich.ts
  • apps/shelve/server/services/audit.ts
  • apps/shelve/server/services/user.ts
  • apps/shelve/server/utils/audit-present.ts
  • apps/shelve/server/utils/userAgent.ts
  • apps/shelve/shared/types/auth.d.ts
  • apps/shelve/test/e2e/flows/audit-logs.ts
  • apps/shelve/test/unit/audit-enrich.test.ts
  • packages/types/src/Audit.ts

Comment thread apps/shelve/app/components/audit/LogDetail.vue
Comment thread apps/shelve/app/components/audit/LogEntry.vue Outdated
Comment thread apps/shelve/app/composables/useAuditLogs.ts
Comment thread apps/shelve/app/composables/useScopeLabels.ts Outdated
Comment thread apps/shelve/server/api/teams/[slug]/audit-logs/index.get.ts Outdated
Comment thread apps/shelve/server/services/audit-enrich.ts
Comment thread apps/shelve/server/services/audit.ts
Comment thread apps/shelve/server/utils/audit-present.ts Outdated
Comment thread apps/shelve/test/e2e/flows/audit-logs.ts Outdated
Comment thread packages/types/src/Audit.ts
Harden variable audit metadata, fix pagination total count, prevent stale fetches, scope enrichment to team, and improve a11y and E2E reliability.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant