# Capability: metabase

Operar Metabase 100% via API REST. Cubre: auth (`metabase_auth` con email/password, `metabase_client_from_pass` cargando la credencial desde `pass` — sesión o API-key), CRUD de cards/dashboards/collections/snippets/permissions/databases, ejecucion de queries (`metabase_execute_card`, `metabase_query`), refresh metadata + result_metadata, listado y archivado, gestion de pulses, y composiciones (`init_metabase`, `setup_metabase_volume`). 108 funciones Go+Py. Cliente reutilizable: `MetabaseClient` (Go: `metabase_client_go_infra`; Py: `MetabaseClient_py_infra`) — el cliente Py detecta el prefijo `mb_` y autentica por header `X-API-KEY`.

## Funciones

| ID | Firma | Que hace |
|---|---|---|
| `init_metabase_go_infra` | `func main() — Despliega stack Metabase + Postgres en Docker` | Pipeline que inicializa un contenedor Metabase con su base de datos Postgres. Crea red Docker, pull de imágenes, inicia Postgres con volume persistente, espera health check y lanza Metabase conectado. |
| `metabase_add_dashboard_filter_py_infra` | `def metabase_add_dashboard_filter(client: MetabaseClient, dashboard_id: int, name: str, slug: str, filter_type: str, card_mappings: list[dict], *, default: str \| None = None) -> dict` | Añade un filtro a un dashboard de Metabase y lo conecta a las cards indicadas. Crea el parameter a nivel de dashboard y actualiza los parameter_mappings de cada dashcard. |
| `metabase_add_database_py_infra` | `def metabase_add_database(client: MetabaseClient, name: str, engine: str, details: dict) -> dict` | Agrega una nueva database a Metabase via POST /api/database. Soporta cualquier engine (sqlite, postgres, mysql, etc.). |
| `metabase_add_membership_py_infra` | `def metabase_add_membership(client: MetabaseClient, user_id: int, group_id: int, is_group_manager: bool = False) -> list[dict]` | Añade un usuario a un Permission Group de Metabase. Endpoint: POST /api/permissions/membership. Retorna la lista completa de membresías del grupo tras la operación. |
| `metabase_add_ops_db_py_pipelines` | `metabase_add_ops_db(app_name: str) -> None` | Registra la operations.db de una app en Metabase como database SQLite. Verifica duplicados y muestra el mount necesario para el contenedor Docker. |
| `metabase_archive_document_py_infra` | `def metabase_archive_document(client: MetabaseClient, document_id: int) -> dict` | Archiva un document (PUT archived=True). Prerequisito para poder eliminarlo. |
| `metabase_archive_snippet_py_infra` | `def metabase_archive_snippet(client: MetabaseClient, snippet_id: int) -> dict` | Archiva un SQL snippet en Metabase. Wrapper sobre metabase_update_snippet con archived=True. |
| `metabase_auth_go_infra` | `func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error)` | Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias (configurable con MAX_SESSION_AGE en Metabase). Endpoint: POST /api/session. |
| `metabase_auth_py_infra` | `def metabase_auth(base_url: str, email: str, password: str) -> MetabaseClient` | Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias. Endpoint: POST /api/session. |
| `metabase_client_from_pass_py_infra` | `def metabase_client_from_pass(pass_key: str, base_url: str, mode: str = 'auto') -> MetabaseClient \| dict` | Construye un `MetabaseClient` autenticado leyendo la credencial desde `pass`. `mode='session'` (secreto multi-línea: L1 password, línea `email:`) usa `metabase_auth`; `mode='api_key'` (secreto de una línea tipo `mb_...`) autentica por header; `mode='auto'` detecta por la forma del secreto. Compone `pass_get_secret` + `parse_metabase_secret` + `metabase_auth`. Devuelve el cliente o `{status:'error', error}` sin lanzar. Cubre Aurgi (API-key) y captación (sesión) sin reescribir la carga de credenciales. |
| `parse_metabase_secret_py_infra` | `def parse_metabase_secret(secret_text: str, mode: str = 'auto') -> dict` | Núcleo **puro** y testeable de `metabase_client_from_pass`: parsea el texto del secreto de `pass` y devuelve `{mode, email, password}` (sesión) o `{mode, api_key}` (API-key). `mode='auto'` clasifica: una sola línea sin `email:`/`login:` → api_key; multi-línea con email → session. Sin I/O. |
| `metabase_copy_card_py_infra` | `def metabase_copy_card(client: MetabaseClient, card_id: int, name: str \| None = None, collection_id: int \| None = None, description: str \| None = None) -> dict` | Crea una copia de una card/pregunta en Metabase via el endpoint nativo POST /api/card/:id/copy. Permite sobrescribir nombre, coleccion y descripcion en la copia. |
| `metabase_copy_dashboard_py_infra` | `def metabase_copy_dashboard(client: MetabaseClient, dashboard_id: int, name: str \| None = None, collection_id: int \| None = None, description: str \| None = None, is_deep_copy: bool = False) -> dict` | Crea una copia de un dashboard en Metabase via POST /api/dashboard/:id/copy. Con is_deep_copy=True tambien clona las cards referenciadas. |
| `metabase_copy_dashcard_mappings_py_infra` | `def metabase_copy_dashcard_mappings(client: MetabaseClient, *, dashboard_id: int, source_card_id: int, dest_card_id: int) -> list[dict]` | Copia los parameter_mappings del primer dashcard con source_card_id al card destino (dest_card_id), devolviendo una lista nueva de mappings sin mutar el original. Util para replicar filtros de dashboard a cards nuevas sin copiar manualmente cada mapping. |
| `metabase_copy_document_py_infra` | `def metabase_copy_document(client: MetabaseClient, document_id: int, name: str \| None = None, collection_id: int \| None = None) -> dict` | Copia un document usando el endpoint nativo POST /api/document/:id/copy. Un solo request HTTP. |
| `metabase_create_card_alert_py_infra` | `def metabase_create_card_alert(client: MetabaseClient, card_id: int, cron_schedule: str, recipients: list[dict], send_condition: str = 'has_result', send_once: bool = False) -> dict` | Crea una alerta sobre los resultados de una card en Metabase. Envia email segun cron cuando la card cumple la condicion (has_result, goal_above, goal_below). Endpoint: POST /api/notification. |
| `metabase_create_card_go_infra` | `func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error)` | Crea una nueva card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card. |
| `metabase_create_card_py_infra` | `def metabase_create_card(client: MetabaseClient, name: str, dataset_query: dict, display: str = 'table', collection_id: int = 0, description: str = '') -> dict` | Crea una card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card. |
| `metabase_create_card_raw_py_infra` | `def metabase_create_card_raw(client: MetabaseClient, payload: dict) -> dict` | Crea una card en Metabase enviando el payload completo sin modificaciones. Version raw para flujos Metabase-as-code donde el caller construye el body entero. Endpoint: POST /api/card. |
| `metabase_create_dashboard_go_infra` | `func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error)` | Crea un nuevo dashboard vacio en Metabase. Para agregar cards usar MetabaseUpdateDashboard con el campo dashcards. Endpoint: POST /api/dashboard. |
| `metabase_create_dashboard_py_infra` | `def metabase_create_dashboard(client: MetabaseClient, name: str, description: str = '', collection_id: int = 0) -> dict` | Crea dashboard vacio en Metabase. Para agregar cards usar metabase_update_dashboard con dashcards. Endpoint: POST /api/dashboard. |
| `metabase_create_dashboard_raw_py_infra` | `def metabase_create_dashboard_raw(client: MetabaseClient, payload: dict) -> dict` | Crea un dashboard en Metabase enviando el payload completo sin modificaciones. Maneja automaticamente la limitacion de la API que no acepta dashcards en el POST inicial: si el payload contiene dashcards, hace POST para crear el dashboard y luego PUT para añadir las cards. Endpoint: POST /api/dashboard (+ PUT condicional). |
| `metabase_create_dashboard_subscription_py_infra` | `def metabase_create_dashboard_subscription(client: MetabaseClient, dashboard_id: int, cron_schedule: str, recipients: list[dict]) -> dict` | Crea una suscripcion periodica a un dashboard de Metabase. Envia el dashboard completo por email segun el cron configurado. Endpoint: POST /api/notification. |
| `metabase_create_document_comment_py_infra` | `def metabase_create_document_comment(client: MetabaseClient, document_id: int, content: dict, *, child_target_id: str \| None = None, parent_comment_id: int \| None = None) -> dict` | Crea un comentario en un document. Soporta anclaje a bloque concreto (via UUID de _id) y respuestas en thread (via parent_comment_id). Endpoint: POST /api/comment. |
| `metabase_create_document_py_infra` | `def metabase_create_document(client: MetabaseClient, name: str, document: dict, collection_id: int = 0, *, validate: bool = True) -> dict` | Crea un document con contenido ProseMirror. Valida el arbol contra la whitelist de nodos ANTES de enviar (evita documentos que la API acepta pero el frontend renderiza vacíos). Usar prosemirror_card_embed() para embeber cards. |
| `metabase_create_group_py_infra` | `def metabase_create_group(client: MetabaseClient, name: str) -> dict` | Crea un nuevo Permission Group en Metabase. El nombre debe ser unico. Retorna el grupo creado con su id asignado. Endpoint: POST /api/permissions/group. |
| `metabase_create_model_py_infra` | `def metabase_create_model(client: MetabaseClient, name: str, sql: str, database_id: int, collection_id: int = 0, description: str = '') -> dict` | Crea un modelo de Metabase (card con type='model') que otras cards MBQL pueden usar como fuente via source-table: 'card__<id>'. |
| `metabase_create_ops_dashboard_py_pipelines` | `metabase_create_ops_dashboard(app_name: str) -> None` | Crea dashboard operativo en Metabase para una app: KPIs de entities/relations/executions/assertions, distribucion por status y tipo, relaciones frecuentes, resultados de ejecuciones y assertions. |
| `metabase_create_snippet_py_infra` | `def metabase_create_snippet(client: MetabaseClient, name: str, content: str, description: str = "", collection_id: int = 0) -> dict` | Crea un nuevo SQL snippet reutilizable en Metabase. El snippet se referencia en queries nativas con {{snippet: nombre}}. |
| `metabase_create_user_go_infra` | `func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error)` | Crea un nuevo usuario en Metabase. Si no se provee password, Metabase envia email de invitacion. Requiere permisos de superusuario. Endpoint: POST /api/user. |
| `metabase_create_user_py_infra` | `def metabase_create_user(client: MetabaseClient, first_name: str, last_name: str, email: str, password: str = '', group_ids: list[int] \| None = None) -> dict` | Crea un nuevo usuario en Metabase. Sin password envia invitacion por email. Requiere superusuario. Endpoint: POST /api/user. |
| `metabase_dashboard_append_row_py_infra` | `def metabase_dashboard_append_row(client: MetabaseClient, *, dashboard_id: int, tab_id: int, card_ids: list[int], height: int = 4, donor_card_id: int = 0, grid_width: int = 24) -> dict` | Añade N cards como fila horizontal al final de una tab de dashboard. Calcula la primera fila libre con metabase_dashboard_next_row, distribuye las cards con ancho uniforme (grid_width // N), copia parameter_mappings de un dashcard donante si se indica y llama a metabase_update_dashboard_safe. Util para añadir filas de KPIs con los filtros del dashboard ya conectados. |
| `metabase_dashboard_next_row_py_infra` | `def metabase_dashboard_next_row(client: MetabaseClient, *, dashboard_id: int, tab_id: int = 0) -> int` | Calcula la primera fila libre al final de una tab de dashboard: max(dc.row + dc.size_y) entre las dashcards de esa tab. Retorna 0 si la tab esta vacia. Evita el boilerplate manual de max() al colocar nuevas cards al final del dashboard. |
| `metabase_deactivate_user_go_infra` | `func MetabaseDeactivateUser(client MetabaseClient, userID int) error` | Desactiva (soft-delete) un usuario en Metabase. El usuario no se elimina permanentemente, solo se marca como inactivo. Para reactivar, usar PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id. |
| `metabase_deactivate_user_py_infra` | `def metabase_deactivate_user(client: MetabaseClient, user_id: int) -> None` | Desactiva (soft-delete) un usuario en Metabase. Reactivar con PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id. |
| `metabase_delete_card_go_infra` | `func MetabaseDeleteCard(client MetabaseClient, cardID int) error` | Elimina permanentemente una card/pregunta de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateCard con archived:true. Endpoint: DELETE /api/card/:id. |
| `metabase_delete_card_py_infra` | `def metabase_delete_card(client: MetabaseClient, card_id: int) -> None` | Elimina permanentemente una card/pregunta. IRREVERSIBLE. Preferir archived=True. Endpoint: DELETE /api/card/:id. |
| `metabase_delete_dashboard_go_infra` | `func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error` | Elimina permanentemente un dashboard de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateDashboard con archived:true. Endpoint: DELETE /api/dashboard/:id. |
| `metabase_delete_dashboard_py_infra` | `def metabase_delete_dashboard(client: MetabaseClient, dashboard_id: int) -> None` | Elimina permanentemente un dashboard. IRREVERSIBLE. Preferir archived=True. Endpoint: DELETE /api/dashboard/:id. |
| `metabase_delete_document_py_infra` | `def metabase_delete_document(client: MetabaseClient, document_id: int) -> None` | Elimina un document. Requiere archivado previo (Metabase rechaza DELETE si archived=False). |
| `metabase_delete_group_py_infra` | `def metabase_delete_group(client: MetabaseClient, group_id: int) -> None` | Elimina permanentemente un Permission Group de Metabase. IRREVERSIBLE. No borra los usuarios, solo el grupo. Endpoint: DELETE /api/permissions/group/:id. |
| `metabase_delete_membership_py_infra` | `def metabase_delete_membership(client: MetabaseClient, membership_id: int) -> None` | Elimina una membresía de grupo en Metabase por su membership_id. Endpoint: DELETE /api/permissions/membership/:id. NO acepta user_id+group_id — requiere el membership_id exacto. |
| `metabase_delete_notification_py_infra` | `def metabase_delete_notification(client: MetabaseClient, notification_id: int) -> None` | Elimina una notificacion de Metabase (alerta de card o suscripcion de dashboard). Operacion irreversible. Endpoint: DELETE /api/notification/:id. |
| `metabase_execute_card_go_infra` | `func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error)` | Ejecuta la query de una card/pregunta guardada en Metabase y retorna los resultados. Soporta parametros para queries parametrizadas. Endpoint: POST /api/card/:id/query. |
| `metabase_execute_card_py_infra` | `def metabase_execute_card(client: MetabaseClient, card_id: int, parameters: list[dict] \| None = None) -> dict` | Ejecuta la query de una card guardada y retorna resultados con columnas y filas. Soporta parametros. Endpoint: POST /api/card/:id/query. |
| `metabase_execute_query_go_infra` | `func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error)` | Ejecuta una query SQL ad-hoc contra una database de Metabase sin guardarla como card. Util para consultas rapidas y exploracion. Endpoint: POST /api/dataset. |
| `metabase_execute_query_py_infra` | `def metabase_execute_query(client: MetabaseClient, database_id: int, sql: str, max_results: int = 0) -> dict` | Ejecuta query SQL ad-hoc contra Metabase sin guardarla como card. Util para exploracion rapida. Endpoint: POST /api/dataset. |
| `metabase_export_card_py_infra` | `def metabase_export_card(client: MetabaseClient, card_id: int, format: str = 'csv') -> bytes` | Exporta los resultados de una card de Metabase en CSV, XLSX o JSON. Endpoint: POST /api/card/:id/query/:format. |
| `metabase_fix_null_ratio_py_infra` | `def metabase_fix_null_ratio(client: MetabaseClient, *, dry_run: bool = True, card_ids: list[int] \| None = None) -> dict` | Detecta y repara el patron vulnerable SUM(a-b)/SUM(b) en cards MBQL de Metabase. Cuando una resta pre-agg tiene operandos con NULL, SUM(A-B)!=SUM(A)-SUM(B). El fix reescribe las referencias post-agg al slot diferencia para usar (SUM(A)-SUM(B)) en su lugar. Soporta dry_run para escanear sin modificar. |
| `metabase_fix_permissions_py_pipelines` | `metabase_fix_permissions() -> None` | Arregla permisos SQLITE_READONLY_DIRECTORY en el contenedor Metabase. Hace chmod 777/666 en directorios y archivos .db bajo /data/ para que el usuario metabase (UID 2000) pueda crear journal files. |
| `metabase_get_card_go_infra` | `func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error)` | Obtiene los detalles completos de una card/pregunta de Metabase por su ID. Incluye la query, visualizacion y metadata. Endpoint: GET /api/card/:id. |
| `metabase_get_card_py_infra` | `def metabase_get_card(client: MetabaseClient, card_id: int) -> dict` | Obtiene detalles completos de una card/pregunta de Metabase incluyendo query, visualizacion y metadata. Endpoint: GET /api/card/:id. |
| `metabase_get_collection_graph_py_infra` | `def metabase_get_collection_graph(client: MetabaseClient, namespace: str \| None = None) -> dict` | Obtiene el grafo de permisos de colecciones de Metabase. Endpoint: GET /api/collection/graph. Soporta namespace opcional para snippet collections. El campo revision es crítico para concurrency control en el PUT posterior. |
| `metabase_get_dashboard_go_infra` | `func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error)` | Obtiene un dashboard completo de Metabase incluyendo todas sus dashcards (cards posicionadas en el dashboard), tabs y parametros. Endpoint: GET /api/dashboard/:id. |
| `metabase_get_dashboard_py_infra` | `def metabase_get_dashboard(client: MetabaseClient, dashboard_id: int) -> dict` | Obtiene dashboard completo con dashcards (cards posicionadas), tabs y parametros. Endpoint: GET /api/dashboard/:id. |
| `metabase_get_database_py_infra` | `def metabase_get_database(client: MetabaseClient, database_id: int) -> dict` | Obtiene los detalles de una database de Metabase por su ID. Endpoint: GET /api/database/:id. |
| `metabase_get_document_py_infra` | `def metabase_get_document(client: MetabaseClient, document_id: int) -> dict` | Obtiene un document completo con su arbol ProseMirror (campo document). Endpoint: GET /api/document/:id. |
| `metabase_get_group_py_infra` | `def metabase_get_group(client: MetabaseClient, group_id: int) -> dict` | Obtiene un Permission Group de Metabase por ID, incluyendo la lista completa de miembros con sus datos. Endpoint: GET /api/permissions/group/:id. |
| `metabase_get_permission_graph_py_infra` | `def metabase_get_permission_graph(client: MetabaseClient) -> dict` | Obtiene el grafo de permisos de datos (databases/schemas/tables) de Metabase. Endpoint: GET /api/permissions/graph. El campo revision es crítico para concurrency control en el PUT posterior. |
| `metabase_get_snippet_py_infra` | `def metabase_get_snippet(client: MetabaseClient, snippet_id: int) -> dict` | Obtiene un SQL snippet de Metabase por su ID. Endpoint: GET /api/native-query-snippet/:id. |
| `metabase_get_user_go_infra` | `func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error)` | Obtiene los detalles de un usuario de Metabase por su ID numerico. Endpoint: GET /api/user/:id. |
| `metabase_get_user_py_infra` | `def metabase_get_user(client: MetabaseClient, user_id: int) -> dict` | Obtiene los detalles de un usuario de Metabase por su ID. Endpoint: GET /api/user/:id. |
| `metabase_list_cards_go_infra` | `func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error)` | Lista preguntas/cards de Metabase con filtro opcional. Retorna array de cards. Endpoint: GET /api/card. |
| `metabase_list_cards_py_infra` | `def metabase_list_cards(client: MetabaseClient, filter: str = '', model_id: int = 0) -> list[dict]` | Lista preguntas/cards de Metabase. Filtros: all, mine, fav, archived, recent, popular, database, table. Endpoint: GET /api/card. |
| `metabase_list_dashboards_go_infra` | `func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error)` | Lista dashboards de Metabase con filtro opcional. Retorna array de dashboards resumidos (sin dashcards). Endpoint: GET /api/dashboard. |
| `metabase_list_dashboards_py_infra` | `def metabase_list_dashboards(client: MetabaseClient, filter: str = '') -> list[dict]` | Lista dashboards de Metabase. Filtros: all, mine, archived. Retorna resumen sin dashcards. Endpoint: GET /api/dashboard. |
| `metabase_list_databases_py_infra` | `def metabase_list_databases(client: MetabaseClient, include_tables: bool = False) -> list` | Lista las databases configuradas en Metabase. Endpoint: GET /api/database. Soporta incluir tablas con include_tables=True. |
| `metabase_list_document_comments_py_infra` | `def metabase_list_document_comments(client: MetabaseClient, document_id: int, *, include_resolved: bool = True, include_deleted: bool = False) -> list[dict]` | Lista los comentarios de un document (threads, anclajes por bloque UUID, reacciones). Endpoint no documentado: GET /api/comment?target_type=document&target_id=:id. |
| `metabase_list_documents_py_infra` | `def metabase_list_documents(client: MetabaseClient) -> list[dict]` | Lista documents (texto ProseMirror tipo Notion, feature 0.57+). Endpoint: GET /api/document. Desenrolla {items: [...]}. |
| `metabase_list_groups_py_infra` | `def metabase_list_groups(client: MetabaseClient) -> list[dict]` | Lista todos los Permission Groups de Metabase con su member_count. Requiere superusuario. Endpoint: GET /api/permissions/group. |
| `metabase_list_memberships_py_infra` | `def metabase_list_memberships(client: MetabaseClient) -> dict[str, list[dict]]` | Lista todas las membresías de grupos de Metabase. Endpoint: GET /api/permissions/membership. Retorna dict con group_id (str) como clave y lista de membresías como valor. |
| `metabase_list_notifications_py_infra` | `def metabase_list_notifications(client: MetabaseClient, card_id: int = 0, dashboard_id: int = 0) -> list[dict]` | Lista notificaciones de Metabase (alertas y suscripciones). Filtra opcionalmente por card_id o dashboard_id. Endpoint: GET /api/notification. |
| `metabase_list_snippets_py_infra` | `def metabase_list_snippets(client: MetabaseClient, archived: bool = False) -> list[dict]` | Lista SQL snippets reutilizables de Metabase. Un snippet se referencia en queries con {{snippet: nombre}}. |
| `metabase_list_users_go_infra` | `func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error)` | Lista usuarios de una instancia Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user. Requiere permisos de superusuario. |
| `metabase_list_users_py_infra` | `def metabase_list_users(client: MetabaseClient, status: str = '', query: str = '', limit: int = 0, offset: int = 0) -> dict` | Lista usuarios de Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user. |
| `metabase_mbql_from_source_card_py_metabase` | `def metabase_mbql_from_source_card(*, database_id: int, source_card_id: int, aggregations: list[dict], joins: list[dict] \| None = None, filters: list[list] \| None = None, breakouts: list[dict] \| None = None, expressions: list[dict] \| None = None) -> dict` | Construye un dataset_query MBQL que envuelve una saved card (source-card) con agregaciones, joins opcionales, filtros, breakouts y una segunda etapa de expressions calculadas. Genera automaticamente todos los lib/uuid con uuid4. Elimina la necesidad de construir el JSON MBQL manualmente para el patron comun de agregar sobre una card guardada y anadir expresiones derivadas. |
| `metabase_mbql_validate_py_core` | `def metabase_mbql_validate(dataset_query: dict) -> list[str]` | Valida la estructura de un dataset_query MBQL sin I/O. Detecta UUIDs duplicados, stage mixing (aggregations + expressions que referencian slots en la misma stage), slot refs rotas (sum_X inexistente), case structures invalidas y name collisions en expressions. Retorna lista de errores, vacia si el query es valido. |
| `metabase_move_card_py_infra` | `def metabase_move_card(client: MetabaseClient, card_id: int, collection_id: int \| None) -> dict` | Mueve una card/pregunta a otra coleccion via PUT /api/card/:id. Wrapper thin que solo actualiza collection_id. collection_id=None mueve a 'Our analytics' (root). |
| `metabase_move_collection_py_infra` | `def metabase_move_collection(client: MetabaseClient, collection_id: int, parent_id: int \| None) -> dict` | Mueve una collection (sub-arbol completo) a otro padre. Endpoint: PUT /api/collection/:id con {parent_id: ...}. |
| `metabase_move_dashboard_py_infra` | `def metabase_move_dashboard(client: MetabaseClient, dashboard_id: int, collection_id: int \| None) -> dict` | Mueve un dashboard a otra coleccion via PUT /api/dashboard/:id. Wrapper thin que solo actualiza collection_id. collection_id=None mueve a 'Our analytics' (root). |
| `metabase_move_document_py_infra` | `def metabase_move_document(client: MetabaseClient, document_id: int, collection_id: int \| None) -> dict` | Mueve un document a otra coleccion. Thin wrapper sobre PUT /api/document/:id enviando solo collection_id. |
| `metabase_pair_n_n1_columns_py_infra` | `def metabase_pair_n_n1_columns(client: MetabaseClient, *, dry_run: bool = True, card_ids: list[int] \| None = None, base_field: str = 'Valor_vendido') -> dict` | Para cards Metabase con display table/pivot que agregan SUM(base_field) y SUM(base_field_1), habilita la columna base_field_1 en visualization_settings.table.columns y la posiciona inmediatamente despues de base_field para comparacion visual. Soporta dry_run y campo base configurable. |
| `metabase_resolve_document_comment_py_infra` | `def metabase_resolve_document_comment(client: MetabaseClient, comment_id: int) -> dict` | Marca un comentario como resuelto (is_resolved=True). Los comentarios resueltos se ocultan en la UI pero siguen consultables via metabase_list_document_comments(include_resolved=True). Endpoint: PUT /api/comment/:id. |
| `metabase_setup_py_infra` | `metabase_setup(base_url: str, admin_email: str, admin_password: str, admin_first_name: str, admin_last_name: str, site_name: str, site_locale: str) -> dict` | Ejecuta el setup inicial de una instancia Metabase nueva via POST /api/setup. Obtiene el setup-token automaticamente y crea el usuario admin con preferencias del sitio. |
| `metabase_smartscalar_anothercolumn_viz_py_infra` | `def metabase_smartscalar_anothercolumn_viz(*, main_column: str, compare_column: str, label: str = 'vs N-1', number_style: str = 'percent', decimals: int = 2, currency: str = 'EUR', comparison_id: str = 'cmp_n1') -> dict` | Construye el dict completo de visualization_settings para una card display=smartscalar con comparacion tipo anotherColumn (misma fila, columna diferente). Incluye scalar.field, scalar.comparisons y column_settings para ambas columnas con el mismo formato. Internamente reutiliza metabase_viz_column_format. |
| `metabase_smartscalar_dimension_tag_py_infra` | `def metabase_smartscalar_dimension_tag(*, name: str, field_id: int, base_type: str = 'type/Text', widget_type: str = 'string/=', display_name: str = '') -> dict` | Construye un template-tag de tipo dimension con la firma exacta que Metabase v0.59 espera (incluyendo lib/uuid, base-type, effective-type). Sin estos flags Metabase devuelve PUT 200 pero descarta silenciosamente el dataset_query. |
| `metabase_smartscalar_kpi_payload_py_infra` | `def metabase_smartscalar_kpi_payload(*, name: str, database_id: int, sql: str, template_tags: dict \| None = None, description: str = '', collection_id: int = 0, currency: bool = False, currency_code: str = 'EUR', decimals: int = 0, comparison_label: str = 'vs n-1', extra_visualization_settings: dict \| None = None) -> dict` | Construye el payload completo para POST /api/card de un KPI smartscalar con formato y comparacion previousValue. Combina SQL nativo (idealmente generado por metabase_smartscalar_kpi_sql) con template-tags y visualization_settings predefinidos. Listo para metabase_create_card_raw. |
| `metabase_smartscalar_kpi_sql_py_infra` | `def metabase_smartscalar_kpi_sql(*, act_expr: str, n1_expr: str, body_sql: str, date_expr: str = 'MIN(fecha)') -> str` | Envuelve agregaciones actual+n-1 en el patron de 2 filas (periodo, valor) que el display smartscalar de Metabase v0.59 requiere para mostrar comparacion vs ano anterior sin pedir breakout temporal. Genera SQL nativo BigQuery con UNION ALL d_min/d_min-52w. |
| `metabase_update_card_go_infra` | `func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error)` | Actualiza campos de una card/pregunta en Metabase. Solo se modifican los campos incluidos en el map. Endpoint: PUT /api/card/:id. |
| `metabase_update_card_py_infra` | `def metabase_update_card(client: MetabaseClient, card_id: int, **fields) -> dict` | Actualiza campos de una card/pregunta via kwargs. Campos: name, description, display, dataset_query, collection_id, archived. Endpoint: PUT /api/card/:id. |
| `metabase_update_collection_graph_py_infra` | `def metabase_update_collection_graph(client: MetabaseClient, graph: dict, namespace: str \| None = None) -> dict` | Actualiza el grafo de permisos de colecciones en Metabase. Endpoint: PUT /api/collection/graph. El campo revision en el graph es obligatorio — el servidor rechaza con 409 si no coincide con el actual. Soporta namespace para snippet collections. |
| `metabase_update_dashboard_go_infra` | `func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error)` | Actualiza un dashboard en Metabase incluyendo metadata, cards y tabs. El campo dashcards representa el estado completo deseado: cards nuevas con ID negativo, existentes con ID positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id. |
| `metabase_update_dashboard_py_infra` | `def metabase_update_dashboard(client: MetabaseClient, dashboard_id: int, **fields) -> dict` | Actualiza dashboard incluyendo metadata, cards y tabs via kwargs. dashcards es el estado completo deseado: nuevas con ID negativo, existentes con positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id. |
| `metabase_update_dashboard_safe_py_infra` | `def metabase_update_dashboard_safe(client: MetabaseClient, dashboard_id: int, *, dashcards_update: list[dict] \| None = None, dashcards_add: list[dict] \| None = None, dashcards_remove: list[int] \| None = None, extra_fields: dict \| None = None) -> dict` | Wrapper sobre PUT /api/dashboard/:id que maneja los tres gotchas documentados: strip del campo '.card' denormalizado (evita 413), inclusion obligatoria de 'tabs' (evita 500 FK violation) y asignacion de IDs negativos a dashcards nuevos. Soporta reemplazo completo (dashcards_update), operaciones incrementales (add/remove) y actualizacion de campos extra del dashboard. |
| `metabase_update_document_py_infra` | `def metabase_update_document(client: MetabaseClient, document_id: int, *, validate: bool = True, **fields) -> dict` | Actualiza un document. Solo envia los campos pasados. Si se pasa 'document', valida el ProseMirror antes de enviar (evita documentos vacíos por nodos no soportados). |
| `metabase_update_group_py_infra` | `def metabase_update_group(client: MetabaseClient, group_id: int, name: str) -> dict` | Renombra un Permission Group en Metabase. La API solo permite modificar el nombre del grupo. Endpoint: PUT /api/permissions/group/:id. |
| `metabase_update_notification_py_infra` | `def metabase_update_notification(client: MetabaseClient, notification_id: int, **fields) -> dict` | Actualiza una notificacion existente de Metabase (alerta o suscripcion). Permite modificar active, handlers, subscriptions o payload. Endpoint: PUT /api/notification/:id. |
| `metabase_update_permission_graph_py_infra` | `def metabase_update_permission_graph(client: MetabaseClient, graph: dict) -> dict` | Actualiza el grafo de permisos de datos en Metabase. Endpoint: PUT /api/permissions/graph. El campo revision en el graph es obligatorio — el servidor rechaza con 409 si no coincide con el actual. |
| `metabase_update_snippet_py_infra` | `def metabase_update_snippet(client: MetabaseClient, snippet_id: int, **fields) -> dict` | Actualiza campos de un SQL snippet en Metabase. Acepta name, content, description, collection_id, archived via **fields. |
| `metabase_update_user_go_infra` | `func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error)` | Actualiza campos de un usuario en Metabase. Solo se modifican los campos incluidos en el map. Requiere permisos de superusuario. Endpoint: PUT /api/user/:id. |
| `metabase_update_user_py_infra` | `def metabase_update_user(client: MetabaseClient, user_id: int, **fields) -> dict` | Actualiza campos de un usuario en Metabase via keyword arguments. Campos: first_name, last_name, email, is_superuser, group_ids, locale. Endpoint: PUT /api/user/:id. |
| `metabase_validate_card_payload_py_infra` | `def metabase_validate_card_payload(payload: dict) -> list[str]` | Valida la estructura de un payload de card de Metabase sin necesidad de red. Recorre todos los checks y acumula todos los issues en vez de abortar al primero. Retorna lista vacia si el payload es valido. |
| `metabase_validate_dashboard_payload_py_infra` | `def metabase_validate_dashboard_payload(payload: dict, known_card_ids: set[int]) -> list[str]` | Valida la estructura de un payload de dashboard de Metabase sin red. Verifica campos obligatorios, bounds de dashcards, referencias a cards conocidas y solapamientos entre dashcards. Acumula todos los issues. |
| `metabase_validate_document_payload_py_infra` | `def metabase_validate_document_payload(payload: dict, known_card_slugs: set[str] \| None = None) -> list[str]` | Valida un arbol ProseMirror contra la whitelist de nodos y marks que el editor TipTap de Metabase renderiza. Detecta nodos desconocidos que la API acepta pero el frontend descarta silenciosamente. |
| `metabase_validate_sql_py_infra` | `def metabase_validate_sql(client: MetabaseClient, database_id: int, sql: str, max_rows: int = 0) -> dict` | Valida sintaxis y referencias de una query SQL ejecutandola contra Metabase via POST /api/dataset. Captura tanto errores HTTP como errores embebidos en el body (Metabase a veces devuelve 200 con status failed). Retorna ok, error y rows_returned. |
| `metabase_viz_column_format_py_infra` | `def metabase_viz_column_format(column: str, *, number_style: str = '', decimals: int = -1, currency: str = '', currency_in_header: bool \| None = None, suffix: str = '', prefix: str = '') -> dict` | Construye la entrada column_settings para una columna en visualization_settings de Metabase. Genera la clave con el formato JSON exacto que Metabase requiere ('['name','<columna>']') y el dict de formato con solo los campos explicitamente especificados. El resultado se fusiona en visualization_settings['column_settings']. |
| `prosemirror_card_embed_py_infra` | `def prosemirror_card_embed(card_id: int, height: int = 400) -> dict` | Genera un nodo ProseMirror cardEmbed envuelto en resizeNode con altura adecuada. Un cardEmbed desnudo renderiza con ~50px — este helper produce el formato que Metabase espera para que la card se vea bien. |
| `setup_metabase_volume_bash_pipelines` | `setup_metabase_volume([registry_db_path: string], [container_name: string], [dest_path: string]) -> void` | Copia registry.db al contenedor Docker de Metabase verificando existencia del archivo, disponibilidad de docker, estado del contenedor y coincidencia de tamaños. Todos los argumentos son opcionales con defaults razonables. |


## Ejemplo canonico

### Auth + leer un dashboard (Python)

```python
import os, sys
sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))
from metabase import MetabaseClient, metabase_get_dashboard, metabase_list_cards

client = MetabaseClient(
    base_url=os.environ["METABASE_URL"],
    username=os.environ["METABASE_USER"],
    password=os.environ["METABASE_PASS"],
)
client.auth()

dash = metabase_get_dashboard(client, dashboard_id=42)
cards = metabase_list_cards(client, collection_id=dash["collection_id"])
```

### Cliente autenticado desde `pass` (sin manejar credenciales a mano)

```python
import os, sys
sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))
from metabase import metabase_client_from_pass, metabase_get_dashboard

# Aurgi: API-key de una línea en pass (mb_...)
client = metabase_client_from_pass("metabase/aurgi-api-key", "https://reports.autingo.es", mode="api_key")

# Captación: secreto multi-línea (password + email:) → sesión
# client = metabase_client_from_pass("captacion/metabase", "http://localhost:3030", mode="session")

dash = metabase_get_dashboard(client, dashboard_id=734)
```

### Crear card + dashboard + ejecutar (Go)

```bash
./fn run metabase_auth --base-url $METABASE_URL --user admin --pass $MB_PASS
./fn run metabase_create_card --name "Sales 30d" --sql "SELECT ..." --database-id 1
./fn run metabase_create_dashboard --name "Sales overview" --collection-id 5
./fn run metabase_execute_card --card-id 123 | jq .
```

### Setup local con Docker

```bash
./fn run setup_metabase_volume
./fn run init_metabase --project fn_registry
```

## Fronteras

- **NO instala Metabase**. El binario/Docker container debe existir. Solo cliente API + composiciones.
- **NO usa el cliente Java/Python oficial**. Llama API REST directa via HTTP — mas simple, sin deps pesadas.
- **NO escribe SQL**. La query la pasa el caller. Para construir SQL custom, usar funciones del grupo `sql`.
- **NO maneja Metabase Embedded JS SDK** (frontend embed). Solo backend/API.
- Wrappear cada endpoint nuevo via `fn-constructor` con tag `metabase`. NO hacer `client._http.request(...)` raw.
