# API de widgets de dashboards

> Lista, agrega, modifica, reordena y quita los widgets de un dashboard, y lee lo que muestra cada widget, a través de la API para integradores.

Un [dashboard](https://docs.probara.net/es/developers/api/dashboards/) contiene una cuadrícula de widgets. Cada widget tiene un ULID asignado por el servidor, un `type` de un catálogo cerrado, los campos de configuración de ese tipo, un `width`, un `height` (en todos los tipos salvo el título de sección), una `position` en la cuadrícula y, en todos los tipos salvo el título de sección, un alcance de proyectos.

Todo lo que describe esta página sigue las reglas del dashboard: el control por plan (`403 forbidden` sin un plan que incluya dashboards), `404 not_found` para un dashboard que no puedes ver y `403 forbidden` para un cambio de alguien que no creó el dashboard ni es propietario o administrador de la organización. Cualquiera que pueda ver un dashboard puede listar sus widgets.

## Catálogo

| `type`          | Campos de configuración                                                                                                                                                                                                      | `width` por defecto | `height` por defecto |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------- |
| `section_title` | Ninguno. `title` es obligatorio: el widget es un encabezado que divide la cuadrícula                                                                                                                                         | `full`              | Ninguno              |
| `count`         | `metric`: `test_cases`, `open_defects`, `active_runs`, `suites`, `flaky_cases`                                                                                                                                               | `third`             | `2`                  |
| `created_count` | `metric`: `test_cases`, `defects`, `results`, `runs`; `rangeDays`                                                                                                                                                            | `third`             | `2`                  |
| `distribution`  | `customFieldUlid`: un campo personalizado de casos de prueba; `chart`: `donut` (por defecto) o `bar`                                                                                                                         | `half`              | `4`                  |
| `time_series`   | `metric`: `open_defects`, `result_events`, `case_events`, `run_pass_rate`, `run_average_duration`; `rangeDays`; `chart`: `auto` (por defecto) o `heatmap`, solo para `result_events` y `case_events`, como máximo `365` días | `two_thirds`        | `4`                  |
| `pass_rate`     | `rangeDays`; `chart`: `gauge` (por defecto) o `bullet`; `target`: un entero de `1` a `100` (por defecto `80`), contra el que se mide la barra                                                                                | `third`             | `3`                  |
| `query`         | `savedQueryUlid`: la consulta guardada que dibuja, leída en vivo; `render`: `table`, `bar`, `line`, `donut` o `value` (por defecto `null`, la visualización de la propia consulta); sin `filters`                            | `half`              | `4`                  |

- `rangeDays` es `7`, `14`, `30`, `90`, `180`, `365` o `730` (por defecto `30`), contados hacia atrás desde el día en que se ve el widget. Cada widget guarda su propio rango, y todos los que ven el dashboard ven el mismo. Un `heatmap` admite `365` días como máximo.
- Un `distribution` agrupa los casos de prueba por un campo personalizado de tipo `select_single`, `radio`, `checkbox`, `select_multi` o `user_picker`, de sistema o creado por la organización. Un campo que no existe devuelve `404 not_found`; un campo de otro tipo, o un campo de defectos o de ejecuciones, devuelve `422 validation_failed`.
- `width` es `third`, `half`, `two_thirds` o `full`.
- `height` es la cantidad de filas de la cuadrícula que ocupa el widget, un entero de `2` a `8`. Un `section_title` no tiene altura y la rechaza.
- `title` admite hasta 200 caracteres. En todos los tipos salvo `section_title` es opcional: `null` muestra el título por defecto del tipo.

## Alcance de proyectos

`projects` define qué proyectos cuenta un widget:

- `{ "mode": "all" }` (por defecto): todos los proyectos que puede abrir quien ve el widget.
- `{ "mode": "selected", "projectUlids": [...] }`: de 1 a 50 proyectos indicados, sin repetir. Al modificar un widget, la lista puede estar vacía (ver más abajo).

Lo que cuenta un widget depende siempre de quién lo ve. Un miembro que no puede abrir un proyecto seleccionado (un proyecto privado al que no tiene acceso, un proyecto bloqueado o uno archivado) no lo ve en `projects.projectUlids`; `projects.hiddenProjectCount` le indica cuántos proyectos seleccionados quedan ocultos para él. Sus ULID nunca se devuelven.

Al agregar o modificar un widget, cada proyecto que indiques debe ser uno que puedas abrir; de lo contrario, `404 not_found` indica el ULID y no se guarda nada. Modificar un alcance `selected` reemplaza solo los proyectos que puedes abrir: los proyectos seleccionados que no puedes ver se mantienen como están. Por eso puedes enviar `projectUlids` vacío para quitar todos los proyectos que puedes abrir mientras queden proyectos ocultos; un alcance `selected` que se quedaría sin ningún proyecto devuelve `422 validation_failed`. Cambiar el alcance a `all` quita todos los proyectos seleccionados, incluidos los ocultos. Cuando se elimina un proyecto, sale de todos los widgets que lo seleccionaban.

## Filtros

Todos los tipos salvo `section_title` pueden tener `filters`, que acotan su número para todos los que ven el dashboard. Solo quienes pueden editar el dashboard los modifican.

```json
"filters": [
  { "field": "assignee", "values": ["01J9Z3Q4M8R2T6V1W5X7Y9A0C2", "01J9Z3Q4M8R2T6V1W5X7Y9A0C3"] },
  { "field": "custom_field", "customFieldUlid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BF", "values": ["01J9Z3Q4M8R2T6V1W5X7Y9A0C0"] }
]
```

- Como máximo 10 filtros, uno por `field` (uno por campo personalizado), cada uno con 1 a 50 valores, sin repetir.
- Los valores de un filtro se combinan con O; los distintos filtros, con Y. El ejemplo cuenta los defectos asignados a cualquiera de las dos personas **y** que tienen esa opción.

Los campos que ofrece un widget dependen de lo que cuenta su métrica:

| Cuenta          | Widgets                                                                                                            | `field`                                                                                      |
| --------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Casos de prueba | `count` `test_cases` y `flaky_cases`, `created_count` `test_cases`, `time_series` `case_events`, `distribution`    | `author`, `suite`, `tag`, `milestone`, `custom_field`                                        |
| Defectos        | `count` y `time_series` `open_defects`, `created_count` `defects`                                                  | `reporter`, `assignee`, `milestone`, `tag`, `custom_field`; `status` solo en `created_count` |
| Ejecuciones     | `count` `active_runs`, `created_count` `runs`, `time_series` `run_pass_rate` y `run_average_duration`, `pass_rate` | `author`, `environment`, `milestone`, `test_plan`, `tag`, `custom_field`                     |
| Resultados      | `created_count` `results`, `time_series` `result_events`                                                           | `status`, `executed_by`, `environment`, `milestone`                                          |

Un widget `count` `suites` no acepta filtros. El entorno y el hito de un resultado son los de su ejecución. Un filtro `suite` también incluye los casos de las suites que están debajo.

Qué es cada valor:

- `author`, `reporter`, `assignee`, `executed_by`: ULID de miembros.
- `suite`, `tag`, `milestone`, `environment`, `test_plan`: sus ULID. Las suites, los hitos, los entornos y los planes de prueba deben estar en proyectos que puedas abrir.
- `status`: `open`, `in_progress`, `resolved` o `closed` para defectos; `passed`, `failed`, `skipped` o `blocked` para resultados.
- `custom_field`: `customFieldUlid` indica un campo de la entidad contada (caso de prueba, defecto o ejecución) de tipo `select_single`, `radio`, `checkbox`, `select_multi` o `user_picker`. Los valores son ULID de opciones, ULID de miembros para `user_picker`, o `"true"` / `"false"` para `checkbox`. `"false"` coincide solo con los elementos que guardan un valor sin marcar.

Un filtro que la métrica no ofrece, o un valor que no indica nada que puedas usar (un miembro, etiqueta, campo u opción que no existe, o una suite, hito, entorno o plan de prueba de un proyecto que no puedes abrir), devuelve `422 validation_failed`; `details` indica la ruta de cada valor rechazado y no se guarda nada. Si más adelante se elimina una entidad filtrada, su valor se mantiene en el widget, con etiqueta `null`, y no coincide con nada hasta que alguien que edita el dashboard lo quite.

Los filtros se guardan, y se devuelven, en un orden fijo y con sus valores ordenados, así que enviar los mismos filtros en otro orden no cambia nada.

## Listar

`GET /api/v1/dashboards/{dashboardUlid}/widgets`

`200` con `{ "items": [...] }`, todos los widgets por `position`. Un dashboard tiene como máximo 50 widgets, por lo que la lista no se pagina.

## Agregar

`POST /api/v1/dashboards/{dashboardUlid}/widgets`

Agrega un widget después del último. El cuerpo lleva `type`, los campos de configuración del tipo y, de forma opcional, `title`, `width`, `height`, `projects` y `filters` (los tres últimos no van en un `section_title`). El cuerpo es estricto: un campo que el tipo no tiene devuelve `422 validation_failed`. Un dashboard que ya tiene 50 widgets devuelve `409 conflict`.

`201` con el widget. Consulta [Campos de la respuesta](#campos-de-la-respuesta).

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"time_series","metric":"run_pass_rate","rangeDays":90,"projects":{"mode":"selected","projectUlids":["01J9Z3Q4M8R2T6V1W5X7Y9A0BC"]}}' \
  "https://app.probara.net/api/v1/dashboards/$DASHBOARD_ULID/widgets"
```

## Modificar

`PATCH /api/v1/dashboards/{dashboardUlid}/widgets/{widgetUlid}`

Envía al menos uno de `title`, `width`, `height`, `projects`, `metric`, `rangeDays`, `customFieldUlid`, `chart`, `target` o `filters`. El `type` de un widget no cambia: quítalo y agrega otro. Un campo que el tipo del widget no tiene, o un valor que el tipo no acepta (por ejemplo, una métrica de `count` en un `time_series`), devuelve `422 validation_failed`. Un título de sección conserva su título (`null` se rechaza) y no acepta `height` ni `projects`. Un cuerpo que no cambia nada devuelve el widget sin modificar.

`filters` reemplaza la lista completa; `[]` quita todos los filtros. Los valores que el widget ya guarda se mantienen aunque su entidad se haya eliminado; los valores nuevos se validan. Cambiar `metric` mientras el widget tiene un filtro que la nueva métrica no ofrece devuelve `422 validation_failed`, salvo que el mismo cuerpo envíe `filters`.

`200` con el widget.

## Reordenar

`PUT /api/v1/dashboards/{dashboardUlid}/widgets/order`

Envía `{ "widgetUlids": [...] }` con cada widget del dashboard exactamente una vez, en el nuevo orden; cualquier otra lista devuelve `422 validation_failed`. `200` con los widgets en su nuevo orden.

## Quitar

`DELETE /api/v1/dashboards/{dashboardUlid}/widgets/{widgetUlid}`

`204`. Los widgets que estaban después suben una posición. Eliminar un dashboard quita sus widgets.

## Widgets de consulta

Un widget `query` dibuja una consulta guardada en vivo, así que una edición de la consulta llega al widget. Crea uno con `"type":"query"`, `savedQueryUlid` (una consulta guardada que puedas ver) y, opcionalmente, `render` para reemplazar la visualización de la propia consulta (`null`, por defecto, la conserva). Recibe `width`, `height` y `projects` como cualquier widget de datos, y no recibe `filters`: el texto de la consulta decide sus filas. Una modificación puede apuntarlo a otra consulta (`savedQueryUlid`) o cambiar su `render`. Un widget cuya consulta guardada se eliminó conserva su lugar con `savedQueryUlid` en `null`; un editor lo quita.

Sus datos son la consulta guardada ejecutada con tu propio acceso a los proyectos y limitada a los proyectos del widget: `savedQueryUlid`, `savedQueryName`, `queryDeleted`, `canOpenQuery` (si puedes abrir la consulta, que es cuando aplica un enlace a ella), `render` (el dibujo en uso), `scope` (los mismos conteos que reporta cualquier widget de datos) y `result`, la primera página de la respuesta de la consulta en la forma que responde el [endpoint de consultas](https://docs.probara.net/es/developers/api/pql/): filas, o grupos cuando la consulta agrega. Quien puede ver el dashboard ve esa página aunque la consulta sea privada para otra persona, sin su texto. Una consulta eliminada responde `queryDeleted: true` con `savedQueryUlid`, `savedQueryName`, `render` y `result` en `null`.

Los registros de un widget `query` no se listan aquí: sus filas se leen en su consulta guardada, así que el endpoint de registros responde `422 validation_failed`.

## Datos de un widget

`GET /api/v1/dashboards/{dashboardUlid}/widgets/{widgetUlid}/data`

Devuelve lo que muestra un widget. Cualquiera que pueda ver el dashboard puede leerlo, y los números siempre se calculan para **ti**: solo cubren los proyectos del alcance del widget que puedes abrir. Los propietarios y administradores de la organización abren todos los proyectos que no están bloqueados. Los proyectos bloqueados y archivados nunca se cuentan.

```json
{
  "type": "created_count",
  "widgetUlid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BD",
  "metric": "defects",
  "scope": { "includedProjectCount": 3, "hiddenProjectCount": 1, "emptySelection": false },
  "range": {
    "requestedDays": 365,
    "effectiveDays": 90,
    "from": 1751976000000,
    "to": 1759752000000,
    "clampedByRetention": true
  },
  "data": { "value": 42 }
}
```

`scope` indica si el número está completo:

- `includedProjectCount`: cuántos proyectos se contaron.
- `hiddenProjectCount`: cuántos quedaron fuera. En un widget `selected`, los proyectos seleccionados que no puedes abrir (el mismo número que `projects.hiddenProjectCount` en el widget). En un widget `all`, los proyectos de la organización que no están archivados y que no puedes abrir, incluidos los bloqueados. Si es mayor que 0, el valor es parcial: muéstralo como tal, no como el total.
- `emptySelection`: `true` en un widget `selected` cuyos proyectos se eliminaron todos. Su valor es `0`.

Qué cuenta cada métrica:

| `type`          | `metric`       | `data.value`                                                                 |
| --------------- | -------------- | ---------------------------------------------------------------------------- |
| `count`         | `test_cases`   | Casos de prueba no archivados                                                |
| `count`         | `open_defects` | Defectos con estado `open` o `in_progress`                                   |
| `count`         | `active_runs`  | Ejecuciones que no están cerradas                                            |
| `count`         | `suites`       | Suites                                                                       |
| `count`         | `flaky_cases`  | Casos de prueba no archivados con **Is flaky** marcado                       |
| `created_count` | `test_cases`   | Casos de prueba creados en el rango, incluidos los que se archivaron después |
| `created_count` | `defects`      | Defectos creados en el rango                                                 |
| `created_count` | `results`      | Resultados registrados en el rango, según su hora de ejecución               |
| `created_count` | `runs`         | Ejecuciones iniciadas en el rango                                            |

Un widget `created_count` también devuelve `range`, la ventana que contó: desde `from` (incluido) hasta `to` (excluido), en milisegundos desde epoch, terminando en el momento de la solicitud. La ventana es el `rangeDays` del widget, recortado a la retención del historial de tu plan: las ejecuciones cerradas más antiguas se eliminan, así que una ventana más larga contaría de menos. `effectiveDays` es la duración que realmente se contó, y `clampedByRetention` es `true` cuando la retención la recortó.

### Distribución

Un widget `distribution` cuenta los casos de prueba del alcance que no están archivados y pertenecen a un proyecto donde se aplica su campo, agrupados por el valor que tienen en ese campo.

```json
{
  "type": "distribution",
  "widgetUlid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BE",
  "customFieldUlid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BF",
  "scope": { "includedProjectCount": 2, "hiddenProjectCount": 0, "emptySelection": false },
  "data": {
    "fieldRemoved": false,
    "field": {
      "ulid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BF",
      "title": "Prioridad",
      "type": "select_single"
    },
    "multiValue": false,
    "totalCases": 120,
    "buckets": [
      {
        "kind": "option",
        "key": "01J9Z3Q4M8R2T6V1W5X7Y9A0C0",
        "label": "Alta",
        "color": "#a890e8",
        "count": 30
      },
      {
        "kind": "option",
        "key": "01J9Z3Q4M8R2T6V1W5X7Y9A0C1",
        "label": "Baja",
        "color": "#a89fb2",
        "count": 75
      },
      { "kind": "not_set", "key": null, "count": 15 }
    ]
  }
}
```

Cada grupo tiene un `kind`:

| `kind`     | `key`             | Notas                                                                                                                                                              |
| ---------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `option`   | ULID de la opción | Campos de selección y de opción única. Se listan todas las opciones en el orden del campo, incluidas las vacías. `label` es el nombre en el idioma de la solicitud |
| `checkbox` | `true`/`false`    | Marcado y sin marcar                                                                                                                                               |
| `user`     | ULID del usuario  | Campos de usuario, de mayor a menor. `formerMember` es `true` cuando la persona dejó la organización; `label` es `null` cuando la organización ya no la conoce     |
| `other`    | `other`           | A partir de 20 grupos, los más pequeños se juntan en este; `groupedCount` indica cuántos                                                                           |
| `not_set`  | `null`            | Siempre al final: casos sin valor, o cuyo valor apunta a una opción eliminada                                                                                      |

`totalCases` es la cantidad de casos contados. En un campo de selección múltiple, `multiValue` es `true`: un caso cuenta una vez en cada opción que tiene, así que la suma de los grupos puede superar `totalCases`.

Si el campo se eliminó después de agregar el widget, la respuesta sigue siendo `200`, con `data` igual a `{ "fieldRemoved": true, "buckets": [] }`.

### Serie temporal

Un widget `pass_rate` devuelve `range` y un único objeto `data`. `value` es la tasa de aprobación agregada de las ejecuciones cerradas en el rango, en porcentaje con un decimal: `passed` sobre `executed`, es decir, los casos aprobados, fallidos, bloqueados u omitidos. Se excluyen las ejecuciones abortadas, y `value` es `null` cuando no se ejecutó nada. `closedRuns` cuenta las ejecuciones que agrupa, y sus registros listan esas mismas ejecuciones:

```json
{
  "type": "pass_rate",
  "widgetUlid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BC",
  "scope": { "includedProjectCount": 2, "hiddenProjectCount": 0, "emptySelection": false },
  "range": {
    "requestedDays": 30,
    "effectiveDays": 30,
    "from": 1756742400000,
    "to": 1759334400000,
    "clampedByRetention": false
  },
  "data": { "value": 83.3, "passed": 10, "executed": 12, "closedRuns": 2 }
}
```

Un widget `time_series` devuelve `range`, recortado a la retención de tu plan igual que un widget `created_count`, dividido en intervalos:

```json
{
  "type": "time_series",
  "widgetUlid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BG",
  "metric": "run_pass_rate",
  "scope": { "includedProjectCount": 2, "hiddenProjectCount": 0, "emptySelection": false },
  "range": {
    "requestedDays": 7,
    "effectiveDays": 7,
    "from": 1759147200000,
    "to": 1759752000000,
    "clampedByRetention": false
  },
  "data": {
    "granularity": "day",
    "timeZone": "America/Argentina/Buenos_Aires",
    "buckets": [
      { "start": 1759147200000, "end": 1759201200000 },
      { "start": 1759201200000, "end": 1759287600000 }
    ],
    "series": [{ "key": "passRate", "values": [null, 83.3] }]
  }
}
```

(Abreviado: un rango de 7 días tiene 8 intervalos.)

- `granularity` es `day` para rangos de hasta 31 días, `week` hasta 186 días (las semanas empiezan el lunes) y `month` para rangos más largos. Un `heatmap` es siempre `day`.
- Los límites de los intervalos caen a la medianoche de `timeZone`: la zona horaria de **tu** perfil, o `UTC` si no elegiste ninguna. Dos personas pueden ver límites distintos en el mismo widget. Cuando cambia el horario de verano, un intervalo puede durar una hora más o una hora menos.
- `buckets` son ventanas `[start, end)` contiguas, en milisegundos desde epoch; la primera empieza en `range.from` y la última termina en `range.to`, así que ambas pueden ser parciales.
- `series` tiene un valor por intervalo, en el mismo orden, para cada serie de la métrica:

| `metric`               | Series                                   | Valor por intervalo                                                                                                                                                                                |
| ---------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open_defects`         | `open`                                   | Defectos abiertos (`open` o `in_progress`) al final del intervalo. Los defectos eliminados nunca se cuentan                                                                                        |
| `result_events`        | `passed`, `failed`, `skipped`, `blocked` | Resultados registrados en el intervalo con ese estado, según su hora de ejecución                                                                                                                  |
| `case_events`          | `created`, `edited`                      | Casos de prueba creados, y ediciones guardadas: un guardado, una importación o una restauración. Los cambios hechos a través de un paso compartido no cuentan como ediciones                       |
| `run_pass_rate`        | `passRate`                               | De las ejecuciones cerradas en el intervalo (sin las abortadas), casos aprobados ÷ casos ejecutados, como porcentaje con un decimal. `null` si no se ejecutó nada                                  |
| `run_average_duration` | `averageDurationMs`                      | Tiempo promedio entre el inicio y el cierre de las ejecuciones cerradas en el intervalo (sin las abortadas; una ejecución cerrada antes de su inicio cuenta como 0). `null` si no se cerró ninguna |

La tasa de aprobación suma los casos de todas las ejecuciones del intervalo antes de dividir: una ejecución con 100 casos ejecutados pesa más que una con 2. Los casos ejecutados son los aprobados, fallidos, bloqueados y omitidos; no cuentan los casos sin probar ni los que esperan volver a probarse.

### Errores

Un `section_title` no tiene datos y devuelve `422 validation_failed`. Un widget que no existe, o un dashboard que no puedes ver, devuelve `404 not_found`.

## Campos de la respuesta

| Campo                         | Tipo           | Notas                                                                  |
| ----------------------------- | -------------- | ---------------------------------------------------------------------- |
| `ulid`                        | string         | El ULID del widget                                                     |
| `type`                        | string         | Consulta [Catálogo](#catálogo)                                         |
| `title`                       | string \| null | Siempre es un string en un `section_title`                             |
| `width`                       | string         | `third`, `half`, `two_thirds` o `full`                                 |
| `height`                      | number         | Filas de la cuadrícula, de `2` a `8`; no aparece en un `section_title` |
| `position`                    | number         | Lugar en la cuadrícula, desde 0                                        |
| campos de configuración       | —              | Los del tipo del widget, por ejemplo `metric` y `rangeDays`            |
| `projects`                    | object \| null | `null` en un `section_title`                                           |
| `projects.mode`               | string         | `all` o `selected`                                                     |
| `projects.projectUlids`       | string[]       | Los proyectos seleccionados que **tú** puedes abrir; vacío con `all`   |
| `projects.hiddenProjectCount` | number         | Proyectos seleccionados que no puedes abrir                            |
| `filters`                     | object[]       | No aparece en un `section_title`. Consulta [Filtros](#filtros)         |
| `filters[].values[].label`    | string \| null | Ver más abajo                                                          |
| `createdAt`, `updatedAt`      | number         | Milisegundos desde epoch; reordenar no cambia `updatedAt`              |

Cada filtro de una respuesta repite `field` (y `customFieldUlid`, además de `customFieldTitle` y `customFieldType`, ambos `null` cuando el campo se eliminó) y lista sus valores como `{ "value", "label" }`. `label` es el nombre del miembro, o el nombre de la suite, etiqueta, hito, entorno, plan de prueba u opción; los títulos de campos personalizados y los nombres de opciones siguen el idioma de la solicitud. Es `null` para un estado o un valor de casilla, para una entidad eliminada y para una entidad de un proyecto que no puedes abrir.

Cada cambio en un widget también actualiza el `updatedAt` del dashboard.

## Registros de un widget

`GET /api/v1/dashboards/{dashboardUlid}/widgets/{widgetUlid}/records`

Lista los registros detrás de un valor de un widget, de a una página por vez: los casos, defectos, ejecuciones, resultados o suites que cuenta el número. Sigue las mismas reglas que los datos del widget. Cualquiera que pueda ver el dashboard puede leerlos, y solo se listan los proyectos del alcance del widget que puedes abrir. El `total` es el valor que muestran los datos del widget para el mismo selector. En `run_pass_rate` y `run_average_duration`, `total` es la cantidad de ejecuciones detrás del punto, no la tasa de aprobación ni el promedio graficado.

Elige el valor con el selector que admite el widget:

| Widget                   | Selector                                                                                                                                                                                 | Lista                                         |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| `count`, `created_count` | ninguno: el valor completo                                                                                                                                                               | los registros que cuenta la métrica           |
| `distribution`           | `bucket`: una `key` de `data.buckets` (el ULID de una opción o de una persona, `true`, `false`, `not_set` u `other`)                                                                     | los casos de ese grupo                        |
| `time_series`            | `bucketStart`: un `start` de `data.buckets`, y `series` cuando la métrica tiene varias (`result_events`: `passed`, `failed`, `skipped` o `blocked`; `case_events`: `created` o `edited`) | los registros de ese intervalo y de esa serie |

En `time_series`, una métrica con una sola serie admite su propio nombre de serie, de forma opcional: `open_defects` admite `open`, `run_pass_rate` admite `passRate` y `run_average_duration` admite `averageDurationMs`. La serie se verifica con la métrica y no cambia nada; cualquier otra serie devuelve `422 validation_failed`. En un widget `heatmap`, `bucketStart` nombra un día local, y `series` sigue las mismas reglas. Una celda cuenta todas las series juntas, así que los registros de una serie son solo una parte del recuento de ese día; la aplicación web no abre los registros de una celda.

```
GET /api/v1/dashboards/01J9Z3Q4M8R2T6V1W5X7Y9A0BD/widgets/01J9Z3Q4M8R2T6V1W5X7Y9A0BG/records?bucket=not_set&page=1&pageSize=50
```

```json
{
  "kind": "test_case",
  "total": 2,
  "page": 1,
  "pageSize": 50,
  "items": [
    {
      "kind": "test_case",
      "projectUlid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BK",
      "projectCode": "PAY",
      "projectName": "Payments",
      "ulid": "01J9Z3Q4M8R2T6V1W5X7Y9A0BM",
      "displayId": "PAY-41",
      "title": "Reembolsar una tarjeta",
      "suiteName": "Compra",
      "archived": false,
      "createdAt": 1759600000000,
      "revision": null
    }
  ]
}
```

- `kind` es el tipo de cada elemento: `test_case`, `defect`, `run`, `result` o `suite`.
- Cada elemento trae lo que una fila necesita para mostrar y enlazar: su proyecto (`projectUlid`, `projectCode`, `projectName`), su `ulid` y su `displayId`. El identificador visible de un caso es el código de su proyecto y su número (`PAY-41`); el de un defecto es `D-` y su número; el de una ejecución es `R-` y su número; el de una suite es el código de su proyecto, `S` y su número (`PAY-S4`).
- Los elementos van del más reciente al más antiguo según la marca de tiempo que usa la métrica; si empatan, por ID. Las páginas empiezan en 1; `pageSize` vale 50 por defecto y como máximo 200.

Los campos según el tipo:

| `kind`      | Campos                                                                                                                                                                                                                                                                                                                                  |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test_case` | `title`, `suiteName`, `archived`, `createdAt`, y `revision` (`number`, `source`, `savedAt`) en un registro `edited` de `case_events`; en los demás es `null`                                                                                                                                                                            |
| `defect`    | `title`, `status` (el estado actual: un defecto que estaba abierto en el pasado pudo cerrarse después), `createdAt`, `closedAt`                                                                                                                                                                                                         |
| `run`       | `name`, `state`, `status`, `startedAt`, `closedAt`, `passed`, `executed`, `passRate` (`null` si no se ejecutó nada), `durationMs` (`null` mientras la ejecución está abierta)                                                                                                                                                           |
| `result`    | `status`, `executedAt`, `executedByName` (`null` si la organización ya no conoce a quien lo ejecutó), `runUlid`, `runDisplayId`, `runCaseUlid` (`null` si el caso ya no está en la ejecución), `testCaseUlid`, `testCaseDisplayId`, `testCaseTitle` (el título con el que se ejecutó; los campos del caso son `null` una vez eliminado) |
| `suite`     | `name`, `createdAt`                                                                                                                                                                                                                                                                                                                     |

Un porcentaje de aprobación es la suma de `passed` de los elementos dividida entre la suma de `executed`, tal como lo calcula la serie. Un tiempo promedio es la media de `durationMs` de los elementos.

Un `bucketStart` identifica el intervalo del día, la semana o el mes local en el que cae. El primer intervalo empieza al inicio del rango, que se mueve con el tiempo, así que un inicio tomado de una respuesta anterior sigue encontrándolo. En una distribución de `select_multi`, un caso que tiene dos opciones de `other` es un solo registro, igual que en el conteo del grupo.

Errores:

- `422 validation_failed` cuando el selector no corresponde al widget (un `bucket` en un `count`, o una serie temporal sin `bucketStart`), y para un `section_title`, que no tiene registros.
- `404 not_found` cuando el grupo no es uno que muestre el widget (una opción agrupada en `other`), cuando el intervalo no es uno que muestre (un `bucketStart` anterior al rango), y para un widget o un dashboard que no puedes ver.
- `403 forbidden` cuando el plan no incluye dashboards.

Solo lectura: no se registra ningún evento de auditoría.

## Eventos de auditoría

Cada cambio registra un evento sobre el dashboard (tipo de entidad `dashboard`, con su nombre como etiqueta) con el `widgetUlid` y el `type` del widget en la metadata: `dashboard.widget_added`, `dashboard.widget_updated` (además de `changedFields`, los nombres de los campos que cambiaron, como `title`, `width`, `height`, `projects`, `rangeDays` o `filters`) y `dashboard.widget_removed`. Un reordenamiento registra `dashboard.widgets_reordered` con `widgetUlids` en su nuevo orden. Un cambio que no cambia nada no registra nada. Consulta [Eventos de auditoría](https://docs.probara.net/es/developers/api/audit-events/).

## MCP

Las mismas operaciones están disponibles como las herramientas MCP `list_dashboard_widgets`, `create_dashboard_widget`, `update_dashboard_widget`, `reorder_dashboard_widgets`, `delete_dashboard_widget`, `get_dashboard_widget_data` y `list_dashboard_widget_records`, con las mismas reglas. Consulta la [referencia de herramientas MCP](https://docs.probara.net/es/developers/mcp-tools/).
