SaaS dashboard for annotation workers to track tasks, AHT (Average Handle Time), and earnings with immutable rate snapshots.
| Layer | Stack | Hosting |
|---|---|---|
| Frontend | Next.js (App Router) | Vercel |
| Backend API | FastAPI (Python) | Render (free tier) |
| Database / Auth | Supabase (PostgreSQL + Auth) | Supabase |
web/ → Next.js client (Vercel)
backend/ → FastAPI API (Render)
supabase/ → SQL migrations & schema
Auth is handled by Supabase Auth (auth.users). App data lives in public.profiles, public.projects, and public.task_logs with Row Level Security.
The web app defaults to English and offers Spanish as an option. Product-facing UI uses project terminology throughout.
AHT is entered and stored in minutes for projects and immutable task-log snapshots. The formula divides weighted AHT minutes by 60 to convert minutes to hours.
hours = (tasks_attempter * snapshot_aht_attempter
+ tasks_reviewer * snapshot_aht_reviewer) / 60.0
calculated_earnings = hours * hourly_rate_used
USD is the canonical accounting currency. global_hourly_rate is an editable USD hourly rate for future logs; once a log is committed, its hourly_rate_used and earnings snapshots (calculated_earnings / calculated_earnings_usd) remain immutable USD values. preferred_currency changes display values only: the web app gets current FX rates from FastAPI and never rewrites historical USD accounting values.
On INSERT a BEFORE INSERT trigger:
- Loads the project’s
current_aht_attempter/current_aht_reviewerin minutes - Loads the profile’s USD
global_hourly_rate - Overwrites client-supplied snapshot fields for integrity
- Ensures
user_idmatchesproject.user_id - Computes
calculated_earningsusing minute-based AHT - Forces canonical USD metadata and copies the result to
calculated_earnings_usd
On UPDATE only date, tasks_attempter, and tasks_reviewer may change. Snapshot columns, hourly_rate_used, and accounting metadata are immutable. Earnings are recalculated from the existing minute-based row snapshots — never from the project’s current AHT. Changing project AHT or the global USD rate does not rewrite historical logs.
Payment status is stored per task log in task_logs.payment_status and can be pending or paid; paid_at stores the timestamp for the user-reported paid state. Status is per log because a project can be paid in installments. This is self-reported bookkeeping, not external payment verification.
The dashboard displays gross earnings, acquired/paid earnings, and pending earnings in the selected display currency. Accounting and stored earnings remain canonical USD; selecting another currency changes presentation only.
FX conversion is display-only; accounting remains canonical USD. Stored earnings, aggregates, and exports are not rewritten when the display currency changes.
- When
ALLRATES_API_KEYis configured, the authenticated AllRatesToday endpoint is the primary provider and requests the required display currencies in one batch. Without a key, or when it fails, the backend uses the ExchangeRate-API open endpoint and then Frankfurter as fallbacks. ALLRATES_API_KEYis backend-only. It must never be placed in frontend code,NEXT_PUBLIC_*variables, or Vercel. Render stores it privately.- The normal in-memory refresh window is 3600 seconds (one hour) by default; set
FX_CACHE_TTL_SECONDSto change it. One hour or longer is recommended for free-tier quota protection. A manualrefresh=truebypasses the cache and can consume a provider request, so avoid polling it. - Successful responses include the provider's published update timestamp and source; if a refresh fails after rates are available, the last known rates are retained and marked stale. AllRatesToday 401/429 responses safely fall through to the other providers.
- The currency selector has a manual refresh control and displays the provider update timestamp, source, and stale status.
- Both FX API endpoints support
refresh=trueto bypass the normal in-memory window and force a fetch from the current provider. rate_to_usdis the USD value of 1 unit of the target currency. Providers return target-currency units per 1 USD, so the backend inverts those quotes. FX remains display-only; stored USD earnings and accounting data are never rewritten.- Free providers publish the latest available rates, not tick-by-tick trading prices. If true real-time market quotes are required, a paid/live provider key is needed.
Projects can be deleted from the UI. The confirmation warning explains that deleting a project permanently deletes all of its task logs through the database foreign-key cascade. This cannot be undone.
Apply the migrations in this order:
supabase/migrations/20260807_initial_schema.sqlsupabase/migrations/20260808_aht_minutes_currency_i18n.sqlsupabase/migrations/20260809_payment_status.sqlsupabase/migrations/20260810_onboarding_status.sql
The 20260808_aht_minutes_currency_i18n.sql migration must run after the initial migration. If the initial schema was applied manually, run it once in the Supabase SQL Editor. Its aht_unit guard prevents a second AHT conversion. It converts existing AHT values from seconds to minutes once, adds currency and locale metadata, and installs the minutes/USD triggers. Run 20260809_payment_status.sql after it to add per-task-log payment bookkeeping. Run 20260810_onboarding_status.sql after the payment migration and before deploying the frontend that uses profiles.onboarding_status. It adds persistent per-user tutorial state with pending, skipped, or completed; existing profiles start as pending.
Option A – Supabase CLI
supabase login
supabase link --project-ref <your-project-ref>
supabase db pushsupabase db push applies all pending migrations in order. If the initial migration is already tracked as applied, it applies the three follow-up migrations as the remaining pending migrations.
Option B – Dashboard SQL
- Open Supabase Dashboard → SQL Editor
- Paste and run
supabase/migrations/20260807_initial_schema.sql - Paste and run
supabase/migrations/20260808_aht_minutes_currency_i18n.sql - Paste and run
supabase/migrations/20260809_payment_status.sql - Paste and run
supabase/migrations/20260810_onboarding_status.sql
If the initial migration is already applied, skip step 2. If the 20260808 migration is already applied, skip step 3. Run the payment migration after 20260808, then run 20260810_onboarding_status.sql after the payment migration and before deploying the frontend that uses profiles.onboarding_status.
Use the example file for each app and fill in local values without committing secrets:
- Web (
web/.env.example):NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY,NEXT_PUBLIC_API_URL - Backend (
backend/.env.example):SUPABASE_URL,SUPABASE_ANON_KEY,SUPABASE_JWT_SECRET,ALLOWED_ORIGINS, optionalALLRATES_API_KEY, andFX_CACHE_TTL_SECONDS PORTis optional locally and is supplied by Render in production.ALLRATES_API_KEYis private backend configuration; there is no frontend key.- A Supabase service-role key is optional for separate admin work only; it is not required by the web app or API.
cd web
npm install
npm run dev
# http://localhost:3000cd backend
python -m venv venv
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# set SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_JWT_SECRET, ALLOWED_ORIGINS
uvicorn main:app --reload --port 8000
# http://localhost:8000 · docs: /docs · health: /health| Component | Steps |
|---|---|
| Supabase | Create project → apply the initial migration, then 20260808_aht_minutes_currency_i18n.sql, 20260809_payment_status.sql, and 20260810_onboarding_status.sql in that order (supabase db push or SQL Editor); run the onboarding migration before deploying the frontend that uses profiles.onboarding_status |
| Vercel | Set Root Directory to web/; set NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and NEXT_PUBLIC_API_URL (the deployed FastAPI URL); deploy |
| Render | From the repository root, create a Blueprint from render.yaml (it sets the service Root Directory to backend/); set ALLOWED_ORIGINS to http://localhost:3000,https://annotate-pay.vercel.app; enter the Supabase variables; optionally add the private ALLRATES_API_KEY and FX_CACHE_TTL_SECONDS=3600; deploy |
For a repository-root Render Blueprint, use the root-level render.yaml. The retained backend/render.yaml is valid when backend/ is selected as the service root, or when a custom setup explicitly supplies rootDir: backend; selecting the nested Blueprint path alone is not enough.
Both Render Blueprints intentionally declare ALLOWED_ORIGINS with sync: false,
so the production value is configured in Render rather than committed to the
repository. In Render Dashboard → annotate-pay-api → Environment, add or update
ALLOWED_ORIGINS with exactly http://localhost:3000,https://annotate-pay.vercel.app,
then Save Changes → Manual Deploy → Deploy latest commit. The Vercel origin has no
trailing slash. No FastAPI code change is required for CORS.
See .env.example.
| Variable | Used by | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
web | Supabase project URL; required |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
web | Public anon key (RLS applies); required |
SUPABASE_SERVICE_ROLE_KEY |
optional admin only | Bypass RLS (never expose to browser; API does not require it) |
NEXT_PUBLIC_API_URL |
web | FastAPI base URL; required for deployment |
SUPABASE_JWT_SECRET |
backend | Verify Supabase JWTs (HS256); required |
SUPABASE_URL |
backend | Supabase project URL for the PostgREST client; required |
SUPABASE_ANON_KEY |
backend | PostgREST client key (forwards user JWT); required |
ALLOWED_ORIGINS |
backend | CORS origins (comma-separated); required |
PORT |
backend | Render supplies this; optional locally |
ALLRATES_API_KEY |
backend | Optional private AllRatesToday Bearer key; never frontend |
FX_CACHE_TTL_SECONDS |
backend | Optional positive FX cache TTL; default/recommendation 3600 |
No FX key is required for fallback operation. When configured, AllRatesToday is tried first and the backend falls back safely if it is unavailable.
For the exact private Render action: open Render Dashboard → annotate-pay-api → Environment → Add Environment Variable → add ALLRATES_API_KEY with the private value → add FX_CACHE_TTL_SECONDS with 3600 → Save Changes → Manual Deploy → Deploy latest commit. Do not add the key to Vercel or frontend environment variables.
annotate_pay/
├── render.yaml # Root Render Blueprint (service rootDir: backend)
├── web/ # Next.js App Router (dashboard, logs, projects, analytics)
│ └── src/
│ ├── app/ # Routes: login, dashboard, logs, projects, analytics
│ ├── components/ # UI, forms, charts, layout
│ ├── hooks/ # React Query data hooks
│ ├── lib/ # Supabase clients, API client, earnings math
│ └── types/ # Shared TS types (matches SQL column names)
├── backend/ # FastAPI (analytics, FX, CSV/XLSX export, preview)
│ ├── main.py
│ ├── requirements.txt
│ ├── render.yaml # Backend-root Blueprint alternative
│ ├── Dockerfile # Optional container deploy
│ └── app/
│ ├── auth.py # Supabase JWT (HS256) verification
│ ├── config.py # Env / CORS
│ ├── deps.py # Auth + PostgREST client (user JWT → RLS)
│ ├── models/schemas.py
│ ├── routers/ # health, analytics, exports, calculations, fx
│ └── services/ # earnings, analytics aggregation, export builders, FX
├── supabase/
│ └── migrations/
│ ├── 20260807_initial_schema.sql
│ ├── 20260808_aht_minutes_currency_i18n.sql
│ ├── 20260809_payment_status.sql
│ └── 20260810_onboarding_status.sql
├── .env.example
├── .gitignore
├── README.md
└── stitch_annotatepay_saas_dashboard.zip # HTML prototype reference
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
No | Liveness |
GET |
/api/v1/analytics/summary |
Bearer | KPIs + series (group_by=month|project) |
GET |
/api/v1/exports/task-logs |
Bearer | CSV or XLSX download |
POST |
/api/v1/calculations/preview |
Bearer | USD math preview plus optional display conversion |
GET |
/api/v1/fx/rates |
No | Current {code, rate_to_usd} list plus as_of, source, and stale; supports optional refresh=true |
GET |
/api/v1/fx/rate/{currency} |
No | Current {currency, rate_to_usd, as_of, source, stale}; supports optional refresh=true |
Aggregations and exports use snapshot columns only (snapshot_aht_*, hourly_rate_used) — never current project AHT. FX endpoints provide current display rates; they do not change canonical USD aggregates or exports.
| Capa | Stack | Hosting |
|---|---|---|
| Frontend | Next.js (App Router) | Vercel |
| API backend | FastAPI (Python) | Render (plan gratuito) |
| Base de datos / Auth | Supabase (PostgreSQL + Auth) | Supabase |
web/ → Cliente Next.js (Vercel)
backend/ → API FastAPI (Render)
supabase/ → Migraciones SQL y esquema
La autenticación usa Supabase Auth (auth.users). Los datos de la app están en public.profiles, public.projects y public.task_logs con Row Level Security (RLS).
La aplicación web usa inglés por defecto y ofrece español como opción. La interfaz del producto usa terminología de proyectos en todo momento.
El AHT se introduce y se guarda en minutos para los proyectos y los snapshots inmutables de los logs. La fórmula divide el AHT ponderado en minutos entre 60 para convertirlo a horas.
horas = (tasks_attempter * snapshot_aht_attempter
+ tasks_reviewer * snapshot_aht_reviewer) / 60.0
calculated_earnings = horas * hourly_rate_used
USD es la moneda contable canónica. global_hourly_rate es una tarifa por hora editable en USD para logs futuros; al registrar un log, sus snapshots de hourly_rate_used y ganancias (calculated_earnings / calculated_earnings_usd) permanecen como valores USD inmutables. preferred_currency solo cambia los valores mostrados: la aplicación obtiene los tipos de cambio actuales desde FastAPI y nunca reescribe los valores contables históricos en USD.
Al INSERTAR, un trigger BEFORE INSERT:
- Carga el AHT actual del proyecto (
current_aht_attempter/current_aht_reviewer) en minutos - Carga la tarifa
global_hourly_ratedel perfil en USD - Sobrescribe los campos de snapshot enviados por el cliente (integridad)
- Verifica que
user_idcoincida conproject.user_id - Calcula
calculated_earningsusando AHT en minutos - Fuerza los metadatos USD canónicos y copia el resultado a
calculated_earnings_usd
Al ACTUALIZAR, solo pueden cambiar date, tasks_attempter y tasks_reviewer. Las columnas de snapshot, hourly_rate_used y los metadatos contables son inmutables. Las ganancias se recalculan con los snapshots en minutos ya guardados en la fila — nunca con el AHT actual del proyecto. Cambiar el AHT del proyecto o la tarifa global en USD no reescribe el historial.
El estado de pago se guarda por log de tareas en task_logs.payment_status y puede ser pending o paid; paid_at guarda la marca de tiempo del estado pagado declarado por el usuario. El estado es por log porque un proyecto puede pagarse en varias cuotas. Es una contabilidad autodeclarada, no una verificación de pagos externa.
El dashboard muestra ganancias brutas, ganancias adquiridas/pagadas y ganancias pendientes en la moneda de visualización seleccionada. La contabilidad y las ganancias almacenadas siguen siendo USD canónicos; seleccionar otra moneda solo cambia la presentación.
La conversión de FX solo cambia la visualización; la contabilidad sigue siendo USD canónico. Las ganancias almacenadas, los agregados y los exports no se reescriben al cambiar la moneda de visualización.
- Cuando se configura
ALLRATES_API_KEY, el endpoint autenticado de AllRatesToday es el proveedor principal y solicita las monedas de visualización requeridas en un lote. Sin clave, o si falla, la API usa el endpoint abierto de ExchangeRate-API y después Frankfurter como respaldos. ALLRATES_API_KEYes solo para el backend. Nunca debe estar en el código del frontend, variablesNEXT_PUBLIC_*ni Vercel. Render la guarda de forma privada.- La ventana normal de actualización en memoria es de 3600 segundos (una hora) por defecto;
FX_CACHE_TTL_SECONDSpermite cambiarla. Se recomienda una hora o más para proteger la cuota del nivel gratuito.refresh=trueomite la caché y puede consumir una solicitud, así que no se debe consultar continuamente. - Las respuestas correctas incluyen la marca de tiempo publicada y la fuente; si falla una actualización después de obtener tasas, se conservan las últimas tasas conocidas y se marcan como desactualizadas. Los errores 401/429 de AllRatesToday pasan de forma segura al siguiente proveedor.
- El selector de moneda tiene un control de actualización manual y muestra la marca de tiempo de actualización del proveedor, la fuente y el estado de desactualización.
- Ambos endpoints de FX admiten
refresh=truepara omitir la ventana normal en memoria y forzar una consulta al proveedor actual. rate_to_usdes el valor en USD de 1 unidad de la moneda objetivo. Los proveedores devuelven unidades de la moneda objetivo por 1 USD, por lo que el backend invierte esas cotizaciones. FX solo cambia la visualización; las ganancias almacenadas y la contabilidad en USD nunca se reescriben.- Los proveedores gratuitos publican las últimas tasas disponibles, no precios de mercado tick a tick. Si se requieren cotizaciones de mercado verdaderamente en tiempo real, se necesita una clave de un proveedor de pago/en vivo.
Los proyectos se pueden eliminar desde la interfaz. La advertencia de confirmación explica que eliminar un proyecto elimina permanentemente todos sus logs de tareas mediante la cascada de clave foránea de la base de datos. Esta acción no se puede deshacer.
Aplica las migraciones en este orden:
supabase/migrations/20260807_initial_schema.sqlsupabase/migrations/20260808_aht_minutes_currency_i18n.sqlsupabase/migrations/20260809_payment_status.sqlsupabase/migrations/20260810_onboarding_status.sql
La migración 20260808_aht_minutes_currency_i18n.sql debe ejecutarse después de la migración inicial. Si el esquema inicial se aplicó manualmente, ejecútala una sola vez en el SQL Editor de Supabase. Su guardia aht_unit evita una segunda conversión de AHT. Convierte una sola vez los valores existentes de AHT de segundos a minutos, añade los metadatos de moneda e idioma e instala los triggers de minutos/USD. Ejecuta 20260809_payment_status.sql después para añadir la contabilidad de pagos por log de tareas. Ejecuta 20260810_onboarding_status.sql después de la migración de pagos y antes de desplegar el frontend que usa profiles.onboarding_status. Añade el estado persistente del tutorial por usuario (pending, skipped o completed); los perfiles existentes parten como pending.
Opción A – CLI de Supabase
supabase login
supabase link --project-ref <tu-project-ref>
supabase db pushsupabase db push aplica todas las migraciones pendientes en orden. Si la migración inicial ya figura como aplicada, aplica las tres migraciones de seguimiento como las migraciones pendientes restantes.
Opción B – SQL en el Dashboard
- Supabase Dashboard → SQL Editor
- Pegar y ejecutar
supabase/migrations/20260807_initial_schema.sql - Pegar y ejecutar
supabase/migrations/20260808_aht_minutes_currency_i18n.sql - Pegar y ejecutar
supabase/migrations/20260809_payment_status.sql - Pegar y ejecutar
supabase/migrations/20260810_onboarding_status.sql
Si la migración inicial ya está aplicada, omite el paso 2. Si la migración 20260808 ya está aplicada, omite el paso 3. Ejecuta la migración de pagos después de 20260808 y después ejecuta 20260810_onboarding_status.sql, antes de desplegar el frontend que usa profiles.onboarding_status.
Usa el archivo de ejemplo de cada aplicación y completa valores locales sin confirmar secretos:
- Web (
web/.env.example):NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY,NEXT_PUBLIC_API_URL - Backend (
backend/.env.example):SUPABASE_URL,SUPABASE_ANON_KEY,SUPABASE_JWT_SECRET,ALLOWED_ORIGINS,ALLRATES_API_KEYopcional yFX_CACHE_TTL_SECONDS PORTes opcional en local y Render lo proporciona en producción.ALLRATES_API_KEYes configuración privada del backend; no existe una clave para el frontend.- La clave service-role de Supabase solo es opcional para tareas administrativas separadas; la web y la API no la necesitan.
cd web
npm install
npm run dev
# http://localhost:3000cd backend
python -m venv venv
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# definir SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_JWT_SECRET, ALLOWED_ORIGINS
uvicorn main:app --reload --port 8000
# http://localhost:8000 · docs: /docs · health: /health| Componente | Pasos |
|---|---|
| Supabase | Crear proyecto → aplicar la migración inicial y después 20260808_aht_minutes_currency_i18n.sql, 20260809_payment_status.sql y 20260810_onboarding_status.sql en ese orden (supabase db push o SQL Editor); ejecutar la migración de onboarding antes de desplegar el frontend que usa profiles.onboarding_status |
| Vercel | Definir Root Directory como web/; configurar NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY y NEXT_PUBLIC_API_URL (la URL de FastAPI desplegada); desplegar |
| Render | Desde la raíz del repositorio, crear un Blueprint con render.yaml (define backend/ como Root Directory del servicio); establecer ALLOWED_ORIGINS como http://localhost:3000,https://annotate-pay.vercel.app; configurar las variables de Supabase; añadir opcionalmente ALLRATES_API_KEY privada y FX_CACHE_TTL_SECONDS=3600; desplegar |
Para un Blueprint de Render desde la raíz del repositorio, usa el render.yaml de nivel raíz. El backend/render.yaml conservado es válido cuando se selecciona backend/ como raíz del servicio o cuando una configuración personalizada define explícitamente rootDir: backend; seleccionar solo la ruta del Blueprint anidado no es suficiente.
Ambos Blueprints de Render declaran intencionadamente ALLOWED_ORIGINS con
sync: false, por lo que el valor de producción se configura en Render y no se
confirma en el repositorio. En Render Dashboard → annotate-pay-api → Environment,
añadir o actualizar ALLOWED_ORIGINS exactamente como
http://localhost:3000,https://annotate-pay.vercel.app; después, Save Changes →
Manual Deploy → Deploy latest commit. El origen de Vercel no lleva barra final. No
es necesario cambiar el código FastAPI para CORS.
Ver .env.example.
| Variable | Usado por | Propósito |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
web | URL del proyecto Supabase; obligatorio |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
web | Clave anon pública (aplica RLS); obligatorio |
SUPABASE_SERVICE_ROLE_KEY |
admin opcional | Bypass RLS (nunca en el navegador; la API no la requiere) |
NEXT_PUBLIC_API_URL |
web | URL base de FastAPI; obligatorio en despliegue |
SUPABASE_JWT_SECRET |
backend | Verificar JWTs de Supabase (HS256); obligatorio |
SUPABASE_URL |
backend | URL del proyecto Supabase para PostgREST; obligatorio |
SUPABASE_ANON_KEY |
backend | Clave para PostgREST (reenvía el JWT del usuario); obligatorio |
ALLOWED_ORIGINS |
backend | Orígenes CORS (separados por coma); obligatorio |
PORT |
backend | Render lo proporciona; opcional en local |
ALLRATES_API_KEY |
backend | Clave Bearer privada opcional de AllRatesToday; nunca en frontend |
FX_CACHE_TTL_SECONDS |
backend | TTL opcional positivo de caché FX; predeterminado/recomendado 3600 |
No se requiere una clave FX para el funcionamiento con respaldos. Cuando se configura, AllRatesToday se prueba primero y el backend cambia de forma segura a los otros proveedores si no está disponible.
Acción privada exacta en Render: abrir Render Dashboard → annotate-pay-api → Environment → Add Environment Variable → añadir ALLRATES_API_KEY con el valor privado → añadir FX_CACHE_TTL_SECONDS con 3600 → Save Changes → Manual Deploy → Deploy latest commit. No añadir la clave a Vercel ni a variables del frontend.
annotate_pay/
├── render.yaml # Blueprint raíz de Render (rootDir del servicio: backend)
├── web/ # Next.js App Router (dashboard, logs, projects, analytics)
│ └── src/
│ ├── app/ # Rutas: login, dashboard, logs, projects, analytics
│ ├── components/ # UI, formularios, gráficos, layout
│ ├── hooks/ # Hooks de React Query
│ ├── lib/ # Clientes Supabase, API, fórmula de ganancias
│ └── types/ # Tipos TS (nombres de columnas SQL)
├── backend/ # FastAPI (analytics, FX, export CSV/XLSX, preview)
│ ├── main.py
│ ├── requirements.txt
│ ├── render.yaml # Alternativa de Blueprint con raíz backend
│ ├── Dockerfile # Deploy opcional con contenedor
│ └── app/
│ ├── auth.py # Verificación JWT Supabase (HS256)
│ ├── config.py # Env / CORS
│ ├── deps.py # Auth + cliente PostgREST (JWT → RLS)
│ ├── models/schemas.py
│ ├── routers/ # health, analytics, exports, calculations, fx
│ └── services/ # earnings, agregación, builders de export, FX
├── supabase/
│ └── migrations/
│ ├── 20260807_initial_schema.sql
│ ├── 20260808_aht_minutes_currency_i18n.sql
│ ├── 20260809_payment_status.sql
│ └── 20260810_onboarding_status.sql
├── .env.example
├── .gitignore
├── README.md
└── stitch_annotatepay_saas_dashboard.zip # Prototipo HTML de referencia
| Método | Ruta | Auth | Descripción |
|---|---|---|---|
GET |
/health |
No | Liveness |
GET |
/api/v1/analytics/summary |
Bearer | KPIs + series (group_by=month|project) |
GET |
/api/v1/exports/task-logs |
Bearer | Descarga CSV o XLSX |
POST |
/api/v1/calculations/preview |
Bearer | Preview de cálculo USD con conversión mostrada opcional |
GET |
/api/v1/fx/rates |
No | Lista actual de {code, rate_to_usd} y as_of, source y stale; admite refresh=true opcional |
GET |
/api/v1/fx/rate/{currency} |
No | {currency, rate_to_usd, as_of, source, stale} actual; admite refresh=true opcional |
Las agregaciones y exports usan solo columnas snapshot (snapshot_aht_*, hourly_rate_used) — nunca el AHT actual del proyecto. Los endpoints FX proporcionan tipos actuales para mostrar valores; no cambian los agregados ni exports canónicos en USD.