---
id: w4/m14/t002
title: "`useAuditLog` hook: query + cursor-based \"load more\" + 403/503 state handling"
worker: worker4
status: done
estimate: 45m
depends_on: [w4/m14/t001]
---

## Objective

A `useAuditLog(workspaceId)` hook that fetches a workspace's audit trail, exposes "load more" (appending, not replacing), and distinguishes the three non-happy states the backend actually produces: forbidden (non-admin), unavailable (store-less, 503), and a generic error.

## Context

- `internal/audit.Service.List` (`lego/backend/internal/audit/service.go`): 403 for a non-`can_manage` caller, `core.ErrAuditUnavailable` (503) when the control-plane store isn't wired, else the event list.
- Precedent for the accumulate-on-load-more + distinct-error-state shape: `features/team/hooks/use-team.ts` (`canManage` derived from whether the admin-only `workspaceInvites` query 403s, not a separate flag) — mirror that pattern rather than inventing a new one: a 403 on `auditLogs` means "hide the panel", not "show an error".
- `useCurrentWorkspace` (`features/team/hooks/use-current-workspace.ts`) is the existing source of the caller's current `workspaceId` — reuse it, don't re-fetch.

## Steps

1. Create `dashboard/src/features/audit/hooks/use-audit-log.ts`: Apollo query hook over `AuditLogs`, `ownerId: workspaceId`.
2. State: `events` (accumulated array), `loading` (initial), `loadingMore`, `error` (non-403/503 failures), `unavailable` (503), `forbidden` (403), `hasMore` (derived from whether the last page returned a full `limit`-sized batch).
3. `loadMore()`: re-queries with `cursor` = last event's `id`, appends results.
4. Map GraphQL errors to `forbidden`/`unavailable`/`error` by inspecting the error extension/status the Apollo error carries (match how `use-team.ts` or `use-api-keys.ts` already do this — reuse the existing error-classification helper if one exists under `common/`, don't duplicate it).

## Files

- `dashboard/src/features/audit/hooks/use-audit-log.ts` (new)
- `dashboard/src/features/audit/types.ts` (new — `AuditEvent` shape)

## Acceptance criteria

- [ ] Initial load populates `events`; `loadMore()` appends without duplicating or dropping rows.
- [ ] A 403 response sets `forbidden: true` and no `error`.
- [ ] A 503 (`ErrAuditUnavailable`) response sets `unavailable: true` and no `error`.
- [ ] Any other failure sets `error`.

## Out of scope

- Rendering (t004, t005).
