[{"data":1,"prerenderedAt":3915},["ShallowReactive",2],{"docs-overview":3,"docs-navigation":3906},[4,962,1900,2982],{"id":5,"title":6,"body":7,"description":953,"extension":954,"meta":955,"navigation":956,"path":958,"seo":959,"stem":960,"__hash__":961},"docs\u002Fdocs\u002F1.setup.md","Guía de setup",{"type":8,"value":9,"toc":941},"minimark",[10,28,40,45,127,131,152,156,163,258,272,277,399,403,666,693,731,744,748,784,788,799,805,811,854,865,869,904,908,937],[11,12,13,14,18,19,23,24,27],"p",{},"Esta guía te deja el template corriendo en local, con la base de datos migrada y una cuenta con la que entrar. Todo se ejecuta con ",[15,16,17],"strong",{},"pnpm",", que es el gestor fijado en ",[20,21,22],"code",{},"packageManager"," dentro de ",[20,25,26],{},"package.json",".",[29,30,31],"tip",{},[11,32,33,36,37,27],{},[20,34,35],{},"pnpm install"," instala también los hooks de git: en cada commit corre ESLint sobre lo que esté en stage (segundos) y en cada push la suite completa de Playwright. Para saltar la suite de forma deliberada existe ",[20,38,39],{},"SKIP_E2E=1 git push",[41,42,44],"h2",{"id":43},"requisitos","Requisitos",[46,47,48,64],"table",{},[49,50,51],"thead",{},[52,53,54,58,61],"tr",{},[55,56,57],"th",{},"Herramienta",[55,59,60],{},"Versión",[55,62,63],{},"Para qué",[65,66,67,89,106],"tbody",{},[52,68,69,75,80],{},[70,71,72],"td",{},[15,73,74],{},"Node.js",[70,76,77],{},[20,78,79],{},">= 22.12",[70,81,82,83,85,86],{},"El runtime fijado en ",[20,84,26],{}," → ",[20,87,88],{},"engines",[52,90,91,95,100],{},[70,92,93],{},[15,94,17],{},[70,96,97],{},[20,98,99],{},"10.x",[70,101,102,103],{},"Gestor de paquetes; actívalo con ",[20,104,105],{},"corepack enable",[52,107,108,113,116],{},[70,109,110],{},[15,111,112],{},"PostgreSQL",[70,114,115],{},"15 o superior",[70,117,118,119,126],{},"Base de datos. Sirve una gratuita de ",[120,121,125],"a",{"href":122,"rel":123},"https:\u002F\u002Fneon.tech",[124],"nofollow","Neon"," o una local",[41,128,130],{"id":129},"_1-instala-las-dependencias","1. Instala las dependencias",[132,133,138],"pre",{"className":134,"code":135,"language":136,"meta":137,"style":137},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pnpm install\n","bash","",[20,139,140],{"__ignoreMap":137},[141,142,145,148],"span",{"class":143,"line":144},"line",1,[141,146,17],{"class":147},"sBMFI",[141,149,151],{"class":150},"sfazB"," install\n",[41,153,155],{"id":154},"_2-configura-el-entorno","2. Configura el entorno",[11,157,158,159,162],{},"Crea un ",[20,160,161],{},".env"," en la raíz del proyecto. Estas son las variables que necesitas para arrancar:",[132,164,168],{"className":165,"code":166,"language":167,"meta":137,"style":137},"language-ini shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Base de datos PostgreSQL (usa la connection string completa)\nNUXT_DATABASE_URL=\"postgresql:\u002F\u002Fusuario:password@host:5432\u002Fbase?sslmode=require\"\n\n# Better Auth: cadena larga y aleatoria, distinta en cada entorno\nBETTER_AUTH_SECRET=\"pega-aqui-un-secreto-largo\"\n\n# URL pública de la app (Better Auth la usa como baseURL)\nNUXT_APP_URL=\"http:\u002F\u002Flocalhost:3000\"\n\n# Email del administrador de la instancia: con la tabla `user` vacía, `\u002Fsetup`\n# solo acepta esta dirección para crear la primera cuenta (paso 4).\nNUXT_ADMIN_EMAIL=\"admin@example.com\"\n# Opcional: solo la usa la tarea no interactiva `db:seed_admin`. La contraseña\n# normal se elige en `\u002Fsetup`, no en el entorno.\nNUXT_ADMIN_PASSWORD=\"una-password-larga\"\n","ini",[20,169,170,175,181,188,194,200,205,211,217,222,228,234,240,246,252],{"__ignoreMap":137},[141,171,172],{"class":143,"line":144},[141,173,174],{},"# Base de datos PostgreSQL (usa la connection string completa)\n",[141,176,178],{"class":143,"line":177},2,[141,179,180],{},"NUXT_DATABASE_URL=\"postgresql:\u002F\u002Fusuario:password@host:5432\u002Fbase?sslmode=require\"\n",[141,182,184],{"class":143,"line":183},3,[141,185,187],{"emptyLinePlaceholder":186},true,"\n",[141,189,191],{"class":143,"line":190},4,[141,192,193],{},"# Better Auth: cadena larga y aleatoria, distinta en cada entorno\n",[141,195,197],{"class":143,"line":196},5,[141,198,199],{},"BETTER_AUTH_SECRET=\"pega-aqui-un-secreto-largo\"\n",[141,201,203],{"class":143,"line":202},6,[141,204,187],{"emptyLinePlaceholder":186},[141,206,208],{"class":143,"line":207},7,[141,209,210],{},"# URL pública de la app (Better Auth la usa como baseURL)\n",[141,212,214],{"class":143,"line":213},8,[141,215,216],{},"NUXT_APP_URL=\"http:\u002F\u002Flocalhost:3000\"\n",[141,218,220],{"class":143,"line":219},9,[141,221,187],{"emptyLinePlaceholder":186},[141,223,225],{"class":143,"line":224},10,[141,226,227],{},"# Email del administrador de la instancia: con la tabla `user` vacía, `\u002Fsetup`\n",[141,229,231],{"class":143,"line":230},11,[141,232,233],{},"# solo acepta esta dirección para crear la primera cuenta (paso 4).\n",[141,235,237],{"class":143,"line":236},12,[141,238,239],{},"NUXT_ADMIN_EMAIL=\"admin@example.com\"\n",[141,241,243],{"class":143,"line":242},13,[141,244,245],{},"# Opcional: solo la usa la tarea no interactiva `db:seed_admin`. La contraseña\n",[141,247,249],{"class":143,"line":248},14,[141,250,251],{},"# normal se elige en `\u002Fsetup`, no en el entorno.\n",[141,253,255],{"class":143,"line":254},15,[141,256,257],{},"NUXT_ADMIN_PASSWORD=\"una-password-larga\"\n",[11,259,260,261,264,265,267,268,271],{},"El archivo ",[20,262,263],{},".env.example"," de la raíz es la plantilla comentada: cópialo a ",[20,266,161],{}," y completa los valores. ",[20,269,270],{},"server\u002Futils\u002FruntimeConfig.ts"," es la fuente de verdad de los nombres.",[273,274,276],"h3",{"id":275},"obligatorias","Obligatorias",[46,278,279,291],{},[49,280,281],{},[52,282,283,286,288],{},[55,284,285],{},"Variable",[55,287,63],{},[55,289,290],{},"Dónde se lee",[65,292,293,315,342,362,381],{},[52,294,295,300,303],{},[70,296,297],{},[20,298,299],{},"NUXT_DATABASE_URL",[70,301,302],{},"Connection string de PostgreSQL",[70,304,305,308,309,308,312],{},[20,306,307],{},"server\u002Futils\u002Fdb.ts",", ",[20,310,311],{},"server\u002Futils\u002Fpgboss.ts",[20,313,314],{},"server\u002Fdatabase\u002Fdrizzle.config.ts",[52,316,317,322,325],{},[70,318,319],{},[20,320,321],{},"BETTER_AUTH_SECRET",[70,323,324],{},"Firma de sesiones, magic links y tokens",[70,326,327,328,85,331,334,335,308,338,341],{},"Better Auth la toma del entorno (fallback del SDK); alternativa ",[20,329,330],{},"NUXT_BETTER_AUTH_SECRET",[20,332,333],{},"runtimeConfig.betterAuthSecret"," (",[20,336,337],{},"server\u002Futils\u002Fauth.ts",[20,339,340],{},"secret",")",[52,343,344,349,352],{},[70,345,346],{},[20,347,348],{},"NUXT_APP_URL",[70,350,351],{},"Origen público: baseURL y trustedOrigins de Better Auth, enlaces de invitación y exportaciones, baseUrl de i18n",[70,353,354,308,357,308,359],{},[20,355,356],{},"nuxt.config.ts",[20,358,337],{},[20,360,361],{},"server\u002Futils\u002Finvitations.ts",[52,363,364,369,376],{},[70,365,366],{},[20,367,368],{},"NUXT_ADMIN_EMAIL",[70,370,371,372,375],{},"Email del administrador: es la única dirección que ",[20,373,374],{},"\u002Fsetup"," acepta para crear la primera cuenta",[70,377,378],{},[20,379,380],{},"server\u002Fapi\u002Fsetup\u002F*",[52,382,383,388,394],{},[70,384,385],{},[20,386,387],{},"NUXT_ADMIN_PASSWORD",[70,389,390,391],{},"Opcional — solo la tarea no interactiva ",[20,392,393],{},"db:seed_admin",[70,395,396],{},[20,397,398],{},"server\u002Ftasks\u002Fdb\u002Fseed_admin.ts",[273,400,402],{"id":401},"opcionales","Opcionales",[46,404,405,416],{},[49,406,407],{},[52,408,409,411,414],{},[55,410,285],{},[55,412,413],{},"Para qué y sin ella",[55,415,290],{},[65,417,418,436,460,478,495,512,530,568,588,619,636,651],{},[52,419,420,425,432],{},[70,421,422],{},[20,423,424],{},"NUXT_NITRO_PRESET",[70,426,427,428,431],{},"Preset de Nitro; con ",[20,429,430],{},"node-server"," (el del repo) el pool de Drizzle se cachea por proceso",[70,433,434],{},[20,435,307],{},[52,437,438,453,456],{},[70,439,440,443,444,443,447,443,450],{},[20,441,442],{},"NUXT_DB_POOL_MAX"," · ",[20,445,446],{},"_IDLE_TIMEOUT",[20,448,449],{},"_CONNECT_TIMEOUT",[20,451,452],{},"_MAX_LIFETIME",[70,454,455],{},"Ajuste del pool de postgres-js (segundos); sin ellas, 10 conexiones y timeouts 20\u002F10\u002F1800 s",[70,457,458],{},[20,459,307],{},[52,461,462,467,470],{},[70,463,464],{},[20,465,466],{},"NUXT_REDIS_URL",[70,468,469],{},"Redis como almacenamiento secundario de Better Auth; sin ella (o si Redis cae) todo va contra la base de datos",[70,471,472,308,475],{},[20,473,474],{},"server\u002Futils\u002Fsecondary-storage.ts",[20,476,477],{},"server\u002Fapi\u002Fadmin\u002Fsystem\u002Fhealth.get.ts",[52,479,480,485,488],{},[70,481,482],{},[20,483,484],{},"NUXT_RESEND_API_KEY",[70,486,487],{},"Correos transaccionales; sin clave se registran pero no se envían",[70,489,490,308,493],{},[20,491,492],{},"server\u002Futils\u002Fmails-templates.ts",[20,494,477],{},[52,496,497,502,505],{},[70,498,499],{},[20,500,501],{},"NUXT_APP_NAME",[70,503,504],{},"Nombre visible en correos y comunicaciones (el de la interfaz sale de i18n)",[70,506,507,308,509],{},[20,508,492],{},[20,510,511],{},"server\u002Femails\u002Fcommunication.ts",[52,513,514,519,526],{},[70,515,516],{},[20,517,518],{},"NUXT_APP_NOTIFY_EMAIL",[70,520,521,522,525],{},"Remitente ",[20,523,524],{},"From"," de los correos",[70,527,528],{},[20,529,492],{},[52,531,532,549,555],{},[70,533,534,443,537,443,540,443,543,443,546],{},[20,535,536],{},"NUXT_APP_S3_ENDPOINT",[20,538,539],{},"_REGION",[20,541,542],{},"_ACCESS_KEY",[20,544,545],{},"_SECRET_KEY",[20,547,548],{},"_BUCKET_NAME",[70,550,551,552,341],{},"Documentos, fotos y adjuntos; sin ellas las subidas quedan apagadas (región por defecto ",[20,553,554],{},"us-east-1",[70,556,557,308,560,308,563,308,566],{},[20,558,559],{},"server\u002Futils\u002Fcreate-s3-client.ts",[20,561,562],{},"server\u002Futils\u002Fget-presigned.ts",[20,564,565],{},"server\u002Fapi\u002Fupload\u002F*",[20,567,477],{},[52,569,570,575,578],{},[70,571,572],{},[20,573,574],{},"NUXT_APP_S3_BACKUP_BUCKET_NAME",[70,576,577],{},"Bucket dedicado a los backups de base de datos del dashboard admin; sin él los backups quedan apagados (requiere la sección S3 de arriba)",[70,579,580,308,583,308,586],{},[20,581,582],{},"server\u002Futils\u002Fdatabase-backup.ts",[20,584,585],{},"server\u002Fapi\u002Fadmin\u002Fbackups\u002F*",[20,587,477],{},[52,589,590,598,615],{},[70,591,592,443,595],{},[20,593,594],{},"NUXT_BACKUP_DATABASE_URL",[20,596,597],{},"NUXT_BACKUP_PG_DUMP_PATH",[70,599,600,601,604,605,607,608,611,612,614],{},"Backups: conexión directa para ",[20,602,603],{},"pg_dump"," (si la principal es pooled) y ruta del binario; sin ellas se usa ",[20,606,299],{}," quitándole ",[20,609,610],{},"-pooler"," y el ",[20,613,603],{}," del PATH",[70,616,617],{},[20,618,582],{},[52,620,621,629,632],{},[70,622,623,443,626],{},[20,624,625],{},"NUXT_APP_GOOGLE_CLIENT_ID",[20,627,628],{},"NUXT_APP_GOOGLE_CLIENT_SECRET",[70,630,631],{},"Login con Google; sin claves el proveedor solo emite un warning al arrancar",[70,633,634],{},[20,635,337],{},[52,637,638,643,646],{},[70,639,640],{},[20,641,642],{},"NUXT_GOOGLE_MAPS_SERVER_KEY",[70,644,645],{},"Geocodificación en el servidor (autocomplete, place details, timezone)",[70,647,648],{},[20,649,650],{},"server\u002Fapi\u002Fgoogle\u002F*.get.ts",[52,652,653,658,661],{},[70,654,655],{},[20,656,657],{},"NUXT_PUBLIC_GOOGLE_MAPS_CLIENT_KEY",[70,659,660],{},"Key pública del selector de ubicación (restringida por referrer)",[70,662,663],{},[20,664,665],{},"app\u002Fcomponents\u002Fplan\u002FLocationPicker.vue",[667,668,669],"note",{},[11,670,671,672,334,675,678,679,681,682,685,686,689,690,27],{},"Los ",[15,673,674],{},"backups de base de datos",[20,676,677],{},"\u002Fapp\u002Fadmin\u002Fdashboard",") necesitan además el binario ",[20,680,603],{}," (versión ≥ a la del servidor PostgreSQL) en la máquina que corre la app: en local ",[20,683,684],{},"brew install postgresql@17"," o ",[20,687,688],{},"apt install postgresql-client-17","; en el deploy lo instala el ",[20,691,692],{},"Dockerfile",[667,694,695],{},[11,696,697,700,701,704,705,708,709,711,712,308,715,308,718,308,721,334,724,727,728,27],{},[20,698,699],{},"runtimeConfig"," lee algunas variables que hoy ",[15,702,703],{},"no tienen consumidores",", así que no hacen falta para arrancar: ",[20,706,707],{},"BETTER_AUTH_URL"," (el baseURL sale de ",[20,710,348],{},"), ",[20,713,714],{},"NUXT_RESEND_WEBHOOK_SECRET",[20,716,717],{},"NUXT_ADMIN_USER_IDS",[20,719,720],{},"NUXT_APP_CONTACT_EMAIL",[20,722,723],{},"NODE_ENV",[20,725,726],{},"public.appEnv",") y ",[20,729,730],{},"NUXT_PUBLIC_SITE_URL",[732,733,734],"warning",{},[11,735,736,737,739,740,743],{},"El ",[20,738,321],{}," local, el de dev y el de producción deben ser ",[15,741,742],{},"distintos",". Ese secreto firma sesiones y enlaces mágicos: si se filtra, hay que rotarlo y todos los enlaces pendientes dejan de valer.",[41,745,747],{"id":746},"_3-migra-la-base-de-datos","3. Migra la base de datos",[132,749,751],{"className":134,"code":750,"language":136,"meta":137,"style":137},"pnpm db:migrate    # aplica las migraciones pendientes\npnpm db:generate   # genera una migración después de cambiar el schema\npnpm studio        # abre Drizzle Studio para inspeccionar datos\n",[20,752,753,764,774],{"__ignoreMap":137},[141,754,755,757,760],{"class":143,"line":144},[141,756,17],{"class":147},[141,758,759],{"class":150}," db:migrate",[141,761,763],{"class":762},"sHwdD","    # aplica las migraciones pendientes\n",[141,765,766,768,771],{"class":143,"line":177},[141,767,17],{"class":147},[141,769,770],{"class":150}," db:generate",[141,772,773],{"class":762},"   # genera una migración después de cambiar el schema\n",[141,775,776,778,781],{"class":143,"line":183},[141,777,17],{"class":147},[141,779,780],{"class":150}," studio",[141,782,783],{"class":762},"        # abre Drizzle Studio para inspeccionar datos\n",[41,785,787],{"id":786},"_4-crea-la-cuenta-admin","4. Crea la cuenta admin",[11,789,790,791,794,795,798],{},"El template no tiene registro público. La primera cuenta se crea desde la pantalla de\n",[15,792,793],{},"setup de primer arranque",": arranca el servidor y abre ",[20,796,797],{},"http:\u002F\u002Flocalhost:3000\u002Fsetup","\n(con la app en marcha, cualquier visita a una ruta privada te lleva ahí mientras no exista\nningún usuario).",[11,800,801,802,804],{},"La pantalla pide el email y la contraseña. El email tiene que coincidir con\n",[20,803,368],{}," — el servidor rechaza cualquier otro, así que funciona como el secreto\nde la instalación — y la contraseña la eliges tú en ese momento. La cuenta que se crea es\nplatform admin, queda verificada y se inicia sesión automáticamente.",[11,806,807,808,810],{},"La pantalla deja de existir en cuanto hay un usuario: ",[20,809,374],{}," redirige al login.",[812,813,814,829,848],"blockquote",{},[11,815,816,819,820,822,823,825,826,828],{},[15,817,818],{},"Alternativa no interactiva."," Para un deploy automatizado, ",[20,821,393],{}," crea la\ncuenta a partir de ",[20,824,368],{}," y ",[20,827,387],{}," del entorno. Con el\nservidor corriendo:",[132,830,832],{"className":134,"code":831,"language":136,"meta":137,"style":137},"curl -X POST http:\u002F\u002Flocalhost:3000\u002F_nitro\u002Ftasks\u002Fdb:seed_admin\n",[20,833,834],{"__ignoreMap":137},[141,835,836,839,842,845],{"class":143,"line":144},[141,837,838],{"class":147},"curl",[141,840,841],{"class":150}," -X",[141,843,844],{"class":150}," POST",[141,846,847],{"class":150}," http:\u002F\u002Flocalhost:3000\u002F_nitro\u002Ftasks\u002Fdb:seed_admin\n",[11,849,850,851,27],{},"La tarea es idempotente: si el usuario ya existe, no hace nada. La lista completa de\ntareas está en ",[20,852,853],{},"GET \u002F_nitro\u002Ftasks",[11,855,856,857,860,861,864],{},"Esa cuenta es también la que usa la suite de Playwright (",[20,858,859],{},"E2E_EMAIL"," \u002F ",[20,862,863],{},"E2E_PASSWORD",", con\nlas credenciales del seed como fallback).",[41,866,868],{"id":867},"_5-arranca","5. Arranca",[132,870,872],{"className":134,"code":871,"language":136,"meta":137,"style":137},"pnpm dev              # http:\u002F\u002Flocalhost:3000\npnpm test:e2e:smoke   # smoke de Playwright sobre las páginas principales\npnpm lint             # ESLint (es el único formatter del repo)\n",[20,873,874,884,894],{"__ignoreMap":137},[141,875,876,878,881],{"class":143,"line":144},[141,877,17],{"class":147},[141,879,880],{"class":150}," dev",[141,882,883],{"class":762},"              # http:\u002F\u002Flocalhost:3000\n",[141,885,886,888,891],{"class":143,"line":177},[141,887,17],{"class":147},[141,889,890],{"class":150}," test:e2e:smoke",[141,892,893],{"class":762},"   # smoke de Playwright sobre las páginas principales\n",[141,895,896,898,901],{"class":143,"line":183},[141,897,17],{"class":147},[141,899,900],{"class":150}," lint",[141,902,903],{"class":762},"             # ESLint (es el único formatter del repo)\n",[41,905,907],{"id":906},"siguientes-pasos","Siguientes pasos",[909,910,911,919,930],"ul",{},[912,913,914,918],"li",{},[120,915,917],{"href":916},"\u002Fdocs\u002Fbranching","Ramas y flujo de trabajo",": cómo crear una feature, un fix o un hotfix.",[912,920,921,925,926,929],{},[120,922,924],{"href":923},"\u002Fdocs\u002Fdesign-system","Design system",": los tokens, las clases ",[20,927,928],{},".ds-*"," y las reglas de UI.",[912,931,932,936],{},[120,933,935],{"href":934},"\u002Fdocs\u002Fdeploy","Deploy y CI\u002FCD",": Dokploy, entornos y migraciones.",[938,939,940],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":137,"searchDepth":177,"depth":177,"links":942},[943,944,945,949,950,951,952],{"id":43,"depth":177,"text":44},{"id":129,"depth":177,"text":130},{"id":154,"depth":177,"text":155,"children":946},[947,948],{"id":275,"depth":183,"text":276},{"id":401,"depth":183,"text":402},{"id":746,"depth":177,"text":747},{"id":786,"depth":177,"text":787},{"id":867,"depth":177,"text":868},{"id":906,"depth":177,"text":907},"Requisitos, variables de entorno y primer arranque, con comandos listos para copiar y pegar.","md",{},{"icon":957},"i-lucide-rocket","\u002Fdocs\u002Fsetup",{"title":6,"description":953},"docs\u002F1.setup","h8LihQJEUm-ugkFEP3U3rTqCN_HBiiMZ94aH34l8il4",{"id":963,"title":917,"body":964,"description":1893,"extension":954,"meta":1894,"navigation":1895,"path":916,"seo":1897,"stem":1898,"__hash__":1899},"docs\u002Fdocs\u002F2.branching.md",{"type":8,"value":965,"toc":1881},[966,985,1093,1101,1105,1175,1179,1202,1206,1384,1388,1401,1466,1470,1477,1537,1550,1554,1611,1658,1662,1699,1703,1775,1786,1790,1878],[11,967,968,969,825,972,975,976,978,979,981,982,984],{},"Dos ramas permanentes (",[20,970,971],{},"main",[20,973,974],{},"dev",") más ramas cortas de trabajo. ",[20,977,974],{}," es la rama de integración y se despliega sola en el entorno dev; ",[20,980,971],{}," es producción y solo recibe lo que ya pasó por ",[20,983,974],{}," (o un hotfix).",[46,986,987,1000],{},[49,988,989],{},[52,990,991,994,997],{},[55,992,993],{},"Rama",[55,995,996],{},"Entorno",[55,998,999],{},"Reglas",[65,1001,1002,1017,1039,1058,1074],{},[52,1003,1004,1008,1011],{},[70,1005,1006],{},[20,1007,971],{},[70,1009,1010],{},"Producción",[70,1012,1013,1014,1016],{},"Protegida. Solo llega código que pasó por ",[20,1015,974],{},". Siempre desplegable.",[52,1018,1019,1023,1029],{},[70,1020,1021],{},[20,1022,974],{},[70,1024,1025,1026,341],{},"Entorno dev (por ejemplo ",[20,1027,1028],{},"dev.tudominio.com",[70,1030,1031,1032,825,1035,1038],{},"Protegida. Integra ",[20,1033,1034],{},"feature\u002F*",[20,1036,1037],{},"fix\u002F*",". Cada merge dispara el despliegue de dev.",[52,1040,1041,1046,1049],{},[70,1042,1043],{},[20,1044,1045],{},"feature\u002F\u003Cslug>",[70,1047,1048],{},"—",[70,1050,1051,1052,1054,1055,1057],{},"Nueva funcionalidad. Sale de ",[20,1053,974],{},", PR a ",[20,1056,974],{},". Corta (días, no semanas).",[52,1059,1060,1065,1067],{},[70,1061,1062],{},[20,1063,1064],{},"fix\u002F\u003Cslug>",[70,1066,1048],{},[70,1068,1069,1070,1054,1072,27],{},"Corrección de un bug. Sale de ",[20,1071,974],{},[20,1073,974],{},[52,1075,1076,1081,1083],{},[70,1077,1078],{},[20,1079,1080],{},"hotfix\u002F\u003Cslug>",[70,1082,1048],{},[70,1084,1085,1086,1054,1088,1090,1091,27],{},"Bug crítico en producción. Sale de ",[20,1087,971],{},[20,1089,971],{}," y se sincroniza a ",[20,1092,974],{},[132,1094,1099],{"className":1095,"code":1097,"language":1098,"meta":137},[1096],"language-text","feature\u002F* · fix\u002F* ──PR──▶ dev ──PR release──▶ main\n   (corta)              (entorno dev)        (producción)\n","text",[20,1100,1097],{"__ignoreMap":137},[41,1102,1104],{"id":1103},"reglas-de-oro","Reglas de oro",[1106,1107,1108,1123,1129,1135,1146,1156],"ol",{},[912,1109,1110,1118,1119,1122],{},[15,1111,1112,1113,1115,1116],{},"Nunca se commitea directo a ",[20,1114,971],{}," ni a ",[20,1117,974],{},": todo llega por PR con el check ",[20,1120,1121],{},"Lint & build"," en verde.",[912,1124,1125,1128],{},[15,1126,1127],{},"Una rama = un propósito."," No mezcles un refactor con una feature ni dos features en la misma rama.",[912,1130,1131,1134],{},[15,1132,1133],{},"Ramas cortas."," Si algo lleva semanas, divídelo en entregas pequeñas que se integren por separado.",[912,1136,1137,1142,1143,1145],{},[15,1138,1139,1141],{},[20,1140,974],{}," siempre desplegable."," Es lo que ve el resto del equipo en el entorno dev; lo que se rompe se arregla con ",[20,1144,1037],{}," o se revierte cuanto antes.",[912,1147,1148,1151,1152,1155],{},[15,1149,1150],{},"El flujo verificado a mano se persiste como spec de Playwright"," en ",[20,1153,1154],{},"tests\u002Fe2e\u002F",". Un chequeo manual que no quedó como spec no cuenta como cobertura.",[912,1157,1158,1161,1162,860,1165,860,1168,1171,1172,1174],{},[15,1159,1160],{},"Migraciones expand\u002Fcontract",": nunca ",[20,1163,1164],{},"DROP",[20,1166,1167],{},"RENAME",[20,1169,1170],{},"NOT NULL"," en el mismo deploy que todavía usa la forma vieja (ver ",[120,1173,935],{"href":934},").",[41,1176,1178],{"id":1177},"nombres-de-rama","Nombres de rama",[909,1180,1181,1189,1195],{},[912,1182,1183,308,1186,27],{},[20,1184,1185],{},"feature\u002Fproducts-export-csv",[20,1187,1188],{},"fix\u002Fcommunications-dst-ventana",[912,1190,1191,1192,27],{},"Con ticket delante si existe: ",[20,1193,1194],{},"feature\u002F123-products-export-csv",[912,1196,1197,1198,1201],{},"Minúsculas, guiones, sin espacios ni acentos. El slug describe el resultado (",[20,1199,1200],{},"export-excel","), no la acción.",[41,1203,1205],{"id":1204},"nueva-feature-paso-a-paso","Nueva feature, paso a paso",[132,1207,1209],{"className":134,"code":1208,"language":136,"meta":137,"style":137},"# 1. Parte de un dev actualizado\ngit switch dev\ngit pull origin dev\n\n# 2. Crea la rama\ngit switch -c feature\u002Fproducts-export-csv\n\n# 3. Implementa. El pre-commit arregla el lint de lo staged automáticamente.\n\n# 4. Antes de subir, corre el spec del flujo tocado (la suite completa no hace falta aquí)\npnpm exec playwright test tests\u002Fe2e\u002Fapp\u002Fproducts.spec.ts\n\n# 5. Sube: el hook pre-push corre la suite completa\ngit push -u origin feature\u002Fproducts-export-csv\n\n# 6. Abre el PR hacia dev\ngh pr create --base dev --title \"...\" --body \"...\"\n\n# 7. CI en verde → merge con squash → dev se despliega solo.\n# 8. Verifica en el entorno dev.\n",[20,1210,1211,1216,1227,1239,1243,1248,1260,1264,1269,1273,1278,1294,1298,1303,1317,1321,1327,1367,1372,1378],{"__ignoreMap":137},[141,1212,1213],{"class":143,"line":144},[141,1214,1215],{"class":762},"# 1. Parte de un dev actualizado\n",[141,1217,1218,1221,1224],{"class":143,"line":177},[141,1219,1220],{"class":147},"git",[141,1222,1223],{"class":150}," switch",[141,1225,1226],{"class":150}," dev\n",[141,1228,1229,1231,1234,1237],{"class":143,"line":183},[141,1230,1220],{"class":147},[141,1232,1233],{"class":150}," pull",[141,1235,1236],{"class":150}," origin",[141,1238,1226],{"class":150},[141,1240,1241],{"class":143,"line":190},[141,1242,187],{"emptyLinePlaceholder":186},[141,1244,1245],{"class":143,"line":196},[141,1246,1247],{"class":762},"# 2. Crea la rama\n",[141,1249,1250,1252,1254,1257],{"class":143,"line":202},[141,1251,1220],{"class":147},[141,1253,1223],{"class":150},[141,1255,1256],{"class":150}," -c",[141,1258,1259],{"class":150}," feature\u002Fproducts-export-csv\n",[141,1261,1262],{"class":143,"line":207},[141,1263,187],{"emptyLinePlaceholder":186},[141,1265,1266],{"class":143,"line":213},[141,1267,1268],{"class":762},"# 3. Implementa. El pre-commit arregla el lint de lo staged automáticamente.\n",[141,1270,1271],{"class":143,"line":219},[141,1272,187],{"emptyLinePlaceholder":186},[141,1274,1275],{"class":143,"line":224},[141,1276,1277],{"class":762},"# 4. Antes de subir, corre el spec del flujo tocado (la suite completa no hace falta aquí)\n",[141,1279,1280,1282,1285,1288,1291],{"class":143,"line":230},[141,1281,17],{"class":147},[141,1283,1284],{"class":150}," exec",[141,1286,1287],{"class":150}," playwright",[141,1289,1290],{"class":150}," test",[141,1292,1293],{"class":150}," tests\u002Fe2e\u002Fapp\u002Fproducts.spec.ts\n",[141,1295,1296],{"class":143,"line":236},[141,1297,187],{"emptyLinePlaceholder":186},[141,1299,1300],{"class":143,"line":242},[141,1301,1302],{"class":762},"# 5. Sube: el hook pre-push corre la suite completa\n",[141,1304,1305,1307,1310,1313,1315],{"class":143,"line":248},[141,1306,1220],{"class":147},[141,1308,1309],{"class":150}," push",[141,1311,1312],{"class":150}," -u",[141,1314,1236],{"class":150},[141,1316,1259],{"class":150},[141,1318,1319],{"class":143,"line":254},[141,1320,187],{"emptyLinePlaceholder":186},[141,1322,1324],{"class":143,"line":1323},16,[141,1325,1326],{"class":762},"# 6. Abre el PR hacia dev\n",[141,1328,1330,1333,1336,1339,1342,1344,1347,1351,1354,1357,1360,1362,1364],{"class":143,"line":1329},17,[141,1331,1332],{"class":147},"gh",[141,1334,1335],{"class":150}," pr",[141,1337,1338],{"class":150}," create",[141,1340,1341],{"class":150}," --base",[141,1343,880],{"class":150},[141,1345,1346],{"class":150}," --title",[141,1348,1350],{"class":1349},"sMK4o"," \"",[141,1352,1353],{"class":150},"...",[141,1355,1356],{"class":1349},"\"",[141,1358,1359],{"class":150}," --body",[141,1361,1350],{"class":1349},[141,1363,1353],{"class":150},[141,1365,1366],{"class":1349},"\"\n",[141,1368,1370],{"class":143,"line":1369},18,[141,1371,187],{"emptyLinePlaceholder":186},[141,1373,1375],{"class":143,"line":1374},19,[141,1376,1377],{"class":762},"# 7. CI en verde → merge con squash → dev se despliega solo.\n",[141,1379,1381],{"class":143,"line":1380},20,[141,1382,1383],{"class":762},"# 8. Verifica en el entorno dev.\n",[273,1385,1387],{"id":1386},"release-a-producción","Release a producción",[11,1389,1390,1391,1393,1394,1397,1398,1400],{},"Cuando lo integrado en ",[20,1392,974],{}," está estable, se promociona con un PR ",[20,1395,1396],{},"dev → main"," (merge commit, sin squash, para conservar la historia de ",[20,1399,974],{},"):",[132,1402,1404],{"className":134,"code":1403,"language":136,"meta":137,"style":137},"git switch dev && git pull origin dev\ngh pr create --base main --head dev --title \"Release: \u003Cresumen>\" --body \"...\"\n# CI en verde → merge → deploy de producción con health check\n",[20,1405,1406,1426,1461],{"__ignoreMap":137},[141,1407,1408,1410,1412,1414,1417,1420,1422,1424],{"class":143,"line":144},[141,1409,1220],{"class":147},[141,1411,1223],{"class":150},[141,1413,880],{"class":150},[141,1415,1416],{"class":1349}," &&",[141,1418,1419],{"class":147}," git",[141,1421,1233],{"class":150},[141,1423,1236],{"class":150},[141,1425,1226],{"class":150},[141,1427,1428,1430,1432,1434,1436,1439,1442,1444,1446,1448,1451,1453,1455,1457,1459],{"class":143,"line":177},[141,1429,1332],{"class":147},[141,1431,1335],{"class":150},[141,1433,1338],{"class":150},[141,1435,1341],{"class":150},[141,1437,1438],{"class":150}," main",[141,1440,1441],{"class":150}," --head",[141,1443,880],{"class":150},[141,1445,1346],{"class":150},[141,1447,1350],{"class":1349},[141,1449,1450],{"class":150},"Release: \u003Cresumen>",[141,1452,1356],{"class":1349},[141,1454,1359],{"class":150},[141,1456,1350],{"class":1349},[141,1458,1353],{"class":150},[141,1460,1366],{"class":1349},[141,1462,1463],{"class":143,"line":183},[141,1464,1465],{"class":762},"# CI en verde → merge → deploy de producción con health check\n",[41,1467,1469],{"id":1468},"fix-de-bug-paso-a-paso","Fix de bug, paso a paso",[11,1471,1472,1473,1476],{},"Igual que una feature, con ",[20,1474,1475],{},"fix\u002F"," y con el spec de regresión: primero el spec que reproduce el bug (falla), después el arreglo, después el spec en verde.",[132,1478,1480],{"className":134,"code":1479,"language":136,"meta":137,"style":137},"git switch dev && git pull origin dev\ngit switch -c fix\u002Fcommunications-dst-ventana\n# 1) spec que reproduce el bug  2) arreglo mínimo  3) spec en verde\ngit push -u origin fix\u002Fcommunications-dst-ventana   # pre-push: suite completa\n# PR a dev → CI → squash merge → entorno dev\n",[20,1481,1482,1500,1511,1516,1532],{"__ignoreMap":137},[141,1483,1484,1486,1488,1490,1492,1494,1496,1498],{"class":143,"line":144},[141,1485,1220],{"class":147},[141,1487,1223],{"class":150},[141,1489,880],{"class":150},[141,1491,1416],{"class":1349},[141,1493,1419],{"class":147},[141,1495,1233],{"class":150},[141,1497,1236],{"class":150},[141,1499,1226],{"class":150},[141,1501,1502,1504,1506,1508],{"class":143,"line":177},[141,1503,1220],{"class":147},[141,1505,1223],{"class":150},[141,1507,1256],{"class":150},[141,1509,1510],{"class":150}," fix\u002Fcommunications-dst-ventana\n",[141,1512,1513],{"class":143,"line":183},[141,1514,1515],{"class":762},"# 1) spec que reproduce el bug  2) arreglo mínimo  3) spec en verde\n",[141,1517,1518,1520,1522,1524,1526,1529],{"class":143,"line":190},[141,1519,1220],{"class":147},[141,1521,1309],{"class":150},[141,1523,1312],{"class":150},[141,1525,1236],{"class":150},[141,1527,1528],{"class":150}," fix\u002Fcommunications-dst-ventana",[141,1530,1531],{"class":762},"   # pre-push: suite completa\n",[141,1533,1534],{"class":143,"line":196},[141,1535,1536],{"class":762},"# PR a dev → CI → squash merge → entorno dev\n",[11,1538,1539,1540,1543,1544,1546,1547,1549],{},"Un fix normal ",[15,1541,1542],{},"no"," va directo a ",[20,1545,971],{},": ",[20,1548,971],{}," solo recibe lo que ya pasó por el entorno dev (la excepción es el hotfix).",[41,1551,1553],{"id":1552},"hotfix-bug-crítico-en-producción","Hotfix (bug crítico en producción)",[132,1555,1557],{"className":134,"code":1556,"language":136,"meta":137,"style":137},"git switch main && git pull origin main\ngit switch -c hotfix\u002Flogin-callback\n# arreglo mínimo + spec de regresión\ngit push -u origin hotfix\u002Flogin-callback\n# PR a main (excepción deliberada: no pasa por dev) → CI → merge → deploy de producción\n",[20,1558,1559,1578,1589,1594,1606],{"__ignoreMap":137},[141,1560,1561,1563,1565,1567,1569,1571,1573,1575],{"class":143,"line":144},[141,1562,1220],{"class":147},[141,1564,1223],{"class":150},[141,1566,1438],{"class":150},[141,1568,1416],{"class":1349},[141,1570,1419],{"class":147},[141,1572,1233],{"class":150},[141,1574,1236],{"class":150},[141,1576,1577],{"class":150}," main\n",[141,1579,1580,1582,1584,1586],{"class":143,"line":177},[141,1581,1220],{"class":147},[141,1583,1223],{"class":150},[141,1585,1256],{"class":150},[141,1587,1588],{"class":150}," hotfix\u002Flogin-callback\n",[141,1590,1591],{"class":143,"line":183},[141,1592,1593],{"class":762},"# arreglo mínimo + spec de regresión\n",[141,1595,1596,1598,1600,1602,1604],{"class":143,"line":190},[141,1597,1220],{"class":147},[141,1599,1309],{"class":150},[141,1601,1312],{"class":150},[141,1603,1236],{"class":150},[141,1605,1588],{"class":150},[141,1607,1608],{"class":143,"line":196},[141,1609,1610],{"class":762},"# PR a main (excepción deliberada: no pasa por dev) → CI → merge → deploy de producción\n",[732,1612,1613,1623],{},[11,1614,1615,1616,1619,1620,1622],{},"Después del hotfix hay que ",[15,1617,1618],{},"cerrar el ciclo",", o ",[20,1621,974],{}," reintroducirá el bug en la próxima release:",[132,1624,1626],{"className":134,"code":1625,"language":136,"meta":137,"style":137},"git switch dev\ngit merge main   # o un PR main → dev\ngit push origin dev\n",[20,1627,1628,1636,1648],{"__ignoreMap":137},[141,1629,1630,1632,1634],{"class":143,"line":144},[141,1631,1220],{"class":147},[141,1633,1223],{"class":150},[141,1635,1226],{"class":150},[141,1637,1638,1640,1643,1645],{"class":143,"line":177},[141,1639,1220],{"class":147},[141,1641,1642],{"class":150}," merge",[141,1644,1438],{"class":150},[141,1646,1647],{"class":762},"   # o un PR main → dev\n",[141,1649,1650,1652,1654,1656],{"class":143,"line":183},[141,1651,1220],{"class":147},[141,1653,1309],{"class":150},[141,1655,1236],{"class":150},[141,1657,1226],{"class":150},[41,1659,1661],{"id":1660},"commits","Commits",[909,1663,1664,1689,1696],{},[912,1665,1666,1667,1670,1671,860,1674,860,1677,860,1680,860,1683,860,1686,27],{},"Formato recomendado (no obligatorio): ",[20,1668,1669],{},"tipo: descripción",", con ",[20,1672,1673],{},"feat",[20,1675,1676],{},"fix",[20,1678,1679],{},"chore",[20,1681,1682],{},"docs",[20,1684,1685],{},"refactor",[20,1687,1688],{},"test",[912,1690,1691,1692,1695],{},"Título corto; el cuerpo explica ",[15,1693,1694],{},"por qué",", no el qué (el diff ya dice el qué).",[912,1697,1698],{},"El pre-commit pasa ESLint sobre lo staged: no hace falta un commit aparte para formato.",[41,1700,1702],{"id":1701},"pull-requests","Pull requests",[909,1704,1705,1711],{},[912,1706,1707,1708,27],{},"Título y descripción: qué cambia y ",[15,1709,1710],{},"cómo probarlo",[912,1712,1713,1714],{},"Checklist antes de pedir review:\n",[909,1715,1718,1731,1750,1759,1769],{"className":1716},[1717],"contains-task-list",[912,1719,1722,1726,1727,1730],{"className":1720},[1721],"task-list-item",[1723,1724],"input",{"disabled":186,"type":1725},"checkbox"," Spec de Playwright del flujo tocado (o ",[20,1728,1729],{},"pnpm test:e2e:smoke",") corriendo en local.",[912,1732,1734,1736,1737,1740,1741,860,1744,860,1747,27],{"className":1733},[1721],[1723,1735],{"disabled":186,"type":1725}," ",[20,1738,1739],{},"pnpm lint"," limpio y, si hay texto visible, i18n en ",[20,1742,1743],{},"en",[20,1745,1746],{},"es",[20,1748,1749],{},"pt",[912,1751,1753,1755,1756,1174],{"className":1752},[1721],[1723,1754],{"disabled":186,"type":1725}," Entidad nueva: eventos de dominio, audit log y triggers (ver ",[20,1757,1758],{},"AGENTS.md",[912,1760,1762,1764,1765,1768],{"className":1761},[1721],[1723,1763],{"disabled":186,"type":1725}," Migración: expand\u002Fcontract, generada con ",[20,1766,1767],{},"pnpm db:generate"," en el mismo PR.",[912,1770,1772,1774],{"className":1771},[1721],[1723,1773],{"disabled":186,"type":1725}," PR pequeño (≤ ~400 líneas) cuando sea posible.",[11,1776,1777,1778,825,1780,1782,1783,1785],{},"Protege ",[20,1779,971],{},[20,1781,974],{}," en GitHub (Settings → Branches) exigiendo el check ",[20,1784,1121],{}," y prohibiendo force push.",[41,1787,1789],{"id":1788},"qué-corre-y-cuándo","Qué corre y cuándo",[46,1791,1792,1805],{},[49,1793,1794],{},[52,1795,1796,1799,1802],{},[55,1797,1798],{},"Momento",[55,1800,1801],{},"Qué corre",[55,1803,1804],{},"Se salta con",[65,1806,1807,1830,1844,1854,1867],{},[52,1808,1809,1814,1824],{},[70,1810,1811],{},[20,1812,1813],{},"git commit",[70,1815,1816,1819,1820,1823],{},[20,1817,1818],{},"lint-staged"," (ESLint ",[20,1821,1822],{},"--fix"," sobre lo staged)",[70,1825,1826,1829],{},[20,1827,1828],{},"--no-verify"," (excepcional)",[52,1831,1832,1837,1840],{},[70,1833,1834],{},[20,1835,1836],{},"git push",[70,1838,1839],{},"Suite Playwright completa",[70,1841,1842],{},[20,1843,39],{},[52,1845,1846,1849,1852],{},[70,1847,1848],{},"PR abierto",[70,1850,1851],{},"CI: lint + build (bloqueante), typecheck (informativo)",[70,1853,1048],{},[52,1855,1856,1861,1864],{},[70,1857,1858,1859],{},"Push\u002Fmerge a ",[20,1860,974],{},[70,1862,1863],{},"CI + despliegue del entorno dev",[70,1865,1866],{},"Interruptor de deploy apagado",[52,1868,1869,1873,1876],{},[70,1870,1858,1871],{},[20,1872,971],{},[70,1874,1875],{},"CI + despliegue de producción",[70,1877,1866],{},[938,1879,1880],{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":137,"searchDepth":177,"depth":177,"links":1882},[1883,1884,1885,1888,1889,1890,1891,1892],{"id":1103,"depth":177,"text":1104},{"id":1177,"depth":177,"text":1178},{"id":1204,"depth":177,"text":1205,"children":1886},[1887],{"id":1386,"depth":183,"text":1387},{"id":1468,"depth":177,"text":1469},{"id":1552,"depth":177,"text":1553},{"id":1660,"depth":177,"text":1661},{"id":1701,"depth":177,"text":1702},{"id":1788,"depth":177,"text":1789},"El modelo de dos ramas permanentes, los pasos para features y fixes, y qué corre en cada gate.",{},{"icon":1896},"i-lucide-git-branch",{"title":917,"description":1893},"docs\u002F2.branching","C85C13qQWe9uGdmxB94m5RHm1c3_4ySZA-xlCwvliJc",{"id":1901,"title":924,"body":1902,"description":2975,"extension":954,"meta":2976,"navigation":2977,"path":923,"seo":2979,"stem":2980,"__hash__":2981},"docs\u002Fdocs\u002F3.design-system.md",{"type":8,"value":1903,"toc":2959},[1904,1911,1936,1940,2004,2008,2012,2105,2111,2179,2202,2228,2232,2237,2378,2392,2396,2400,2403,2498,2502,2540,2544,2550,2692,2758,2762,2912,2916,2956],[11,1905,1906,1907,1910],{},"El look del template es ",[15,1908,1909],{},"Claude Amber",": lienzo crema, acento terracota, radios generosos y títulos con serif. La regla que lo sostiene todo es una sola:",[812,1912,1913],{},[11,1914,1915,1918,1919,1922,1923,1925,1926,308,1929,308,1932,1935],{},[15,1916,1917],{},"Nada hardcodea un color."," Cada valor visual vive en un token de ",[20,1920,1921],{},"app\u002Fassets\u002Fcss\u002Fmain.css"," y se consume con una clase ",[20,1924,928],{}," o una utilidad semántica (",[20,1927,1928],{},"bg-default",[20,1930,1931],{},"border-default",[20,1933,1934],{},"text-muted","). Como claro y oscuro tienen su propio bloque de tokens, un cambio se hace una vez y se ve bien en los dos modos.",[41,1937,1939],{"id":1938},"dónde-vive-cada-cosa","Dónde vive cada cosa",[46,1941,1942,1955],{},[49,1943,1944],{},[52,1945,1946,1949,1952],{},[55,1947,1948],{},"Pieza",[55,1950,1951],{},"Archivo",[55,1953,1954],{},"Rol",[65,1956,1957,1971,1991],{},[52,1958,1959,1964,1968],{},[70,1960,1961,1962],{},"Tokens y clases ",[20,1963,928],{},[70,1965,1966],{},[20,1967,1921],{},[70,1969,1970],{},"La única fuente de colores, radios, sombras y tipografías",[52,1972,1973,1976,1981],{},[70,1974,1975],{},"Temas globales de componentes",[70,1977,1978],{},[20,1979,1980],{},"app\u002Fapp.config.ts",[70,1982,1983,1984,308,1987,1990],{},"Router de slots de Nuxt UI (",[20,1985,1986],{},"ui.card",[20,1988,1989],{},"ui.modal",", …)",[52,1992,1993,1996,2001],{},[70,1994,1995],{},"Guía completa (repo)",[70,1997,1998],{},[20,1999,2000],{},"DESIGN_SYSTEM.md",[70,2002,2003],{},"Recetas, verificación y troubleshooting paso a paso",[41,2005,2007],{"id":2006},"los-tres-niveles","Los tres niveles",[273,2009,2011],{"id":2010},"_1-tokens","1. Tokens",[909,2013,2014,2039,2056,2078],{},[912,2015,2016,1546,2019,2022,2023,2026,2027,2030,2031,2034,2035,2038],{},[15,2017,2018],{},"Tipografía",[20,2020,2021],{},"--font-sans"," (Outfit) para todo el texto de interfaz, ",[20,2024,2025],{},"--font-heading"," (Fraunces) solo para títulos reales (",[20,2028,2029],{},"h1…h6",", títulos de page card, modal y navbar), ",[20,2032,2033],{},"--font-mono"," (Geist Mono) para código e identificadores. Si un título se reutiliza como etiqueta pequeña, añade ",[20,2036,2037],{},"font-sans"," a esa instancia.",[912,2040,2041,2044,2045,2048,2049,1151,2052,2055],{},[15,2042,2043],{},"Acento",": la escala ",[20,2046,2047],{},"--color-claude-50…950"," (el 500 es el terracota de marca) más ",[20,2050,2051],{},"ui.colors.primary = 'claude'",[20,2053,2054],{},"app.config.ts",". No hay un segundo color de marca.",[912,2057,2058,1546,2061,308,2064,825,2067,2070,2071,2074,2075,27],{},[15,2059,2060],{},"Superficies",[20,2062,2063],{},"--ds-card",[20,2065,2066],{},"--ds-panel",[20,2068,2069],{},"--ds-field",", con valor claro en ",[20,2072,2073],{},":root"," y oscuro en ",[20,2076,2077],{},".dark",[912,2079,2080,1546,2083,308,2086,308,2089,2092,2093,860,2096,860,2099,860,2101,2104],{},[15,2081,2082],{},"Semánticos",[20,2084,2085],{},"--ui-bg",[20,2087,2088],{},"--ui-border",[20,2090,2091],{},"--ui-text*"," se sobrescriben en ambos modos. ",[20,2094,2095],{},"success",[20,2097,2098],{},"info",[20,2100,732],{},[20,2102,2103],{},"error"," se quedan como vienen: son señales de estado y deben seguir siendo reconocibles.",[273,2106,2108,2109],{"id":2107},"_2-clases-ds","2. Clases ",[20,2110,928],{},[46,2112,2113,2126],{},[49,2114,2115],{},[52,2116,2117,2120,2123],{},[55,2118,2119],{},"Clase",[55,2121,2122],{},"Efecto",[55,2124,2125],{},"Cuándo usarla",[65,2127,2128,2141,2154],{},[52,2129,2130,2135,2138],{},[70,2131,2132],{},[20,2133,2134],{},"ds-card",[70,2136,2137],{},"Fondo de tarjeta + borde de 1px + sombra suave",[70,2139,2140],{},"Tarjetas y page cards",[52,2142,2143,2148,2151],{},[70,2144,2145],{},[20,2146,2147],{},"ds-panel",[70,2149,2150],{},"Fondo elevado + borde de 1px",[70,2152,2153],{},"Modales, popovers, dropdowns, selects, toasts, tooltips",[52,2155,2156,2161,2164],{},[70,2157,2158],{},[20,2159,2160],{},"ds-field",[70,2162,2163],{},"Fondo de campo",[70,2165,2166,308,2169,2172,2173,860,2176],{},[20,2167,2168],{},"UInput",[20,2170,2171],{},"UTextarea"," y triggers de ",[20,2174,2175],{},"USelect",[20,2177,2178],{},"USelectMenu",[11,2180,2181,2182,2188,2189,308,2191,308,2194,2197,2198,2201],{},"Están declaradas ",[15,2183,2184,2185],{},"fuera de cualquier ",[20,2186,2187],{},"@layer",", y por eso ganan a las utilidades de Tailwind (",[20,2190,1928],{},[20,2192,2193],{},"bg-elevated",[20,2195,2196],{},"shadow-lg",") sin ",[20,2199,2200],{},"!important",". Dos consecuencias:",[909,2203,2204,2214],{},[912,2205,2206,2207,2209,2210,2213],{},"No las muevas a ",[20,2208,2187],{}," ni las conviertas a ",[20,2211,2212],{},"@utility",": perderían esa prioridad.",[912,2215,2216,2217,334,2220,2223,2224,2227],{},"Al ser CSS plano ",[15,2218,2219],{},"no aceptan variantes",[20,2221,2222],{},"md:ds-panel"," no existe). Si necesitas una variante, escribe una regla ",[20,2225,2226],{},"@media"," explícita.",[273,2229,2231],{"id":2230},"_3-temas-de-componentes","3. Temas de componentes",[11,2233,2234,2236],{},[20,2235,1980],{}," solo enruta slots hacia tokens y clases; nunca declara colores literales:",[132,2238,2242],{"className":2239,"code":2240,"language":2241,"meta":137,"style":137},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","export default defineAppConfig({\n  ui: {\n    colors: { primary: 'claude', neutral: 'stone' },\n    card: { slots: { root: 'ds-card', title: 'font-heading' } }\n  }\n})\n","ts",[20,2243,2244,2264,2276,2318,2365,2370],{"__ignoreMap":137},[141,2245,2246,2250,2253,2257,2261],{"class":143,"line":144},[141,2247,2249],{"class":2248},"s7zQu","export",[141,2251,2252],{"class":2248}," default",[141,2254,2256],{"class":2255},"s2Zo4"," defineAppConfig",[141,2258,2260],{"class":2259},"sTEyZ","(",[141,2262,2263],{"class":1349},"{\n",[141,2265,2266,2270,2273],{"class":143,"line":177},[141,2267,2269],{"class":2268},"swJcz","  ui",[141,2271,2272],{"class":1349},":",[141,2274,2275],{"class":1349}," {\n",[141,2277,2278,2281,2283,2286,2289,2291,2294,2297,2300,2303,2306,2308,2310,2313,2315],{"class":143,"line":183},[141,2279,2280],{"class":2268},"    colors",[141,2282,2272],{"class":1349},[141,2284,2285],{"class":1349}," {",[141,2287,2288],{"class":2268}," primary",[141,2290,2272],{"class":1349},[141,2292,2293],{"class":1349}," '",[141,2295,2296],{"class":150},"claude",[141,2298,2299],{"class":1349},"'",[141,2301,2302],{"class":1349},",",[141,2304,2305],{"class":2268}," neutral",[141,2307,2272],{"class":1349},[141,2309,2293],{"class":1349},[141,2311,2312],{"class":150},"stone",[141,2314,2299],{"class":1349},[141,2316,2317],{"class":1349}," },\n",[141,2319,2320,2323,2325,2327,2330,2332,2334,2337,2339,2341,2343,2345,2347,2350,2352,2354,2357,2359,2362],{"class":143,"line":190},[141,2321,2322],{"class":2268},"    card",[141,2324,2272],{"class":1349},[141,2326,2285],{"class":1349},[141,2328,2329],{"class":2268}," slots",[141,2331,2272],{"class":1349},[141,2333,2285],{"class":1349},[141,2335,2336],{"class":2268}," root",[141,2338,2272],{"class":1349},[141,2340,2293],{"class":1349},[141,2342,2134],{"class":150},[141,2344,2299],{"class":1349},[141,2346,2302],{"class":1349},[141,2348,2349],{"class":2268}," title",[141,2351,2272],{"class":1349},[141,2353,2293],{"class":1349},[141,2355,2356],{"class":150},"font-heading",[141,2358,2299],{"class":1349},[141,2360,2361],{"class":1349}," }",[141,2363,2364],{"class":1349}," }\n",[141,2366,2367],{"class":143,"line":196},[141,2368,2369],{"class":1349},"  }\n",[141,2371,2372,2375],{"class":143,"line":202},[141,2373,2374],{"class":1349},"}",[141,2376,2377],{"class":2259},")\n",[11,2379,2380,2381,2384,2385,2388,2389,2391],{},"Para ajustar ",[15,2382,2383],{},"una sola instancia"," no toques el global: usa la prop ",[20,2386,2387],{},":ui=\"{ slot: '...' }\""," sobre esa instancia. Y recuerda que las ",[20,2390,928],{}," ganan a las utilidades: para anular una superficie puntual necesitas una regla propia.",[41,2393,2395],{"id":2394},"patrones-de-la-casa","Patrones de la casa",[273,2397,2399],{"id":2398},"card-con-cabecera-de-icono","Card con cabecera de icono",[11,2401,2402],{},"El patrón que usan las páginas reales (icono en squircle, título de una línea, subtítulo de una):",[132,2404,2408],{"className":2405,"code":2406,"language":2407,"meta":137,"style":137},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003CUPageCard variant=\"subtle\">\n  \u003Ctemplate #header>\n    \u003Cdiv class=\"flex items-center gap-4 p-1\">\n      \u003Cdiv class=\"flex items-center justify-center w-11 h-11 rounded-xl bg-primary\u002F10 shrink-0\">\n        \u003CUIcon name=\"i-lucide-lock-keyhole\" class=\"w-6 h-6 text-primary\" \u002F>\n      \u003C\u002Fdiv>\n      \u003Cdiv>\n        \u003Ch3 class=\"text-base font-semibold\">Título\u003C\u002Fh3>\n        \u003Cp class=\"text-sm text-muted\">Una frase de subtítulo\u003C\u002Fp>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n\u003C\u002FUPageCard>\n","vue",[20,2409,2410,2435,2440,2445,2450,2455,2460,2465,2470,2475,2479,2484,2489],{"__ignoreMap":137},[141,2411,2412,2415,2418,2422,2425,2427,2430,2432],{"class":143,"line":144},[141,2413,2414],{"class":1349},"\u003C",[141,2416,2417],{"class":2268},"UPageCard",[141,2419,2421],{"class":2420},"spNyl"," variant",[141,2423,2424],{"class":1349},"=",[141,2426,1356],{"class":1349},[141,2428,2429],{"class":150},"subtle",[141,2431,1356],{"class":1349},[141,2433,2434],{"class":1349},">\n",[141,2436,2437],{"class":143,"line":177},[141,2438,2439],{"class":2259},"  \u003Ctemplate #header>\n",[141,2441,2442],{"class":143,"line":183},[141,2443,2444],{"class":2259},"    \u003Cdiv class=\"flex items-center gap-4 p-1\">\n",[141,2446,2447],{"class":143,"line":190},[141,2448,2449],{"class":2259},"      \u003Cdiv class=\"flex items-center justify-center w-11 h-11 rounded-xl bg-primary\u002F10 shrink-0\">\n",[141,2451,2452],{"class":143,"line":196},[141,2453,2454],{"class":2259},"        \u003CUIcon name=\"i-lucide-lock-keyhole\" class=\"w-6 h-6 text-primary\" \u002F>\n",[141,2456,2457],{"class":143,"line":202},[141,2458,2459],{"class":2259},"      \u003C\u002Fdiv>\n",[141,2461,2462],{"class":143,"line":207},[141,2463,2464],{"class":2259},"      \u003Cdiv>\n",[141,2466,2467],{"class":143,"line":213},[141,2468,2469],{"class":2259},"        \u003Ch3 class=\"text-base font-semibold\">Título\u003C\u002Fh3>\n",[141,2471,2472],{"class":143,"line":219},[141,2473,2474],{"class":2259},"        \u003Cp class=\"text-sm text-muted\">Una frase de subtítulo\u003C\u002Fp>\n",[141,2476,2477],{"class":143,"line":224},[141,2478,2459],{"class":2259},[141,2480,2481],{"class":143,"line":230},[141,2482,2483],{"class":2259},"    \u003C\u002Fdiv>\n",[141,2485,2486],{"class":143,"line":236},[141,2487,2488],{"class":2259},"  \u003C\u002Ftemplate>\n",[141,2490,2491,2494,2496],{"class":143,"line":242},[141,2492,2493],{"class":1349},"\u003C\u002F",[141,2495,2417],{"class":2268},[141,2497,2434],{"class":1349},[273,2499,2501],{"id":2500},"formulario","Formulario",[11,2503,2504,85,2507,2510,2511,825,2514,2517,2518,2521,2522,825,2525,2528,2529,2532,2533,2535,2536,2539],{},[20,2505,2506],{},"\u003CUForm class=\"space-y-5\">",[20,2508,2509],{},"UFormField"," con ",[20,2512,2513],{},":label",[20,2515,2516],{},":description"," arriba del campo → ",[20,2519,2520],{},"UInput class=\"w-full\""," → fila de submit a la derecha con ",[20,2523,2524],{},":loading",[20,2526,2527],{},":disabled"," enlazados al estado de la acción. El ",[20,2530,2531],{},"help"," de ",[20,2534,2509],{}," es el que va ",[15,2537,2538],{},"debajo"," del input.",[273,2541,2543],{"id":2542},"estado-de-acción-regla-del-proyecto","Estado de acción (regla del proyecto)",[11,2545,2546,2547,2272],{},"Toda acción asíncrona declara un ref con nombre de verbo, se protege contra doble clic y se resetea en ",[20,2548,2549],{},"finally",[132,2551,2553],{"className":2239,"code":2552,"language":2241,"meta":137,"style":137},"const saving = ref(false)\n\nasync function save() {\n  if (saving.value) return\n  saving.value = true\n  try {\n    await saveMutation.mutateAsync(data)\n  } finally {\n    saving.value = false\n  }\n}\n",[20,2554,2555,2576,2580,2596,2617,2632,2639,2659,2669,2683,2687],{"__ignoreMap":137},[141,2556,2557,2560,2563,2565,2568,2570,2574],{"class":143,"line":144},[141,2558,2559],{"class":2420},"const",[141,2561,2562],{"class":2259}," saving ",[141,2564,2424],{"class":1349},[141,2566,2567],{"class":2255}," ref",[141,2569,2260],{"class":2259},[141,2571,2573],{"class":2572},"sfNiH","false",[141,2575,2377],{"class":2259},[141,2577,2578],{"class":143,"line":177},[141,2579,187],{"emptyLinePlaceholder":186},[141,2581,2582,2585,2588,2591,2594],{"class":143,"line":183},[141,2583,2584],{"class":2420},"async",[141,2586,2587],{"class":2420}," function",[141,2589,2590],{"class":2255}," save",[141,2592,2593],{"class":1349},"()",[141,2595,2275],{"class":1349},[141,2597,2598,2601,2603,2606,2608,2611,2614],{"class":143,"line":190},[141,2599,2600],{"class":2248},"  if",[141,2602,334],{"class":2268},[141,2604,2605],{"class":2259},"saving",[141,2607,27],{"class":1349},[141,2609,2610],{"class":2259},"value",[141,2612,2613],{"class":2268},") ",[141,2615,2616],{"class":2248},"return\n",[141,2618,2619,2622,2624,2626,2629],{"class":143,"line":196},[141,2620,2621],{"class":2259},"  saving",[141,2623,27],{"class":1349},[141,2625,2610],{"class":2259},[141,2627,2628],{"class":1349}," =",[141,2630,2631],{"class":2572}," true\n",[141,2633,2634,2637],{"class":143,"line":202},[141,2635,2636],{"class":2248},"  try",[141,2638,2275],{"class":1349},[141,2640,2641,2644,2647,2649,2652,2654,2657],{"class":143,"line":207},[141,2642,2643],{"class":2248},"    await",[141,2645,2646],{"class":2259}," saveMutation",[141,2648,27],{"class":1349},[141,2650,2651],{"class":2255},"mutateAsync",[141,2653,2260],{"class":2268},[141,2655,2656],{"class":2259},"data",[141,2658,2377],{"class":2268},[141,2660,2661,2664,2667],{"class":143,"line":213},[141,2662,2663],{"class":1349},"  }",[141,2665,2666],{"class":2248}," finally",[141,2668,2275],{"class":1349},[141,2670,2671,2674,2676,2678,2680],{"class":143,"line":219},[141,2672,2673],{"class":2259},"    saving",[141,2675,27],{"class":1349},[141,2677,2610],{"class":2259},[141,2679,2628],{"class":1349},[141,2681,2682],{"class":2572}," false\n",[141,2684,2685],{"class":143,"line":224},[141,2686,2369],{"class":1349},[141,2688,2689],{"class":143,"line":230},[141,2690,2691],{"class":1349},"}\n",[132,2693,2695],{"className":2405,"code":2694,"language":2407,"meta":137,"style":137},"\u003CUButton :loading=\"saving\" :disabled=\"saving\" @click=\"save\">Guardar\u003C\u002FUButton>\n",[20,2696,2697],{"__ignoreMap":137},[141,2698,2699,2701,2704,2707,2710,2712,2714,2716,2718,2720,2723,2725,2727,2729,2731,2734,2737,2739,2741,2744,2746,2749,2752,2754,2756],{"class":143,"line":144},[141,2700,2414],{"class":1349},[141,2702,2703],{"class":2268},"UButton",[141,2705,2706],{"class":1349}," :",[141,2708,2709],{"class":2420},"loading",[141,2711,2424],{"class":1349},[141,2713,1356],{"class":1349},[141,2715,2605],{"class":2259},[141,2717,1356],{"class":1349},[141,2719,2706],{"class":1349},[141,2721,2722],{"class":2420},"disabled",[141,2724,2424],{"class":1349},[141,2726,1356],{"class":1349},[141,2728,2605],{"class":2259},[141,2730,1356],{"class":1349},[141,2732,2733],{"class":1349}," @",[141,2735,2736],{"class":2420},"click",[141,2738,2424],{"class":1349},[141,2740,1356],{"class":1349},[141,2742,2743],{"class":2259},"save",[141,2745,1356],{"class":1349},[141,2747,2748],{"class":1349},">",[141,2750,2751],{"class":2259},"Guardar",[141,2753,2493],{"class":1349},[141,2755,2703],{"class":2268},[141,2757,2434],{"class":1349},[41,2759,2761],{"id":2760},"qué-no-hacer","Qué no hacer",[132,2763,2765],{"className":2405,"code":2764,"language":2407,"meta":137,"style":137},"\u003C!-- ❌ Colores y sombras literales, se rompen en el modo contrario -->\n\u003Cdiv class=\"bg-white text-black shadow-[0_1px_3px_rgba(0,0,0,0.2)]\">…\u003C\u002Fdiv>\n\n\u003C!-- ✅ Superficie del sistema + utilidades semánticas -->\n\u003Cdiv class=\"ds-card rounded-lg p-5 text-highlighted\">…\u003C\u002Fdiv>\n\n\u003C!-- ❌ Un segundo acento inventado -->\n\u003CUBadge class=\"bg-purple-500\">…\u003C\u002FUBadge>\n\n\u003C!-- ✅ El acento es `primary` (o un color de estado para señales) -->\n\u003CUBadge color=\"primary\">…\u003C\u002FUBadge>\n",[20,2766,2767,2772,2802,2806,2811,2838,2842,2847,2875,2879,2884],{"__ignoreMap":137},[141,2768,2769],{"class":143,"line":144},[141,2770,2771],{"class":762},"\u003C!-- ❌ Colores y sombras literales, se rompen en el modo contrario -->\n",[141,2773,2774,2776,2779,2782,2784,2786,2789,2791,2793,2796,2798,2800],{"class":143,"line":177},[141,2775,2414],{"class":1349},[141,2777,2778],{"class":2268},"div",[141,2780,2781],{"class":2420}," class",[141,2783,2424],{"class":1349},[141,2785,1356],{"class":1349},[141,2787,2788],{"class":150},"bg-white text-black shadow-[0_1px_3px_rgba(0,0,0,0.2)]",[141,2790,1356],{"class":1349},[141,2792,2748],{"class":1349},[141,2794,2795],{"class":2259},"…",[141,2797,2493],{"class":1349},[141,2799,2778],{"class":2268},[141,2801,2434],{"class":1349},[141,2803,2804],{"class":143,"line":183},[141,2805,187],{"emptyLinePlaceholder":186},[141,2807,2808],{"class":143,"line":190},[141,2809,2810],{"class":762},"\u003C!-- ✅ Superficie del sistema + utilidades semánticas -->\n",[141,2812,2813,2815,2817,2819,2821,2823,2826,2828,2830,2832,2834,2836],{"class":143,"line":196},[141,2814,2414],{"class":1349},[141,2816,2778],{"class":2268},[141,2818,2781],{"class":2420},[141,2820,2424],{"class":1349},[141,2822,1356],{"class":1349},[141,2824,2825],{"class":150},"ds-card rounded-lg p-5 text-highlighted",[141,2827,1356],{"class":1349},[141,2829,2748],{"class":1349},[141,2831,2795],{"class":2259},[141,2833,2493],{"class":1349},[141,2835,2778],{"class":2268},[141,2837,2434],{"class":1349},[141,2839,2840],{"class":143,"line":202},[141,2841,187],{"emptyLinePlaceholder":186},[141,2843,2844],{"class":143,"line":207},[141,2845,2846],{"class":762},"\u003C!-- ❌ Un segundo acento inventado -->\n",[141,2848,2849,2851,2854,2856,2858,2860,2863,2865,2867,2869,2871,2873],{"class":143,"line":213},[141,2850,2414],{"class":1349},[141,2852,2853],{"class":2268},"UBadge",[141,2855,2781],{"class":2420},[141,2857,2424],{"class":1349},[141,2859,1356],{"class":1349},[141,2861,2862],{"class":150},"bg-purple-500",[141,2864,1356],{"class":1349},[141,2866,2748],{"class":1349},[141,2868,2795],{"class":2259},[141,2870,2493],{"class":1349},[141,2872,2853],{"class":2268},[141,2874,2434],{"class":1349},[141,2876,2877],{"class":143,"line":219},[141,2878,187],{"emptyLinePlaceholder":186},[141,2880,2881],{"class":143,"line":224},[141,2882,2883],{"class":762},"\u003C!-- ✅ El acento es `primary` (o un color de estado para señales) -->\n",[141,2885,2886,2888,2890,2893,2895,2897,2900,2902,2904,2906,2908,2910],{"class":143,"line":230},[141,2887,2414],{"class":1349},[141,2889,2853],{"class":2268},[141,2891,2892],{"class":2420}," color",[141,2894,2424],{"class":1349},[141,2896,1356],{"class":1349},[141,2898,2899],{"class":150},"primary",[141,2901,1356],{"class":1349},[141,2903,2748],{"class":1349},[141,2905,2795],{"class":2259},[141,2907,2493],{"class":1349},[141,2909,2853],{"class":2268},[141,2911,2434],{"class":1349},[41,2913,2915],{"id":2914},"verificación","Verificación",[909,2917,2918,2924,2929,2946],{},[912,2919,2920,2921,27],{},"Cambia con el toggle de tema (menú del usuario o controles de apariencia de las páginas públicas): un cambio está hecho solo cuando se ve bien en ",[15,2922,2923],{},"claro y oscuro",[912,2925,2926,2928],{},[20,2927,1739],{}," no valida colores; la prueba real es el navegador.",[912,2930,2931,2932,2532,2935,2938,2939,2942,2943,1174],{},"Si tocaste tokens, revisa también el ",[20,2933,2934],{},"theme-color",[20,2936,2937],{},"app\u002Fapp.vue"," (claro ",[20,2940,2941],{},"#faf9f5",", oscuro ",[20,2944,2945],{},"#1f1e1b",[912,2947,2948,2949,2952,2953,2955],{},"Para colores nuevos: la paleta necesita los 11 pasos (",[20,2950,2951],{},"50…950",") y se registra como alias en ",[20,2954,2054],{},", no como hex suelto.",[938,2957,2958],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":137,"searchDepth":177,"depth":177,"links":2960},[2961,2962,2968,2973,2974],{"id":1938,"depth":177,"text":1939},{"id":2006,"depth":177,"text":2007,"children":2963},[2964,2965,2967],{"id":2010,"depth":183,"text":2011},{"id":2107,"depth":183,"text":2966},"2. Clases .ds-*",{"id":2230,"depth":183,"text":2231},{"id":2394,"depth":177,"text":2395,"children":2969},[2970,2971,2972],{"id":2398,"depth":183,"text":2399},{"id":2500,"depth":183,"text":2501},{"id":2542,"depth":183,"text":2543},{"id":2760,"depth":177,"text":2761},{"id":2914,"depth":177,"text":2915},"Los tres niveles del sistema (tokens, clases .ds-*, temas de componentes) y las reglas para no salirse de él.",{},{"icon":2978},"i-lucide-swatch-book",{"title":924,"description":2975},"docs\u002F3.design-system","9vXJbrcAL9-_3STXPmoRz1HQo69Px5jGVG3X-v8-cMM",{"id":2983,"title":935,"body":2984,"description":3899,"extension":954,"meta":3900,"navigation":3901,"path":934,"seo":3903,"stem":3904,"__hash__":3905},"docs\u002Fdocs\u002F4.deploy.md",{"type":8,"value":2985,"toc":3887},[2986,2999,3043,3049,3053,3056,3061,3169,3174,3231,3252,3273,3277,3310,3316,3329,3426,3473,3482,3486,3504,3527,3531,3538,3590,3670,3735,3739,3746,3775,3798,3802,3815,3845,3849,3885],[11,2987,2988,2989,2992,2993,2995,2996,2998],{},"El despliegue corre sobre ",[15,2990,2991],{},"Dokploy",", disparado por GitHub Actions después de un CI en verde. Hay dos entornos: producción (rama ",[20,2994,971],{},") y dev (rama ",[20,2997,974],{},", con su propio dominio y su propia base de datos).",[46,3000,3001,3009],{},[49,3002,3003],{},[52,3004,3005,3007],{},[55,3006,1798],{},[55,3008,1801],{},[65,3010,3011,3026,3034],{},[52,3012,3013,3016],{},[70,3014,3015],{},"PR",[70,3017,3018,3019,3021,3022,3025],{},"CI: ",[20,3020,1739],{}," + ",[20,3023,3024],{},"pnpm build"," (bloqueantes) y typecheck informativo",[52,3027,3028,3032],{},[70,3029,1858,3030],{},[20,3031,974],{},[70,3033,1863],{},[52,3035,3036,3040],{},[70,3037,1858,3038],{},[20,3039,971],{},[70,3041,3042],{},"CI + despliegue de producción con health check",[132,3044,3047],{"className":3045,"code":3046,"language":1098,"meta":137},[1096],"feature\u002F* · fix\u002F* ──push──▶ PR ──▶ merge a dev ──▶ CI ──▶ Deploy (dev) ──▶ https:\u002F\u002Fdev.tudominio.com\n                                                     │\n                                       (cuando dev está estable)\n                                                     ▼\n                                    PR release: dev ──▶ main ──▶ CI ──▶ Deploy (prod) ──▶ health check\n",[20,3048,3046],{"__ignoreMap":137},[41,3050,3052],{"id":3051},"activar-el-despliegue","Activar el despliegue",[11,3054,3055],{},"Los workflows vienen apagados; se encienden con variables y secretos del repositorio (Settings → Secrets and variables → Actions).",[11,3057,3058],{},[15,3059,3060],{},"Variables",[46,3062,3063,3074],{},[49,3064,3065],{},[52,3066,3067,3069,3072],{},[55,3068,285],{},[55,3070,3071],{},"Ejemplo",[55,3073,63],{},[65,3075,3076,3091,3108,3123,3137,3154],{},[52,3077,3078,3083,3088],{},[70,3079,3080],{},[20,3081,3082],{},"DOKPLOY_DEPLOY_ENABLED",[70,3084,3085],{},[20,3086,3087],{},"true",[70,3089,3090],{},"Enciende el deploy de producción",[52,3092,3093,3097,3102],{},[70,3094,3095],{},[20,3096,348],{},[70,3098,3099],{},[20,3100,3101],{},"https:\u002F\u002Ftudominio.com",[70,3103,3104,3107],{},[20,3105,3106],{},"baseUrl"," de i18n (build-arg)",[52,3109,3110,3115,3120],{},[70,3111,3112],{},[20,3113,3114],{},"HEALTHCHECK_URL",[70,3116,3117],{},[20,3118,3119],{},"https:\u002F\u002Ftudominio.com\u002Fapi\u002Fhealth",[70,3121,3122],{},"Verifica el rollout",[52,3124,3125,3130,3134],{},[70,3126,3127],{},[20,3128,3129],{},"DOKPLOY_DEV_DEPLOY_ENABLED",[70,3131,3132],{},[20,3133,3087],{},[70,3135,3136],{},"Enciende el deploy del entorno dev",[52,3138,3139,3144,3149],{},[70,3140,3141],{},[20,3142,3143],{},"DEV_NUXT_APP_URL",[70,3145,3146],{},[20,3147,3148],{},"https:\u002F\u002Fdev.tudominio.com",[70,3150,3151,3153],{},[20,3152,3106],{}," de i18n de dev (build-arg)",[52,3155,3156,3161,3166],{},[70,3157,3158],{},[20,3159,3160],{},"DEV_HEALTHCHECK_URL",[70,3162,3163],{},[20,3164,3165],{},"https:\u002F\u002Fdev.tudominio.com\u002Fapi\u002Fhealth",[70,3167,3168],{},"Verifica el rollout de dev",[11,3170,3171],{},[15,3172,3173],{},"Secretos",[46,3175,3176,3185],{},[49,3177,3178],{},[52,3179,3180,3183],{},[55,3181,3182],{},"Secret",[55,3184,63],{},[65,3186,3187,3197,3211,3221],{},[52,3188,3189,3194],{},[70,3190,3191],{},[20,3192,3193],{},"DOKPLOY_WEBHOOK_URL",[70,3195,3196],{},"Webhook de la app de producción en Dokploy",[52,3198,3199,3204],{},[70,3200,3201],{},[20,3202,3203],{},"PRODUCTION_DATABASE_URL",[70,3205,3206,3207,3210],{},"Postgres ",[15,3208,3209],{},"alcanzable desde GitHub"," para migrar (ver nota)",[52,3212,3213,3218],{},[70,3214,3215],{},[20,3216,3217],{},"DOKPLOY_DEV_WEBHOOK_URL",[70,3219,3220],{},"Webhook de la app dev",[52,3222,3223,3228],{},[70,3224,3225],{},[20,3226,3227],{},"DEV_DATABASE_URL",[70,3229,3230],{},"Migraciones de la base de dev",[667,3232,3233],{},[11,3234,3235,3238,3239,3242,3243,3247,3248,3251],{},[15,3236,3237],{},"Migraciones."," El paso de migraciones corre en el runner: con un Postgres interno de Dokploy hay que publicar su ",[15,3240,3241],{},"puerto externo"," (servicio de base de datos → ",[3244,3245,3246],"em",{},"External Credentials",") y usar ",[20,3249,3250],{},"postgresql:\u002F\u002F\u003Cuser>:\u003Cpassword>@\u003Cip-del-servidor>:\u003Cpuerto>\u002F\u003Cbase>",". Esa base queda expuesta a internet: contraseña larga y, si puedes, filtra el puerto por firewall a las IPs de GitHub.",[732,3253,3254],{},[11,3255,3256,3257,3260,3261,3264,3265,3268,3269,3272],{},"Si la app usa un provider ",[3244,3258,3259],{},"Git"," en Dokploy, elige ",[15,3262,3263],{},"un solo mecanismo",": o su auto-deploy (al hacer push) o el workflow — con los dos activos, cada merge despliega dos veces. Con provider ",[3244,3266,3267],{},"Docker"," (imagen de GHCR) no hay conflicto, pero ",[15,3270,3271],{},"Autodeploy debe quedar activado",": es lo que habilita el webhook de despliegue.",[41,3274,3276],{"id":3275},"imagen-ghcr","Imagen (GHCR)",[11,3278,3279,3280,3283,3284,3287,3288,3291,3292,3295,3296,3299,3300,3302,3303,3306,3307,1174],{},"El build corre en GitHub y publica una imagen en GitHub Container Registry\n(",[20,3281,3282],{},"ghcr.io\u002F\u003Cowner>\u002Ffoodstack","): tag ",[20,3285,3286],{},":main"," para producción, ",[20,3289,3290],{},":dev"," para dev y ",[20,3293,3294],{},":sha-\u003Ccommit>","\ninmutable para rollback. Dokploy deja de construir el repo: en cada aplicación, ",[3244,3297,3298],{},"Source"," →\n",[15,3301,3267],{}," → la imagen del entorno (puerto ",[20,3304,3305],{},"3000","). Si el paquete es privado, agrega\ncredenciales de registro (PAT con ",[20,3308,3309],{},"read:packages",[41,3311,3313,3314,341],{"id":3312},"el-entorno-dev-devtudominiocom","El entorno dev (",[20,3315,1028],{},[11,3317,3318,3319,3321,3322,2532,3325,3328],{},"Una segunda aplicación en Dokploy que sirve la rama ",[20,3320,974],{},", con base de datos propia. Los pasos completos están en la sección ",[3244,3323,3324],{},"Entorno dev",[20,3326,3327],{},"DEPLOYMENT.md","; en resumen:",[1106,3330,3331,3342,3362,3376,3390,3401],{},[912,3332,3333,3336,3337,3339,3340,27],{},[15,3334,3335],{},"Base de datos",": una rama (branch) de Neon para ",[20,3338,974],{},", creada desde la principal. Su connection string será ",[20,3341,3227],{},[912,3343,3344,1546,3347,85,3349,3351,3352,3355,3356,3358,3359,27],{},[15,3345,3346],{},"Aplicación en Dokploy",[3244,3348,3298],{},[15,3350,3267],{}," con la imagen ",[20,3353,3354],{},"ghcr.io\u002F\u003Cowner>\u002Ffoodstack:dev",", puerto del contenedor ",[20,3357,3305],{},". Health check en ",[20,3360,3361],{},"\u002Fapi\u002Fhealth",[912,3363,3364,1546,3367,3369,3370,3372,3373,3375],{},[15,3365,3366],{},"Variables de la app dev",[20,3368,299],{}," (rama dev), ",[20,3371,321],{}," propio y ",[20,3374,348],{}," apuntando al dominio de dev. Better Auth usa esa URL como baseURL: magic links, invitaciones y callbacks deben apuntar ahí.",[912,3377,3378,3381,3382,3385,3386,3389],{},[15,3379,3380],{},"Dominio",": agrega el host en Dokploy (HTTPS con Let's Encrypt) y crea un registro DNS ",[20,3383,3384],{},"A"," apuntando a la IP del servidor (o ",[20,3387,3388],{},"CNAME"," al dominio raíz).",[912,3391,3392,3395,3396,3398,3399,27],{},[15,3393,3394],{},"Automatización",": pega el webhook en el secreto ",[20,3397,3217],{}," y enciende ",[20,3400,3129],{},[912,3402,3403,1546,3406,3021,3409,3412,3413,860,3415,3417,3418,3421,3422,3425],{},[15,3404,3405],{},"E2E remoto (opcional)",[20,3407,3408],{},"E2E_ENABLED=true",[20,3410,3411],{},"E2E_BASE_URL"," y los secretos ",[20,3414,859],{},[20,3416,863],{}," de una cuenta ",[20,3419,3420],{},"owner"," de dev. La suite ",[15,3423,3424],{},"escribe y borra datos",": apúntala solo a dev.",[132,3427,3429],{"className":134,"code":3428,"language":136,"meta":137,"style":137},"# Primera vez: crea la rama dev desde un main que ya tenga los workflows\ngit switch main && git pull\ngit switch -c dev\ngit push -u origin dev\n",[20,3430,3431,3436,3451,3461],{"__ignoreMap":137},[141,3432,3433],{"class":143,"line":144},[141,3434,3435],{"class":762},"# Primera vez: crea la rama dev desde un main que ya tenga los workflows\n",[141,3437,3438,3440,3442,3444,3446,3448],{"class":143,"line":177},[141,3439,1220],{"class":147},[141,3441,1223],{"class":150},[141,3443,1438],{"class":150},[141,3445,1416],{"class":1349},[141,3447,1419],{"class":147},[141,3449,3450],{"class":150}," pull\n",[141,3452,3453,3455,3457,3459],{"class":143,"line":183},[141,3454,1220],{"class":147},[141,3456,1223],{"class":150},[141,3458,1256],{"class":150},[141,3460,1226],{"class":150},[141,3462,3463,3465,3467,3469,3471],{"class":143,"line":190},[141,3464,1220],{"class":147},[141,3466,1309],{"class":150},[141,3468,1312],{"class":150},[141,3470,1236],{"class":150},[141,3472,1226],{"class":150},[29,3474,3475],{},[11,3476,3477,3478,3481],{},"Atajo interactivo: ",[20,3479,3480],{},".\u002Fscripts\u002Fsetup-dokploy-dev.sh"," (raíz del repo) recorre este montaje paso a paso — abre Neon y Dokploy, captura los valores, genera el secreto de auth, escribe los secrets\u002Fvariables en GitHub y compone el bloque de variables listo para pegar en Dokploy.",[273,3483,3485],{"id":3484},"variables-por-entorno","Variables por entorno",[11,3487,3488,3489,3492,3493,3496,3497,3500,3501,27],{},"Cada aplicación de Dokploy tiene su propio ",[15,3490,3491],{},"Environment",": la app de dev y la de producción no comparten variables aunque vivan en el mismo proyecto. Hay tres niveles — compartidas del proyecto (",[20,3494,3495],{},"${{project.VAR}}","), del entorno (",[20,3498,3499],{},"${{environment.VAR}}",", si tu versión los soporta) y del servicio (las de cada app, que pisan a las anteriores); lo que cambia por entorno va siempre a nivel de ",[15,3502,3503],{},"servicio",[11,3505,3506,3507,3509,3510,3513,3514,3516,3517,3519,3520,860,3522,3524,3525,1174],{},"Guardar cambios en la pestaña ",[3244,3508,3491],{}," no toca el contenedor en marcha: lanza un ",[15,3511,3512],{},"Deploy"," después de editar. ",[20,3515,348],{}," además se lee al construir (i18n ",[20,3518,3106],{},"): va como build-arg desde las variables ",[20,3521,348],{},[20,3523,3143],{}," de GitHub (ver ",[3244,3526,3276],{},[41,3528,3530],{"id":3529},"páginas-estáticas-prerender","Páginas estáticas (prerender)",[11,3532,3533,3534,3537],{},"Las páginas públicas se generan en el build y se sirven como archivos estáticos desde ",[20,3535,3536],{},".output\u002Fpublic","; el resto (dashboard, auth, API) sigue renderizándose en el servidor.",[46,3539,3540,3550],{},[49,3541,3542],{},[52,3543,3544,3547],{},[55,3545,3546],{},"Ruta",[55,3548,3549],{},"Cómo se sirve",[65,3551,3552,3566,3579],{},[52,3553,3554,3559],{},[70,3555,3556],{},[20,3557,3558],{},"\u002F",[70,3560,3561,3562,3565],{},"Estática (HTML + ",[20,3563,3564],{},"_payload.json","), sin coste SSR",[52,3567,3568,3576],{},[70,3569,3570,3021,3573],{},[20,3571,3572],{},"\u002Fdocs",[20,3574,3575],{},"\u002Fdocs\u002F**",[70,3577,3578],{},"Estática, una por archivo Markdown",[52,3580,3581,3584],{},[70,3582,3583],{},"Todo lo demás",[70,3585,3586,3587],{},"SSR en ",[20,3588,3589],{},"node .output\u002Fserver\u002Findex.mjs",[909,3591,3592,3627,3641,3653,3660],{},[912,3593,3594,3595,334,3598,3600,3601,308,3604,308,3607,825,3610,2510,3613,3616,3617,3620,3621,825,3624,27],{},"La lista vive en ",[20,3596,3597],{},"routeRules",[20,3599,356],{},"): ",[20,3602,3603],{},"'\u002F'",[20,3605,3606],{},"'\u002F_payload.json'",[20,3608,3609],{},"'\u002Fdocs'",[20,3611,3612],{},"'\u002Fdocs\u002F**'",[20,3614,3615],{},"prerender: true",", y un catch-all ",[20,3618,3619],{},"'\u002F**': { prerender: false }"," que mantiene al crawler fuera de ",[20,3622,3623],{},"\u002Fapp",[20,3625,3626],{},"\u002Fauth",[912,3628,3629,3632,3633,3636,3637,3640],{},[15,3630,3631],{},"Agregar una sección no requiere tocar la config",": el crawler (",[20,3634,3635],{},"crawlLinks",") descubre los archivos nuevos de ",[20,3638,3639],{},"content\u002Fdocs\u002F"," desde los enlaces del índice.",[912,3642,3643,3648,3649,3652],{},[15,3644,3645,3646],{},"El build pasa sin ",[20,3647,161],{}," (CI no tiene secretos): auth, colas y Resend se protegen con ",[20,3650,3651],{},"import.meta.prerender",". No hay nada que configurar.",[912,3654,3655,3656,3659],{},"El HTML se genera en inglés; el idioma del visitante (cookie ",[20,3657,3658],{},"i18n_locale",") se aplica en el cliente inmediatamente después de la hidratación, sin recargar.",[912,3661,3662,3663,3665,3666,1151,3668,27],{},"Si en el futuro agregas otra página pública, dale su propia regla ",[20,3664,3615],{}," (el catch-all la bloquearía) y verificá que exista su ",[20,3667,3564],{},[20,3669,3536],{},[132,3671,3673],{"className":134,"code":3672,"language":136,"meta":137,"style":137},"pnpm build                              # 13 rutas prerenderizadas\n(nohup node .output\u002Fserver\u002Findex.mjs &) # sirve .output: estáticas + SSR\nmv .env .env.ci-bak && pnpm build; mv .env.ci-bak .env   # la prueba de CI, sin secretos\n",[20,3674,3675,3685,3704],{"__ignoreMap":137},[141,3676,3677,3679,3682],{"class":143,"line":144},[141,3678,17],{"class":147},[141,3680,3681],{"class":150}," build",[141,3683,3684],{"class":762},"                              # 13 rutas prerenderizadas\n",[141,3686,3687,3689,3692,3695,3698,3701],{"class":143,"line":177},[141,3688,2260],{"class":1349},[141,3690,3691],{"class":147},"nohup",[141,3693,3694],{"class":150}," node",[141,3696,3697],{"class":150}," .output\u002Fserver\u002Findex.mjs",[141,3699,3700],{"class":1349}," &)",[141,3702,3703],{"class":762}," # sirve .output: estáticas + SSR\n",[141,3705,3706,3709,3712,3715,3717,3720,3722,3725,3728,3730,3732],{"class":143,"line":183},[141,3707,3708],{"class":147},"mv",[141,3710,3711],{"class":150}," .env",[141,3713,3714],{"class":150}," .env.ci-bak",[141,3716,1416],{"class":1349},[141,3718,3719],{"class":147}," pnpm",[141,3721,3681],{"class":150},[141,3723,3724],{"class":1349},";",[141,3726,3727],{"class":147}," mv",[141,3729,3714],{"class":150},[141,3731,3711],{"class":150},[141,3733,3734],{"class":762},"   # la prueba de CI, sin secretos\n",[41,3736,3738],{"id":3737},"migraciones-expandcontract","Migraciones (expand\u002Fcontract)",[11,3740,3741,3742,3745],{},"El paso de migraciones corre ",[15,3743,3744],{},"antes"," del rollout, así que durante unos minutos conviven dos versiones del código contra el mismo esquema. Toda migración sigue el patrón expand\u002Fcontract:",[1106,3747,3748,3757,3763,3769],{},[912,3749,3750,3753,3754,1174],{},[15,3751,3752],{},"Expand",": agrega (columna nullable, tabla nueva, índice ",[20,3755,3756],{},"CONCURRENTLY",[912,3758,3759,3762],{},[15,3760,3761],{},"Migrar código",": despliega la versión que escribe ambos formatos o que usa lo nuevo.",[912,3764,3765,3768],{},[15,3766,3767],{},"Backfill",": rellena datos con un script o una tarea, fuera del deploy.",[912,3770,3771,3774],{},[15,3772,3773],{},"Contract",": borra lo viejo (NOT NULL, drop) recién en un deploy posterior.",[132,3776,3778],{"className":134,"code":3777,"language":136,"meta":137,"style":137},"pnpm db:generate   # después de cambiar el schema\npnpm db:migrate    # lo aplica también el workflow de deploy\n",[20,3779,3780,3789],{"__ignoreMap":137},[141,3781,3782,3784,3786],{"class":143,"line":144},[141,3783,17],{"class":147},[141,3785,770],{"class":150},[141,3787,3788],{"class":762},"   # después de cambiar el schema\n",[141,3790,3791,3793,3795],{"class":143,"line":177},[141,3792,17],{"class":147},[141,3794,759],{"class":150},[141,3796,3797],{"class":762},"    # lo aplica también el workflow de deploy\n",[41,3799,3801],{"id":3800},"backups-de-la-base-de-datos","Backups de la base de datos",[11,3803,3804,3805,3807,3808,3811,3812,3814],{},"El dashboard admin (",[20,3806,677],{},") genera backups manuales: un job de pg-boss corre ",[20,3809,3810],{},"pg_dump --format=custom"," y sube el dump al bucket dedicado (",[20,3813,574],{},"). Dos requisitos de despliegue:",[909,3816,3817,3831],{},[912,3818,3819,3824,3825,334,3827,3830],{},[15,3820,3821,3823],{},[20,3822,603],{}," en la imagen",": lo instala el ",[20,3826,692],{},[20,3828,3829],{},"postgresql-client-17"," desde el repo PGDG); debe ser una versión ≥ a la del servidor PostgreSQL.",[912,3832,3833,3836,3837,3839,3840,3842,3843,27],{},[15,3834,3835],{},"Conexión directa",": si ",[20,3838,299],{}," es la pooled de Neon (",[20,3841,610],{},"), el dump deriva la conexión directa automáticamente; también se puede fijar ",[20,3844,594],{},[41,3846,3848],{"id":3847},"salud-y-rollback","Salud y rollback",[909,3850,3851,3859,3876],{},[912,3852,3853,334,3856,3858],{},[15,3854,3855],{},"Liveness",[20,3857,3361],{},"): público y barato, no toca la base de datos. Es el que usan Dokploy y el paso final del workflow.",[912,3860,3861,3864,3865,3558,3867,3869,3870,3872,3873,3875],{},[15,3862,3863],{},"Rollback",": los tags ",[20,3866,3286],{},[20,3868,3290],{}," son móviles; para volver atrás apunta la app a un ",[20,3871,3294],{}," inmutable y lanza un ",[3244,3874,3512],{},". Gracias a expand\u002Fcontract, la versión anterior del código sigue siendo compatible con el esquema.",[912,3877,3878,334,3881,3884],{},[15,3879,3880],{},"Salud profunda",[20,3882,3883],{},"\u002Fapi\u002Fadmin\u002Fsystem\u002Fhealth","): base de datos, Redis, colas y backups, solo para administradores.",[938,3886,1880],{},{"title":137,"searchDepth":177,"depth":177,"links":3888},[3889,3890,3891,3895,3896,3897,3898],{"id":3051,"depth":177,"text":3052},{"id":3275,"depth":177,"text":3276},{"id":3312,"depth":177,"text":3892,"children":3893},"El entorno dev (dev.tudominio.com)",[3894],{"id":3484,"depth":183,"text":3485},{"id":3529,"depth":177,"text":3530},{"id":3737,"depth":177,"text":3738},{"id":3800,"depth":177,"text":3801},{"id":3847,"depth":177,"text":3848},"El pipeline completo (hooks, CI, Dokploy), los dos entornos y las reglas de migración y rollback.",{},{"icon":3902},"i-lucide-cloud-upload",{"title":935,"description":3899},"docs\u002F4.deploy","1B0n4WH2-y05D43FJFGZqeHudP8yrbguJeYaBw-ZfD4",[3907],{"title":3908,"path":3572,"stem":1682,"children":3909,"page":3914},"Docs",[3910,3911,3912,3913],{"title":6,"path":958,"stem":960,"icon":957},{"title":917,"path":916,"stem":1898,"icon":1896},{"title":924,"path":923,"stem":2980,"icon":2978},{"title":935,"path":934,"stem":3904,"icon":3902},false,1791214061719]