> Documentation index: https://docs.mayur.live/llms.txt

# Admin Feedback Management

Query feedback lists, inspect sentiment metrics, retrieve audio playback URLs, and trigger manual reprocessing.

Query feedback lists, inspect sentiment metrics, retrieve audio playback URLs, and trigger manual reprocessing for Voiceflow recordings.

All admin endpoints are routed through `adminRouter` mounted under `/api/admin` and require authenticated requests with an `admin` role, enforced by `requireUser` and `requireAdmin`.

## Listing and Filtering Feedback

Admins can list feedback across all users with cursor-based pagination. Use `listFeedback` by sending a `GET` request to `/api/admin/feedback`.

### Query Parameters

You can provide the following query parameters:

- `cursor`: The string ID of the last feedback item from the previous page.
- `status`: Filter by feedback status (`UPLOADING`, `UPLOADED`, `PROCESSING`, `COMPLETED`, `FAILED`, `EXPIRED`).
- `sentiment`: Filter by analysis sentiment label.
- `language`: Filter by the transcription language code.
- `from`: Start timestamp to filter the `createdAt` date.
- `to`: End timestamp to filter the `createdAt` date.

### Response

The endpoint returns up to 20 items per page ordered newest first (`createdAt: "desc"`, `id: "desc"`), along with a `nextCursor` value:

```json
{
  "items": [
    {
      "id": "cm1abc...",
      "status": "COMPLETED",
      "errorCode": null,
      "durationMs": 14200,
      "createdAt": "2025-01-15T10:00:00.000Z",
      "user": {
        "name": "Alex Doe",
        "email": "alex@example.com"
      },
      "transcription": {
        "englishText": "The app is very intuitive and fast.",
        "languageCode": "en-IN"
      },
      "analysis": {
        "label": "positive",
        "score": 0.9,
        "urgent": false
      }
    }
  ],
  "nextCursor": "cm1xyz..."
}
```

If no additional pages exist, `nextCursor` returns `null`.

## Inspecting Feedback Details

To inspect the full details of a specific submission, use `getFeedback` by calling `GET /api/admin/feedback/:id`.

This endpoint returns the feedback record excluding the raw internal `audioPath`, and includes the related user (name and email), full transcription record, and sentiment analysis.

If the ID does not match any record, the server returns an HTTP 404 error:

```json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Recording not found"
  }
}
```

## Audio Playback URLs

To listen to an uploaded audio recording, call `getAudioUrl` using `GET /api/admin/feedback/:id/audio`.

The endpoint generates a short-lived signed S3 playback URL:

```json
{
  "url": "https://...",
  "expiresIn": 60
}
```

The signed URL is valid for 60 seconds.

:::warning
If the audio retention period has elapsed, if `audioDeletedAt` is set, or if the audio object no longer exists in storage, this endpoint returns a 404 response with code `AUDIO_REMOVED`:
```json
{
  "error": {
    "code": "AUDIO_REMOVED",
    "message": "Audio has been removed"
  }
}
```
:::

## Reprocessing Feedback

When a recording pipeline has completed or failed, admins can re-run processing via `reprocess` with a `POST` request to `/api/admin/feedback/:id/reprocess`.

1. The service checks that the recording is currently in either `FAILED` or `COMPLETED` status.
2. It resets the status to `UPLOADED`, increments the `attempt` counter, and clears `errorCode` and `completedAt`.
3. It emits a new Inngest event named `feedback/uploaded` keyed by `${id}-${attempt}` so the queue processes the new attempt.

Upon success, the server responds with status `202 Accepted`:

```json
{
  "status": "UPLOADED"
}
```

### Reprocessing Errors

- **409 INVALID_STATE**: The recording is not in `FAILED` or `COMPLETED` status (for example, if it is currently `PROCESSING` or `UPLOADING`).
- **404 NOT_FOUND**: No feedback record exists with the provided ID.
- **502 QUEUE_UNAVAILABLE**: The Inngest event could not be dispatched to the queue.

## Feedback Statistics

Admins can retrieve aggregate operational and sentiment metrics by calling `getStats` with `GET /api/admin/stats`.

The endpoint returns aggregated counts across feedback and analysis records:

```json
{
  "total": 120,
  "urgent": 8,
  "byStatus": [
    { "status": "COMPLETED", "count": 105 },
    { "status": "FAILED", "count": 15 }
  ],
  "bySentiment": [
    { "label": "positive", "count": 70 },
    { "label": "negative", "count": 35 }
  ],
  "byLanguage": [
    { "language": "en-IN", "count": 80 },
    { "language": "hi-IN", "count": 25 }
  ],
  "volume": [
    { "day": "2025-01-01T00:00:00.000Z", "count": 4 }
  ],
  "topics": [
    { "topic": "pricing", "count": 14 },
    { "topic": "checkout", "count": 9 }
  ]
}
```

- `urgent`: Total count of analysis entries flagged as `urgent: true`.
- `byStatus`: Counts grouped by submission status.
- `bySentiment`: Counts grouped by sentiment label.
- `byLanguage`: Counts grouped by transcription language code (defaults to `"unknown"` if missing).
- `volume`: Daily counts of submitted feedback over the last 30 days.
- `topics`: The top 8 most frequent topics extracted from sentiment analyses.
