🎭 MODO DEMO ACTIVO — Los datos son ficticios · Toca para salir
JELIAN FT
JELIANFT
COACH PANEL v4
JELIANFT
Coach Panel v4
JELIANFT
Dashboard
Resumen de tu práctica
🏃 Actividad de clientes
🍽 Nutrición hoy
Cargando…
Asesorados recientes
Acciones rápidas
⚠️ Pagos vencidos
Asesorados
0 clientes
IA Coach
Genera rutinas y planes · Manda por WhatsApp
✋ Crear rutina manual
Tú eliges los ejercicios de tu biblioteca; la nota técnica y el video se agregan solos. Sin IA.
Modo de generación
El modo determinístico requiere seleccionar un asesorado abajo (usa su peso, estatura, edad, lesiones y condiciones de salud guardadas en su ficha) y no consume cuota de IA.
Los días restantes serán tratados como días de descanso con menos carbos y sin comidas pre/post entreno.
Plan generado
📸 Análisis corporal por IA
Sube fotos (frontal, lateral, posterior) y la IA estimará composición corporal, postura y recomendaciones.
📷Agregar foto
Resultado análisis
🔁 Comparación con análisis anterior
💊 Stack de suplementación IA
Stack recomendado
Configuración
La guía de aproximaciones se genera sobre esta rutina exacta — mismos días, mismos ejercicios, mismos pesos de trabajo.
Guía de aproximaciones
Configuración
Se genera la descarga estándar (criterio Jelian Biomechanics) sobre esta rutina exacta — mismos ejercicios, ≈-40% de volumen (rango 30-50%), RIR 3-4, sin fallo. Misma vista y mismo PDF que la rutina.
Semana de descarga
🔧 Ajustar esta descarga
Configuración
Genera el protocolo de test para los compuestos clave de esta rutina. Si el asesorado tiene un test anterior guardado, se compara automáticamente contra ese.
Protocolo de test de fuerza
💡 Después de la sesión de test, abre este documento guardado y usa "✏️ Editar" para anotar los pesos y reps reales logrados — así el próximo test generado comparará automáticamente contra estos resultados.
Configuración
Analiza las últimas sesiones registradas de cada ejercicio de esta rutina y sugiere la carga real para la próxima sesión — no la regla genérica de "sube X kg cuando completes Y reps".
Progresión sugerida
Configuración
Revisa los compuestos clave de la rutina en las últimas semanas de sesiones registradas — si alguno lleva 3+ semanas sin progresar carga ni reps, te avisa y sugiere: deload, cambio de ejercicio, o ajuste técnico.
Diagnóstico de estancamiento
Configuración
Peso corporalBandasMancuernasDominadasKettlebellBancoTRXGym de hotel
Marca solo lo que sepas que va a tener disponible. Por defecto: peso corporal + banda (el mínimo común de cualquier viaje).
Mantiene la misma prioridad de grupos musculares que la rutina normal, pero comprimida a los días disponibles.
Rutina de viaje
Configuración
Solo se reemplazan los ejercicios que agravarían esta molestia — el resto de la rutina queda exactamente igual, sin regenerar nada.
Rutina adaptada
Configuración
Recopila las sesiones de los últimos 7 días, calcula adherencia y detecta PRs de la semana — arma un mensaje corto listo para copiar y enviar por WhatsApp.
Resumen semanal
Configuración
Combina medidas corporales, fotos de análisis y rendimiento del período en un documento presentable listo para imprimir/guardar como PDF.
Reporte generado
Escaneo de riesgo
Cruza inasistencia a sesiones, caída de adherencia, pagos vencidos y notas del coach con calificación baja — en todos tus asesorados activos a la vez.
Asesorado:
💬 Chat IA
🤖
Hola, soy tu IA Coach. Selecciona un asesorado arriba — cada uno tiene su propio historial de chat guardado.
🧮 Calculadoras nutricionales
Calcula TMB, TDEE y distribución de macros. El resultado se puede inyectar directamente en el generador de plan de comidas.
—
TMB (kcal)
—
TDEE (kcal)
—
Objetivo (kcal)
—g
Proteína
—g
Carbohidratos
—g
Grasas
📁 Mis plantillas guardadas
Guarda rutinas o planes que uses frecuentemente para reutilizarlos sin llamar a la IA.
📁
Sin plantillas
Genera una rutina y guárdala como plantilla
Estadísticas
Resumen avanzado de tu práctica
Biblioteca de ejercicios
La IA y el generador de rutinas solo usan estos ejercicios. Cada video se guarda en la nube y le llega al atleta automáticamente.
📲 Invitar asesorado
Solo ingresa el nombre y WhatsApp. El asesorado completará su perfil al abrir el link.
Las solicitudes llevan datos personales (WhatsApp, lesiones), así que ahora solo se ven con sesión iniciada. Escribe el usuario que creaste en Supabase (ej.: jalmar). Se pide una vez por dispositivo.
🤖 IA en el servidor
Tus llaves de Claude y Gemini ya no se guardan en este dispositivo ni en tu base de datos: viven como secretos del servidor (Supabase → Edge Functions → Secrets) y la app llama a tu función ai-proxy con tu sesión de coach.
Para cambiar una llave: créala nueva en Anthropic / Google AI Studio y reemplaza el secreto ANTHROPIC_API_KEY o GEMINI_API_KEY. No hay que tocar la app.
💾 Datos
Supabase (nube)
Local
📋 SQL Supabase — Migración completa (idempotente)
Copia y ejecuta en Supabase → SQL Editor → Run. Es seguro ejecutarlo múltiples veces.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- ══════════════════════════════════════════════
-- TABLAS BASE
-- ══════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS clients (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE clients ADD COLUMN IF NOT EXISTS name text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS phone text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS email text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS age numeric;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS weight numeric;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS height numeric;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS bf numeric;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS sex text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS activity text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS goal text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS notes text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS currency text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS status text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS "proteinMult" numeric;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS "createdAt" text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS "updatedAt" text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS onboarding_pending boolean DEFAULT false;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS equipment text DEFAULT '';
ALTER TABLE clients ADD COLUMN IF NOT EXISTS "macroOverride" jsonb;
ALTER TABLE clients ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_clients" ON clients;
CREATE TABLE IF NOT EXISTS progress (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE progress ADD COLUMN IF NOT EXISTS "clientId" text;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS date text;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS weight numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS bf numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS waist numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS chest numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS bicep numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS hip numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS thigh numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS calf numeric;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS notes text;
ALTER TABLE progress ADD COLUMN IF NOT EXISTS "updatedAt" text;
ALTER TABLE progress ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_progress" ON progress;
CREATE TABLE IF NOT EXISTS payments (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE payments ADD COLUMN IF NOT EXISTS "clientId" text;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS amount numeric;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS currency text;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS service text;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS date text;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS due text;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS status text;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS notes text;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS "updatedAt" text;
ALTER TABLE payments ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_payments" ON payments;
CREATE TABLE IF NOT EXISTS events (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE events ADD COLUMN IF NOT EXISTS "clientId" text;
ALTER TABLE events ADD COLUMN IF NOT EXISTS date text;
ALTER TABLE events ADD COLUMN IF NOT EXISTS "time" text;
ALTER TABLE events ADD COLUMN IF NOT EXISTS type text;
ALTER TABLE events ADD COLUMN IF NOT EXISTS notes text;
ALTER TABLE events ADD COLUMN IF NOT EXISTS "updatedAt" text;
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_events" ON events;
CREATE TABLE IF NOT EXISTS session_notes (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE session_notes ADD COLUMN IF NOT EXISTS "clientId" text;
ALTER TABLE session_notes ADD COLUMN IF NOT EXISTS date text;
ALTER TABLE session_notes ADD COLUMN IF NOT EXISTS type text;
ALTER TABLE session_notes ADD COLUMN IF NOT EXISTS body text;
ALTER TABLE session_notes ADD COLUMN IF NOT EXISTS rating text;
ALTER TABLE session_notes ADD COLUMN IF NOT EXISTS "updatedAt" text;
ALTER TABLE session_notes ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_session_notes" ON session_notes;
CREATE TABLE IF NOT EXISTS goals (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE goals ADD COLUMN IF NOT EXISTS "clientId" text;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS "desc" text;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS "start" numeric;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS target numeric;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS "current" numeric;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS unit text;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS sdate text;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS edate text;
ALTER TABLE goals ADD COLUMN IF NOT EXISTS "updatedAt" text;
ALTER TABLE goals ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_goals" ON goals;
CREATE TABLE IF NOT EXISTS templates (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE templates ADD COLUMN IF NOT EXISTS name text;
ALTER TABLE templates ADD COLUMN IF NOT EXISTS type text;
ALTER TABLE templates ADD COLUMN IF NOT EXISTS tags text;
ALTER TABLE templates ADD COLUMN IF NOT EXISTS content text;
ALTER TABLE templates ADD COLUMN IF NOT EXISTS "createdAt" text;
ALTER TABLE templates ADD COLUMN IF NOT EXISTS "updatedAt" text;
ALTER TABLE templates ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_templates" ON templates;
CREATE TABLE IF NOT EXISTS ai_history (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE ai_history ADD COLUMN IF NOT EXISTS data jsonb;
ALTER TABLE ai_history ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_ai_history" ON ai_history;
CREATE TABLE IF NOT EXISTS workout_sessions (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS client_id text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS client_name text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS date text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS routine_name text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS "routineName" text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS day_name text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS "dayName" text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS total_sets int;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS "totalSets" int;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS total_reps int;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS "totalReps" int;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS completed_exs int;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS "completedExs" int;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS duration int;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS summary text;
ALTER TABLE workout_sessions ADD COLUMN IF NOT EXISTS log jsonb;
ALTER TABLE workout_sessions ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_workout_sessions" ON workout_sessions;
CREATE TABLE IF NOT EXISTS coach_config (id text PRIMARY KEY DEFAULT 'coach', created_at timestamptz DEFAULT now());
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS phone text;
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS name text DEFAULT 'JELIAN FT';
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS updated_at timestamptz DEFAULT now();
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS ai_key_tombstones jsonb DEFAULT '{}';
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS gemini_key text;
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS gemini_key_client text;
ALTER TABLE coach_config ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_coach_config" ON coach_config;
-- Diario de comidas por foto (contador de calorías con IA)
CREATE TABLE IF NOT EXISTS food_logs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id text NOT NULL,
food_name text NOT NULL,
portion_desc text,
kcal numeric NOT NULL DEFAULT 0,
protein_g numeric NOT NULL DEFAULT 0,
carbs_g numeric NOT NULL DEFAULT 0,
fat_g numeric NOT NULL DEFAULT 0,
meal_type text,
confidence text,
source text DEFAULT 'photo_ai',
photo_url text,
logged_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE food_logs ADD COLUMN IF NOT EXISTS photo_url text;
CREATE INDEX IF NOT EXISTS food_logs_client_date_idx ON food_logs (client_id, logged_at DESC);
ALTER TABLE food_logs ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_food_logs" ON food_logs;
-- Bucket de Storage para guardar las fotos reales de las comidas registradas
INSERT INTO storage.buckets (id, name, public)
VALUES ('food-photos', 'food-photos', true)
ON CONFLICT (id) DO NOTHING;
DROP POLICY IF EXISTS "food_photos_insert" ON storage.objects;
DROP POLICY IF EXISTS "food_photos_select" ON storage.objects;
DROP POLICY IF EXISTS "food_photos_delete" ON storage.objects;
-- Configuración de cuántas comidas al día quiere hacer cada atleta, y sus nombres
CREATE TABLE IF NOT EXISTS nutrition_settings (
client_id text PRIMARY KEY,
meal_count integer NOT NULL DEFAULT 4,
meal_labels jsonb NOT NULL DEFAULT '["Desayuno","Almuerzo","Snack","Cena"]',
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE nutrition_settings ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_nutrition_settings" ON nutrition_settings;
-- Fotos de progreso que los clientes envían al coach (bandeja "Fotos recibidas")
CREATE TABLE IF NOT EXISTS client_photos (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id text NOT NULL,
batch_id text NOT NULL,
angle text NOT NULL DEFAULT 'front',
storage_path text NOT NULL,
note text,
status text NOT NULL DEFAULT 'new',
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS client_photos_client_idx ON client_photos (client_id, created_at DESC);
CREATE INDEX IF NOT EXISTS client_photos_status_idx ON client_photos (status);
ALTER TABLE client_photos ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_client_photos" ON client_photos;
-- Bucket PRIVADO: sin enlaces públicos, las fotos solo se abren con enlaces temporales
INSERT INTO storage.buckets (id, name, public)
VALUES ('progress-photos', 'progress-photos', false)
ON CONFLICT (id) DO UPDATE SET public = false;
DROP POLICY IF EXISTS "progress_photos_insert" ON storage.objects;
DROP POLICY IF EXISTS "progress_photos_select" ON storage.objects;
DROP POLICY IF EXISTS "progress_photos_delete" ON storage.objects;
-- Web Push real (alarma del timer de descanso funcionando aunque la app esté cerrada en iOS)
CREATE TABLE IF NOT EXISTS push_subscriptions (
client_id text NOT NULL,
endpoint text NOT NULL,
p256dh text NOT NULL,
auth text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (client_id, endpoint)
);
ALTER TABLE push_subscriptions ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_push_subscriptions" ON push_subscriptions;
CREATE TABLE IF NOT EXISTS scheduled_pushes (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id text NOT NULL,
fire_at timestamptz NOT NULL,
title text NOT NULL,
body text,
tag text,
sent boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS scheduled_pushes_due_idx ON scheduled_pushes (fire_at) WHERE sent = false;
ALTER TABLE scheduled_pushes ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_scheduled_pushes" ON scheduled_pushes;
-- Cron que dispara push-dispatch cada minuto (la función internamente revisa cada
-- 5s durante ~50s, así que la precisión real es de unos segundos, no 1 minuto).
-- Ya completado con tu Project Ref y Anon Key reales — listo para correr tal cual.
CREATE EXTENSION IF NOT EXISTS pg_cron;
CREATE EXTENSION IF NOT EXISTS pg_net;
SELECT cron.schedule(
'dispatch-push-notifications',
'* * * * *',
$$
select net.http_post(
url:='https://wmkomowvdpkeziqmujok.supabase.co/functions/v1/push-dispatch',
headers:=jsonb_build_object('Content-Type','application/json','Authorization','Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Indta29tb3d2ZHBrZXppcW11am9rIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODIwNDIyOTQsImV4cCI6MjA5NzYxODI5NH0.a4qbKljcUaaXFhpTbX9AXM6CmZ2b6feS08Xrgcbj-rM'),
body:='{}'::jsonb
);
$$
);
-- Limpieza diaria de fotos viejas de comida (>75 días) — borra solo la imagen,
-- los datos de calorías/macros quedan para siempre. Corre 1 vez al día, no
-- necesita precisión como el push. Ya completado con tus datos reales.
SELECT cron.schedule(
'cleanup-old-food-photos',
'0 4 * * *',
$$
select net.http_post(
url:='https://wmkomowvdpkeziqmujok.supabase.co/functions/v1/cleanup-old-photos',
headers:=jsonb_build_object('Content-Type','application/json','Authorization','Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Indta29tb3d2ZHBrZXppcW11am9rIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODIwNDIyOTQsImV4cCI6MjA5NzYxODI5NH0.a4qbKljcUaaXFhpTbX9AXM6CmZ2b6feS08Xrgcbj-rM'),
body:='{}'::jsonb
);
$$
);
CREATE TABLE IF NOT EXISTS client_tokens (token text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE client_tokens ADD COLUMN IF NOT EXISTS client_id text;
ALTER TABLE client_tokens ADD COLUMN IF NOT EXISTS sb_url text;
ALTER TABLE client_tokens ADD COLUMN IF NOT EXISTS sb_anon_key text;
ALTER TABLE client_tokens ADD COLUMN IF NOT EXISTS active boolean DEFAULT true;
ALTER TABLE client_tokens ADD COLUMN IF NOT EXISTS last_used timestamptz;
ALTER TABLE client_tokens ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_client_tokens" ON client_tokens;
-- FIX BUG 1: tabla workout_progress (sync de progreso entre dispositivos)
CREATE TABLE IF NOT EXISTS workout_progress (id text PRIMARY KEY, log jsonb, updated_at timestamptz DEFAULT now());
ALTER TABLE workout_progress ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_workout_progress" ON workout_progress;
-- Tombstones (sync de eliminaciones)
CREATE TABLE IF NOT EXISTS tombstones (id text PRIMARY KEY, "deletedAt" timestamptz DEFAULT now());
ALTER TABLE tombstones ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_tombstones" ON tombstones;
-- Biblioteca global de videos por ejercicio
CREATE TABLE IF NOT EXISTS exercise_library (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE exercise_library ADD COLUMN IF NOT EXISTS name text;
ALTER TABLE exercise_library ADD COLUMN IF NOT EXISTS video_url text;
ALTER TABLE exercise_library ADD COLUMN IF NOT EXISTS video_title text;
ALTER TABLE exercise_library ADD COLUMN IF NOT EXISTS "updatedAt" timestamptz DEFAULT now();
ALTER TABLE exercise_library ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_exercise_library" ON exercise_library;
-- Ejercicios que el coach agrega a mano desde la pestaña Biblioteca (además de los ya embebidos en la app)
CREATE TABLE IF NOT EXISTS custom_exercises (id text PRIMARY KEY, created_at timestamptz DEFAULT now());
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS n text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS m text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS c text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS cue text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS eq text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS al jsonb DEFAULT '[]';
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS t text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS p integer;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS g text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS "gEq" text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS "gNota" text;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS rec boolean;
ALTER TABLE custom_exercises ADD COLUMN IF NOT EXISTS "updatedAt" timestamptz DEFAULT now();
ALTER TABLE custom_exercises ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_custom_exercises" ON custom_exercises;
-- ══════════════════════════════════════════════
-- TRIGGERS
-- ══════════════════════════════════════════════
CREATE OR REPLACE FUNCTION trg_normalize_workout_session()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.routine_name IS NULL THEN NEW.routine_name := NEW."routineName"; END IF;
IF NEW.day_name IS NULL THEN NEW.day_name := NEW."dayName"; END IF;
IF NEW.total_sets IS NULL THEN NEW.total_sets := NEW."totalSets"; END IF;
IF NEW.total_reps IS NULL THEN NEW.total_reps := NEW."totalReps"; END IF;
IF NEW.completed_exs IS NULL THEN NEW.completed_exs := NEW."completedExs"; END IF;
RETURN NEW;
END; $$;
DROP TRIGGER IF EXISTS trg_ws_normalize ON workout_sessions;
CREATE TRIGGER trg_ws_normalize BEFORE INSERT OR UPDATE ON workout_sessions
FOR EACH ROW EXECUTE FUNCTION trg_normalize_workout_session();
-- ══════════════════════════════════════════════
-- RPCs
-- ══════════════════════════════════════════════
-- generate_client_token
CREATE OR REPLACE FUNCTION generate_client_token(
p_client_id text, p_sb_url text, p_sb_anon_key text DEFAULT NULL
) RETURNS text LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE v_token text;
BEGIN
UPDATE client_tokens SET active = false WHERE client_id = p_client_id;
v_token := gen_random_uuid()::text;
INSERT INTO client_tokens (token, client_id, sb_url, sb_anon_key, active)
VALUES (v_token, p_client_id, p_sb_url, p_sb_anon_key, true);
RETURN v_token;
END; $$;
-- FIX BUG 2: get_client_data devuelve sb_key para evitar pasar anon key en la URL
CREATE OR REPLACE FUNCTION get_client_data(p_token text)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
v_client_id text;
v_sb_key text;
v_client jsonb;
v_ai jsonb;
v_goals jsonb;
v_progress jsonb;
v_sessions jsonb;
v_coach jsonb;
BEGIN
SELECT client_id, sb_anon_key
INTO v_client_id, v_sb_key
FROM client_tokens
WHERE token = p_token AND active = true;
IF v_client_id IS NULL THEN
RETURN jsonb_build_object('error', 'Token inválido o inactivo');
END IF;
UPDATE client_tokens SET last_used = now() WHERE token = p_token;
SELECT to_jsonb(c) INTO v_client FROM clients c WHERE c.id = v_client_id;
SELECT COALESCE(data -> v_client_id, '{}'::jsonb)
INTO v_ai FROM ai_history WHERE id = 'coach_ai_history';
SELECT COALESCE(jsonb_agg(to_jsonb(g)), '[]'::jsonb)
INTO v_goals FROM goals g WHERE g."clientId" = v_client_id;
SELECT COALESCE(jsonb_agg(to_jsonb(p)), '[]'::jsonb)
INTO v_progress
FROM (SELECT * FROM progress WHERE "clientId" = v_client_id
ORDER BY date DESC LIMIT 20) p;
SELECT COALESCE(jsonb_agg(jsonb_build_object(
'id', ws.id, 'date', ws.date,
'routineName', COALESCE(ws.routine_name, ws."routineName"),
'dayName', COALESCE(ws.day_name, ws."dayName"),
'totalSets', COALESCE(ws.total_sets, ws."totalSets"),
'totalReps', COALESCE(ws.total_reps, ws."totalReps"),
'completedExs', COALESCE(ws.completed_exs,ws."completedExs"),
'duration', ws.duration, 'summary', ws.summary,
'log', ws.log, 'created_at', ws.created_at
) ORDER BY ws.created_at DESC), '[]'::jsonb)
INTO v_sessions
FROM (SELECT * FROM workout_sessions
WHERE client_id = v_client_id
ORDER BY created_at DESC LIMIT 50) ws;
SELECT to_jsonb(cc) INTO v_coach FROM coach_config cc WHERE cc.id = 'coach';
RETURN jsonb_build_object(
'client', v_client,
'aiHistory', COALESCE(v_ai, '{}'),
'goals', COALESCE(v_goals, '[]'),
'progress', COALESCE(v_progress, '[]'),
'sessions', COALESCE(v_sessions, '[]'),
'coach', COALESCE(v_coach, '{}'),
'sb_key', COALESCE(v_sb_key, '')
);
END; $$;
-- push_workout_session
CREATE OR REPLACE FUNCTION push_workout_session(p_token text, p_session jsonb)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE v_client_id text;
BEGIN
SELECT client_id INTO v_client_id
FROM client_tokens WHERE token = p_token AND active = true;
IF v_client_id IS NULL THEN
RETURN jsonb_build_object('error', 'Token inválido');
END IF;
IF p_session->>'id' IS NULL OR p_session->>'id' = '' THEN
RETURN jsonb_build_object('error', 'Session id requerido');
END IF;
UPDATE client_tokens SET last_used = now() WHERE token = p_token;
INSERT INTO workout_sessions (
id, client_id, client_name, date,
routine_name, "routineName", day_name, "dayName",
total_sets, "totalSets", total_reps, "totalReps",
completed_exs, "completedExs", duration, summary, log
) VALUES (
p_session->>'id', v_client_id, p_session->>'client_name', p_session->>'date',
COALESCE(p_session->>'routine_name', p_session->>'routineName'), p_session->>'routineName',
COALESCE(p_session->>'day_name', p_session->>'dayName'), p_session->>'dayName',
COALESCE((p_session->>'total_sets')::int, (p_session->>'totalSets')::int), (p_session->>'totalSets')::int,
COALESCE((p_session->>'total_reps')::int, (p_session->>'totalReps')::int), (p_session->>'totalReps')::int,
COALESCE((p_session->>'completed_exs')::int, (p_session->>'completedExs')::int), (p_session->>'completedExs')::int,
(p_session->>'duration')::int, p_session->>'summary', p_session->'log'
)
ON CONFLICT (id) DO UPDATE SET
summary = EXCLUDED.summary,
log = EXCLUDED.log,
total_sets = EXCLUDED.total_sets,
total_reps = EXCLUDED.total_reps,
completed_exs = EXCLUDED.completed_exs,
duration = EXCLUDED.duration;
RETURN jsonb_build_object('ok', true);
END; $$;
-- FIX BUG 4: complete_onboarding integrado en migración principal
CREATE OR REPLACE FUNCTION complete_onboarding(p_token text, p_data jsonb)
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE v_client_id text;
BEGIN
SELECT client_id INTO v_client_id
FROM client_tokens WHERE token = p_token AND active = true;
IF v_client_id IS NULL THEN
RETURN jsonb_build_object('error', 'Token inválido');
END IF;
UPDATE clients SET
name = COALESCE(p_data->>'name', name),
age = COALESCE((p_data->>'age')::numeric, age),
sex = COALESCE(p_data->>'sex', sex),
weight = COALESCE((p_data->>'weight')::numeric, weight),
height = COALESCE((p_data->>'height')::numeric, height),
goal = COALESCE(p_data->>'goal', goal),
activity = COALESCE(p_data->>'activity', activity),
notes = COALESCE(p_data->>'notes', notes),
equipment = COALESCE(p_data->>'equipment', equipment),
onboarding_pending = false,
"updatedAt" = now()::text
WHERE id = v_client_id;
UPDATE client_tokens SET last_used = now() WHERE token = p_token;
RETURN jsonb_build_object('ok', true, 'client_id', v_client_id);
END; $$;
-- ════════════════════════════════════════════════════════════════════
-- JELIAN FT — Migración de las 4 funciones nuevas (idempotente)
-- Ejecutar en Supabase → SQL Editor → Run. Es seguro correrlo varias veces.
-- 1) Check-in semanal estructurado → weekly_checkins
-- 3) Puerta de entrada (aplica a asesoría) → client_applications
-- 4) Avisos de renovación y cobro → columnas nuevas en coach_config
-- 5) Tarjeta de resultados (con permiso) → result_consents
-- Las fotos del check-in reutilizan tu bucket privado "progress-photos" y la
-- tabla "client_photos" que ya existen (no hay que crear nada para eso).
-- ════════════════════════════════════════════════════════════════════
-- ── 1) CHECK-IN SEMANAL ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS weekly_checkins (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id text NOT NULL,
week_start date NOT NULL, -- lunes de la semana del check-in
weight_kg numeric,
sleep_hours numeric,
sleep_quality integer, -- 1 (mala) a 5 (excelente)
stress integer, -- 1 (muy bajo) a 5 (muy alto)
training_adherence integer, -- 0-100 (% de entrenos cumplidos)
nutrition_adherence integer, -- 0-100 (% del plan de comida cumplido)
note text,
photo_batch_id text, -- lote en client_photos (empieza con ci_)
coach_reply text,
coach_adjustments text,
replied_at timestamptz,
reply_seen_at timestamptz, -- cuándo el asesorado abrió tu respuesta
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (client_id, week_start)
);
CREATE INDEX IF NOT EXISTS weekly_checkins_client_idx ON weekly_checkins (client_id, week_start DESC);
CREATE INDEX IF NOT EXISTS weekly_checkins_pending_idx ON weekly_checkins (created_at DESC) WHERE replied_at IS NULL;
ALTER TABLE weekly_checkins ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_weekly_checkins" ON weekly_checkins;
-- ── 5) PERMISO PARA COMPARTIR RESULTADOS ────────────────────────────
-- mode: none (no) · named (con su nombre) · anonymous (solo iniciales)
-- source: app (lo dio el asesorado en su app) · coach (lo confirmaste tú, p. ej. por WhatsApp)
CREATE TABLE IF NOT EXISTS result_consents (
client_id text PRIMARY KEY,
mode text NOT NULL DEFAULT 'none',
source text NOT NULL DEFAULT 'app',
note text,
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE result_consents ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "allow_all_result_consents" ON result_consents;
-- ── 3) SOLICITUDES DE ASESORÍA (formulario público aplicar.html) ────
CREATE TABLE IF NOT EXISTS client_applications (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
full_name text NOT NULL,
whatsapp text,
email text,
country text,
age integer,
sex text,
weight_kg numeric, -- peso en kg (formulario aplicar.html)
height_cm numeric, -- estatura en cm
goal text, -- cut · bulk · recomp · maintain · performance · rehab · other
goal_detail text,
injuries text,
days_per_week text,
schedule text,
equipment text,
experience text,
plan_interest text,
heard_from text,
source text, -- ?ref= de la URL (instagram, tiktok…)
accepted_terms boolean NOT NULL DEFAULT false,
status text NOT NULL DEFAULT 'new', -- new · contacted · accepted · rejected
coach_notes text,
client_id text, -- asesorado creado al aceptar
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- Si la tabla ya existía, agrega las columnas nuevas (seguro correrlo varias veces)
ALTER TABLE client_applications ADD COLUMN IF NOT EXISTS weight_kg numeric;
ALTER TABLE client_applications ADD COLUMN IF NOT EXISTS height_cm numeric;
CREATE INDEX IF NOT EXISTS client_applications_status_idx ON client_applications (status, created_at DESC);
ALTER TABLE client_applications ENABLE ROW LEVEL SECURITY;
-- SEGURIDAD: el público (clave anon) NO puede leer, editar ni borrar solicitudes.
-- Solo puede enviar una nueva, y únicamente por la función submit_application().
-- Tú las ves y gestionas con tu sesión iniciada en el panel (rol "authenticated").
-- IMPORTANTE: en Supabase → Authentication → Sign In / Providers, desactiva
-- "Allow new users to sign up"; si no, cualquiera podría crearse una cuenta.
DROP POLICY IF EXISTS "allow_all_client_applications" ON client_applications;
DROP POLICY IF EXISTS "coach_all_client_applications" ON client_applications;
CREATE POLICY "coach_all_client_applications" ON client_applications
FOR ALL TO authenticated USING (true) WITH CHECK (true);
REVOKE ALL ON client_applications FROM anon;
-- Envío público: valida, evita duplicados (mismo WhatsApp con solicitud abierta en
-- los últimos 14 días → actualiza esa en vez de crear otra) y frena el spam.
CREATE OR REPLACE FUNCTION submit_application(p jsonb) RETURNS text
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
DECLARE
v_name text := left(btrim(coalesce(p->>'full_name', '')), 120);
v_digits text := regexp_replace(coalesce(p->>'whatsapp', ''), '\D', '', 'g');
v_age integer;
v_weight numeric;
v_height numeric;
v_sex text := upper(nullif(btrim(coalesce(p->>'sex', '')), ''));
v_goal text := coalesce(nullif(btrim(p->>'goal'), ''), 'other');
v_dup uuid;
BEGIN
IF length(v_name) < 3 THEN RAISE EXCEPTION 'invalid_name'; END IF;
IF length(v_digits) < 8 OR length(v_digits) > 15 THEN RAISE EXCEPTION 'invalid_whatsapp'; END IF;
IF coalesce(p->>'accepted_terms', 'false') <> 'true' THEN RAISE EXCEPTION 'terms_required'; END IF;
BEGIN v_age := nullif(p->>'age', '')::integer; EXCEPTION WHEN others THEN v_age := NULL; END;
IF v_age IS NOT NULL AND (v_age < 14 OR v_age > 80) THEN v_age := NULL; END IF;
BEGIN v_weight := replace(nullif(p->>'weight_kg', ''), ',', '.')::numeric; EXCEPTION WHEN others THEN v_weight := NULL; END;
IF v_weight IS NOT NULL AND (v_weight < 30 OR v_weight > 300) THEN v_weight := NULL; END IF;
BEGIN v_height := replace(nullif(p->>'height_cm', ''), ',', '.')::numeric; EXCEPTION WHEN others THEN v_height := NULL; END;
IF v_height IS NOT NULL AND (v_height < 120 OR v_height > 230) THEN v_height := NULL; END IF;
IF v_sex IS NOT NULL AND v_sex NOT IN ('M', 'F') THEN v_sex := NULL; END IF;
IF v_goal NOT IN ('cut', 'bulk', 'recomp', 'maintain', 'performance', 'rehab', 'other') THEN v_goal := 'other'; END IF;
-- Duplicado: la misma persona ya tiene una solicitud abierta → se refresca con los datos nuevos
SELECT id INTO v_dup FROM client_applications
WHERE regexp_replace(coalesce(whatsapp, ''), '\D', '', 'g') = v_digits
AND status IN ('new', 'contacted')
AND created_at > now() - interval '14 days'
ORDER BY created_at DESC LIMIT 1;
IF v_dup IS NOT NULL THEN
UPDATE client_applications SET
email = coalesce(nullif(left(p->>'email', 160), ''), email),
country = coalesce(nullif(left(p->>'country', 120), ''), country),
age = coalesce(v_age, age), sex = coalesce(v_sex, sex), goal = v_goal,
weight_kg = coalesce(v_weight, weight_kg), height_cm = coalesce(v_height, height_cm),
goal_detail = coalesce(nullif(left(p->>'goal_detail', 500), ''), goal_detail),
injuries = coalesce(nullif(left(p->>'injuries', 800), ''), injuries),
days_per_week = coalesce(nullif(left(p->>'days_per_week', 80), ''), days_per_week),
schedule = coalesce(nullif(left(p->>'schedule', 200), ''), schedule),
equipment = coalesce(nullif(left(p->>'equipment', 200), ''), equipment),
experience = coalesce(nullif(left(p->>'experience', 200), ''), experience),
updated_at = now()
WHERE id = v_dup;
RETURN 'duplicate';
END IF;
-- Anti-spam global: máximo 30 solicitudes nuevas por hora
IF (SELECT count(*) FROM client_applications WHERE created_at > now() - interval '1 hour') >= 30 THEN
RAISE EXCEPTION 'rate_limited';
END IF;
INSERT INTO client_applications (full_name, whatsapp, email, country, age, sex, weight_kg, height_cm, goal, goal_detail, injuries,
days_per_week, schedule, equipment, experience, plan_interest, heard_from, source, accepted_terms, status)
VALUES (v_name, left(btrim(p->>'whatsapp'), 40), nullif(left(p->>'email', 160), ''), nullif(left(p->>'country', 120), ''),
v_age, v_sex, v_weight, v_height, v_goal, nullif(left(p->>'goal_detail', 500), ''), nullif(left(p->>'injuries', 800), ''),
nullif(left(p->>'days_per_week', 80), ''), nullif(left(p->>'schedule', 200), ''), nullif(left(p->>'equipment', 200), ''),
nullif(left(p->>'experience', 200), ''), nullif(left(p->>'plan_interest', 200), ''), nullif(left(p->>'heard_from', 200), ''),
nullif(left(p->>'source', 120), ''), true, 'new');
RETURN 'ok';
END $$;
REVOKE ALL ON FUNCTION submit_application(jsonb) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION submit_application(jsonb) TO anon, authenticated;
-- AVISO PUSH AL COACH: al llegar una solicitud nueva se agenda un push para el
-- "asesorado" especial __coach__ (tu celular, suscrito desde el panel). Lo envía
-- tu función push-dispatch que ya existe; si algo falla aquí, la solicitud se guarda igual.
CREATE OR REPLACE FUNCTION notify_coach_new_application() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
BEGIN
INSERT INTO scheduled_pushes (client_id, fire_at, title, body, tag, sent)
VALUES ('__coach__', now(), '🆕 Nueva solicitud de asesoría',
NEW.full_name || ' · ' || CASE NEW.goal
WHEN 'cut' THEN 'definición' WHEN 'bulk' THEN 'ganar músculo' WHEN 'recomp' THEN 'recomposición'
WHEN 'maintain' THEN 'mantenimiento' WHEN 'performance' THEN 'rendimiento' WHEN 'rehab' THEN 'volver tras lesión'
ELSE 'otro objetivo' END,
'new-application', false);
RETURN NEW;
EXCEPTION WHEN others THEN
RETURN NEW;
END $$;
DROP TRIGGER IF EXISTS trg_notify_coach_new_application ON client_applications;
CREATE TRIGGER trg_notify_coach_new_application AFTER INSERT ON client_applications
FOR EACH ROW EXECUTE FUNCTION notify_coach_new_application();
-- ── 4) AVISOS DE RENOVACIÓN Y COBRO ─────────────────────────────────
-- payment_methods: [{"label":"PayPal","details":"correo@...","scope":"Fuera de Ecuador"}, ...]
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS payment_methods jsonb DEFAULT '[]';
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS payment_note text;
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS renewal_days integer DEFAULT 7;
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS renewal_template text;
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS renewal_push boolean DEFAULT true;
ALTER TABLE coach_config ADD COLUMN IF NOT EXISTS notice_log jsonb DEFAULT '{}';
-- ════════════════════════════════════════════════════════════════════
-- JELIAN FT — SEGURIDAD: reemplaza las políticas "allow_all" (idempotente)
-- Ejecutar en Supabase → SQL Editor → Run. Es seguro correrlo varias veces.
--
-- Resultado:
-- · COACH (sesión iniciada en el panel, rol "authenticated"): acceso total.
-- · ASESORADO (clave anon + cabecera x-client-token que ya manda la app):
-- solo ve y escribe SUS propios datos.
-- · Público sin token: no ve ni escribe nada (salvo enviar la solicitud de
-- aplicar.html y llamar a las funciones que validan token).
--
-- IMPORTANTE (una sola vez): en Supabase → Authentication → Sign In / Providers
-- desactiva "Allow new users to sign up"; si no, cualquiera podría crearse una
-- cuenta y entrar como "authenticated".
-- ════════════════════════════════════════════════════════════════════
-- ── 1) Quién es el asesorado: lee x-client-token y lo valida en client_tokens ──
CREATE OR REPLACE FUNCTION public.jft_token_client() RETURNS text
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$
SELECT ct.client_id
FROM public.client_tokens ct
WHERE ct.active = true
AND ct.token = NULLIF(
(NULLIF(current_setting('request.headers', true), '')::json ->> 'x-client-token'), '')
LIMIT 1
$$;
REVOKE ALL ON FUNCTION public.jft_token_client() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.jft_token_client() TO anon, authenticated;
-- ── 2) Borra TODAS las políticas viejas de estas tablas (allow_all y cualquier otra) ──
DO $$
DECLARE r record;
BEGIN
FOR r IN
SELECT schemaname, tablename, policyname FROM pg_policies
WHERE schemaname = 'public' AND tablename = ANY (ARRAY[
'clients','progress','payments','events','session_notes','goals','templates',
'ai_history','workout_sessions','coach_config','food_logs','nutrition_settings',
'client_photos','push_subscriptions','scheduled_pushes','client_tokens',
'workout_progress','tombstones','exercise_library','custom_exercises',
'weekly_checkins','result_consents'])
LOOP
EXECUTE format('DROP POLICY %I ON %I.%I', r.policyname, r.schemaname, r.tablename);
END LOOP;
END $$;
-- ── 3) COACH: acceso total con sesión iniciada ──────────────────────────────
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'clients','progress','payments','events','session_notes','goals','templates',
'ai_history','workout_sessions','coach_config','food_logs','nutrition_settings',
'client_photos','push_subscriptions','scheduled_pushes','client_tokens',
'workout_progress','tombstones','exercise_library','custom_exercises',
'weekly_checkins','result_consents']
LOOP
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format(
'CREATE POLICY coach_all ON public.%I FOR ALL TO authenticated USING (true) WITH CHECK (true)', t);
END LOOP;
END $$;
-- ── 4) ASESORADO: solo lo suyo (cabecera x-client-token) ────────────────────
-- Tablas con columna client_id: ver / crear / editar / borrar lo propio
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['food_logs','nutrition_settings','client_photos','weekly_checkins','result_consents']
LOOP
EXECUTE format(
'CREATE POLICY client_own ON public.%I FOR ALL TO anon
USING (client_id = public.jft_token_client())
WITH CHECK (client_id = public.jft_token_client())', t);
END LOOP;
END $$;
-- Sincronización del progreso de entrenos (id = id del asesorado)
CREATE POLICY client_own ON public.workout_progress FOR ALL TO anon
USING (id = public.jft_token_client()) WITH CHECK (id = public.jft_token_client());
-- Sus pagos (solo lectura)
CREATE POLICY client_read_own ON public.payments FOR SELECT TO anon
USING ("clientId" = public.jft_token_client());
-- Datos de cobro del coach (solo lectura, sin llaves: ver permisos por columna abajo)
CREATE POLICY client_read ON public.coach_config FOR SELECT TO anon
USING (id = 'coach' AND public.jft_token_client() IS NOT NULL);
-- Biblioteca de videos (solo lectura, para asesorados con token válido)
CREATE POLICY client_read ON public.exercise_library FOR SELECT TO anon
USING (public.jft_token_client() IS NOT NULL);
-- coach_config: el asesorado solo puede leer estas columnas (nunca las llaves de IA)
REVOKE SELECT ON public.coach_config FROM anon;
GRANT SELECT (id, name, phone, payment_methods, payment_note, renewal_days,
renewal_template, renewal_push, updated_at)
ON public.coach_config TO anon;
-- client_tokens: solo el coach (las funciones con token ya corren como SECURITY DEFINER)
REVOKE ALL ON public.client_tokens FROM anon;
-- ── 5) FUNCIONES ────────────────────────────────────────────────────────────
-- Crear token de un asesorado = solo el coach. (Antes cualquiera con la clave
-- pública podía pedir un token de cualquier asesorado y ver todos sus datos.)
REVOKE EXECUTE ON FUNCTION public.generate_client_token(text, text, text) FROM PUBLIC, anon;
GRANT EXECUTE ON FUNCTION public.generate_client_token(text, text, text) TO authenticated;
-- Las funciones SECURITY DEFINER fijan su search_path (buena práctica de Supabase)
ALTER FUNCTION public.generate_client_token(text, text, text) SET search_path = public;
ALTER FUNCTION public.get_client_data(text) SET search_path = public;
ALTER FUNCTION public.push_workout_session(text, jsonb) SET search_path = public;
ALTER FUNCTION public.complete_onboarding(text, jsonb) SET search_path = public;
-- ── 6) STORAGE (fotos) ──────────────────────────────────────────────────────
-- Las rutas son <id_asesorado>/... → cada asesorado solo toca su carpeta.
-- Coach: todo. food-photos sigue siendo público para VER por enlace directo.
DROP POLICY IF EXISTS "food_photos_insert" ON storage.objects;
DROP POLICY IF EXISTS "food_photos_select" ON storage.objects;
DROP POLICY IF EXISTS "food_photos_delete" ON storage.objects;
DROP POLICY IF EXISTS "progress_photos_insert" ON storage.objects;
DROP POLICY IF EXISTS "progress_photos_select" ON storage.objects;
DROP POLICY IF EXISTS "progress_photos_delete" ON storage.objects;
DROP POLICY IF EXISTS "jft_coach_photos" ON storage.objects;
DROP POLICY IF EXISTS "jft_client_photos" ON storage.objects;
CREATE POLICY "jft_coach_photos" ON storage.objects FOR ALL TO authenticated
USING (bucket_id IN ('food-photos', 'progress-photos'))
WITH CHECK (bucket_id IN ('food-photos', 'progress-photos'));
CREATE POLICY "jft_client_photos" ON storage.objects FOR ALL TO anon
USING (bucket_id IN ('food-photos', 'progress-photos')
AND (storage.foldername(name))[1] = public.jft_token_client())
WITH CHECK (bucket_id IN ('food-photos', 'progress-photos')
AND (storage.foldername(name))[1] = public.jft_token_client());
-- ── 7) COMPROBACIÓN (opcional): debe salir CERO filas con "allow_all" ───────
-- SELECT tablename, policyname FROM pg_policies
-- WHERE schemaname IN ('public','storage') AND policyname LIKE 'allow_all%';
🔐 Cambiar contraseña
🔒 Código de verificación
Abre tu app autenticadora y escribe el código de 6 dígitos de JFT Coach.
🔒 Verificación en 2 pasos
📝 Nota de sesión
🎯 Objetivo
📥 Importar de la app anterior
En la app anterior toca Exportar CSV para Supabase (o Descargar catálogo). Elige aquí ese archivo o pega su contenido. Solo se agregan links a ejercicios que no tengan video; no se borra nada.
➕ Nuevo ejercicio
El video se encuentra con cualquiera de estos nombres, aunque la rutina use otro.
Más opciones (nivel, prioridad, Smart Fit)
📤 Aplicar plantilla a asesorado
📁 Guardar plantilla
🔍 Sugerencias de video
Buscando...
📚 Biblioteca de Videos
Los videos aprobados aquí se muestran automáticamente a tus atletas