AI Hub
Per-country configuration surface for the EnvIA AI agent: business hours, holidays, operational documents, and a chat sandbox. Lives at /ai/config in the admon and talks to the agentic-ai backend.
Overview
The admon historically exposed six "agents" (Tracking, Shipping, Ticket, Pickup, Sales, General) as editable entities. Those entities no longer exist as routable agents — the unified support graph in agentic-ai absorbed all of them. This module replaces that legacy view with:
- A single
EnvIAagent card (read-only metadata). - A per-country settings panel (business hours + holidays) that persists to the
countriesSupabase table. - A per-country documents panel (PDF / Markdown) that uploads to the
country-documentsSupabase Storage bucket and indexes them in the RAG pipeline. - An AgentSandbox that hits
POST /v1/chatfor live testing.
The previous /ai/knowledge cross-country view was retired so managers cannot see documents from other countries. All knowledge management happens per-country from /ai/config.
Key Concepts
| Concept | Description |
|---|---|
| EnvIA | The single unified support agent. Lives as the support graph in agentic-ai/src/graphs/support/. Configurable surface (hours, holidays, docs) is per-country. |
| Country settings | business_hours and holidays JSONB columns on the countries table in Supabase. Read at runtime by business-availability.util.ts and isHoliday(). |
| Country documents | Rows in country_documents referencing files in the country-documents Supabase Storage bucket. Indexed into document_chunks for RAG retrieval. |
| Sandbox | The right column of /ai/config. Calls POST /v1/chat with channel: 'http' and a per-conversation userId so admin tests never collide with real user state. |
| Permission gate | A single permission with class_name = 'menu-ai-hub-config' drives sidebar visibility. Granted via role assignment or per-user override. |
Architecture
┌────────────────────────────────────────────────────────────────┐
│ admon (Vue 3) │
│ │
│ views/ai/config/index.vue │
│ ├─ CountrySettingsPanel.vue (hours + holidays) │
│ ├─ CountryDocumentsPanel.vue (PDF/MD upload + signed URL) │
│ └─ AgentSandbox.vue (POST /v1/chat) │
└────────────┬───────────────────────────────────────────────────┘
│ HTTP /_/ai-hub/* (admon JWT cookie + permission gate)
▼
┌────────────────────────────────────────────────────────────────┐
│ admon (Node + Hapi) — proxy only │
│ │
│ routes/aiHub.routes.js │
│ auth: token_admin │
│ pre: permissionMiddleware.canByName('menu-ai-hub-config') │
│ │
│ controllers/aiHub.controller.js │
│ forwards /ai-hub/{path*} → ${ENVIA_AGENT_API_URL}/{path*} │
│ adds `x-admin-token: ${BOT_AI_TOKEN}` server-side │
└────────────┬───────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ agentic-ai (Node + Hapi) │
│ │
│ src/routes/countries.route.ts │
│ GET /v1/countries (lists with settings) │
│ PATCH /v1/countries/{code} (writes back settings) │
│ │
│ src/routes/documents.route.ts │
│ POST /api/documents/upload (multipart) │
│ GET /api/documents/{country_code} (list per country) │
│ GET /api/documents/{id}/preview (signed URL, 1h TTL) │
│ DELETE /api/documents/{id} (cascade delete) │
│ │
│ src/routes/chat.route.ts │
│ POST /v1/chat (sandbox + production) │
│ │
│ src/config/countries.config.ts │
│ isHoliday(country, date, dbHolidays?, dbTimezone?) │
│ └─ DB-loaded holidays take precedence over holidays.json │
│ │
│ src/shared/business-availability.util.ts │
│ isBusinessAvailable(countryConfig, country) │
│ └─ Reads business_hours.weekday/weekend + DB timezone │
└────────────┬───────────────────────────────────────────────────┘
│
▼ pg pool (writes) + countryService cache (reads)
│
┌────────────▼───────────────────────────────────────────────────┐
│ Supabase (Postgres + Storage) │
│ │
│ countries (business_hours, holidays jsonb) │
│ country_documents (bucket_path, processing_status, …) │
│ document_chunks (chunked + embedded for RAG) │
│ │
│ Storage bucket: country-documents (private, signed URLs) │
└────────────────────────────────────────────────────────────────┘The Supabase ↔ agentic-ai layer also feeds countryService (3-level cache: memory → Redis → pg) which the support graph consults on every conversation. See agentic-ai/src/services/country.service.ts.
Data Flow
Manager edits business hours and holidays
Manager uploads a document
Manager opens a document
Click "Abrir"
↓
api.ai.getDocumentPreview(doc.id)
↓
GET /api/documents/{id}/preview
↓
supabase.storage.from('country-documents').createSignedUrl(path, 3600)
↓
{ previewUrl } (1-hour TTL)
↓
window.open(url, '_blank', 'noopener,noreferrer')The signed URL is never persisted in localStorage, the Vuex store, or any log line. noopener,noreferrer prevents the destination tab from accessing window.opener or leaking the URL via the Referer header.
Database
Tables
| Table | Used for | Notes |
|---|---|---|
catalog_admin_permission_groups | Sidebar grouping ("AI" with sort_order = 50) | Single row added by the seed |
catalog_admin_permissions | One row keyed by class_name = 'menu-ai-hub-config' | parent_id = 0 (top-level), action = 'ai/config', is_menu = 1 |
administrator_role_permissions | Grants the permission to a role | Default seed assigns to role_id = 1 (Super Admin) only |
admin_permissions_overrides | Per-user grant/revoke | Used to give individual managers access without changing their role |
countries (Supabase Postgres) | business_hours, holidays, timezone columns | countryService reads with cache, PATCH writes through |
country_documents (Supabase Postgres) | One row per uploaded document | bucket_path references the Supabase Storage object |
document_chunks (Supabase Postgres) | Chunked + embedded for RAG | Populated by the background ingestion in processDocumentInBackground |
Permission seed
Run manually on each environment before enabling the module. The idempotent SQL and required env vars belong in the pull request description (or internal DBA runbook), not in this repo’s documentation.
Tables touched: catalog_admin_permission_groups, catalog_admin_permissions, administrator_role_permissions (default grant to Super Admin, role_id = 1). Per-user access without role changes uses admin_permissions_overrides.
Key Decisions
| Decision | Reasoning | Alternatives considered |
|---|---|---|
| Single hardcoded EnvIA agent | The unified support graph is the only routable agent. Listing six legacy fantoms misled managers into thinking they could edit each one. | Keep the list but mark them deprecated (rejected — still confusing). Fetch the list dynamically (rejected — there is only one agent). |
| One top-level menu permission (no parent + child) | The module only has one route (/ai/config). A parent container without an action just added an empty intermediate level. | Parent + child structure as originally seeded (rejected — extra clicks, no value when only one child exists). |
| Restrict uploads to PDF + Markdown | The chunker performs noticeably better on structured Markdown (## / ### heading boundaries). PDF preserves layout for documents that already exist as such. CSV/JSON/XLSX/images added noise. | Accept all formats and let the chunker handle them (rejected — quality dropped on unstructured inputs). |
Filter metadata.global = true at the admon UI | Global documents are managed in a different surface (cross-country), and exposing them per-country drowned the manager in irrelevant rows. | Filter at the backend GET (rejected — would affect other consumers). |
Per-row window.open with noopener,noreferrer for previews | Signed URLs are short-lived but still bearer-equivalent. Preventing Referer and window.opener leakage is cheap and standard. | Inline iframe preview (used previously — failed on CORS / X-Frame-Options for some bucket configs). |
channel: 'http' in the sandbox (was 'admin-sandbox') | The chat validator only accepts the enum http / respondio / whatsapp / telegram / enviaclaw / clients_portal. 'admin-sandbox' returned HTTP 400. The traceability moved to metadata.source. | Add 'admin-sandbox' to the backend enum (rejected — pollutes the production enum with a UI-only value). |
isHoliday() reads from DB first, JSON fallback | The previous behaviour ignored the countries.holidays column entirely. Managers could edit it without effect. New signature takes optional dbHolidays and dbTimezone; callers pass values from countryConfig. | Move all holidays to DB and delete the JSON (rejected — keep the JSON as emergency fallback when DB is unavailable). |
Strip bucket_path from preview error logs | Bucket paths leak country and region identifiers into observability pipelines. The document UUID is enough to correlate via DB. | Keep the path for easier debugging (rejected — privacy first). |
Backend write goes through PATCH /v1/countries/:code, not a generic admin endpoint | Scope is tight: only business_hours and holidays. Validation is column-specific. Auth uses the existing x-admin-token pattern. | Generic /admin/countries/:id route (rejected — broader attack surface, harder validation). |
Frontend falls back to localStorage if the PATCH fails | The manager would lose all their edits otherwise. The draft is restored on the next mount and the badge "cambios guardados aquí" makes the state visible. | Block the save and show an error (rejected — bad UX during transient backend outages). |
Dependencies
Internal (admon)
views/ai/config/index.vue(entry point)components/CountrySettingsPanel.vue,CountryDocumentsPanel.vue,AgentSandbox.vueservices/ai.service.js—getCountries,updateCountrySettings,getDocuments,uploadDocument,getDocumentPreview,deleteDocument,chatcomposables/useAiLocale.js(cookie-driven country sync)- Stores
useAiStore(countries cache) anduseAuthStore(menu rendering)
External (agentic-ai)
src/routes/countries.route.ts,src/routes/documents.route.ts,src/routes/chat.route.tssrc/services/country.service.ts(3-level cache)src/config/countries.config.ts(isHoliday, JSON fallback)src/shared/business-availability.util.tssrc/services/documents/document-extractor.service.ts- Supabase Storage SDK (
@supabase/supabase-js)
Permissions / lang
catalog_admin_permission_groupsrow "AI"catalog_admin_permissionsrowmenu-ai-hub-config- i18n keys (
menu.group.ai,menu.ai.hub, etc.) in 20 lang files atbackend/resources/langs/*.json. Synced to S3 by the existing language pipeline.
OpenSpec proposals
This module is documented across three OpenSpec changes in agentic-ai/openspec/changes/:
| Folder | Scope |
|---|---|
expose-country-settings/ | GET /v1/countries returns business_hours + holidays; isHoliday() accepts DB-loaded inputs |
admon-ai-config-polish/ | UX overhaul: single agent, PDF/MD only, link-only previews, manager-friendly copy, pagination, capabilities panel |
persist-country-settings-from-admon/ (pending) | PATCH /v1/countries/:code + frontend wiring (this proposal not yet formalized but the code lives in the same commit cluster) |
Related Documentation
- User Guide:
/guide/modules/ai-hub - API Endpoints:
./api.md— Endpoint reference for this module - UI Screens:
./ui.md— Frontend screens and flows - Roles & Permissions module:
/technical/modules/roles-permissions/
