{"id":11164,"date":"2025-11-24T10:19:00","date_gmt":"2025-11-24T09:19:00","guid":{"rendered":"https:\/\/bitacora.eniac2000.com\/?p=11164"},"modified":"2025-11-24T12:15:42","modified_gmt":"2025-11-24T11:15:42","slug":"aplicacion-de-lista-de-compras-interactiva-en-tiempo-real","status":"publish","type":"post","link":"https:\/\/bitacora.eniac2000.com\/?p=11164","title":{"rendered":"Aplicaci\u00f3n de Lista de Compras Interactiva en Tiempo Real"},"content":{"rendered":"<h2>Introducci\u00f3n<\/h2>\n<p>La primera de las aproximaciones que realic\u00e9 a una aplicaci\u00f3n desarrollada con asistencia de IA fue una idea que llevaba a\u00f1os rond\u00e1ndome por la cabeza. Se trataba de realizar una aplicaci\u00f3n de lista de la compra compartida, que permitiera a los usuarios de la lista actualizar la informaci\u00f3n de la misma en tiempo real, de tal manera que los cambios pudieran ser vistos por la persona que estaba realizando la compra sobre la marcha.<\/p>\n<p>Hab\u00eda tenido experiencia con aplicaciones similares en el\u00a0<em>marketplace<\/em> de Android, pero, o bien su dise\u00f1o e implementaci\u00f3n eran pobres y muy descuidados desde el punto de vista de la seguridad de los datos, o bien eran engendros plagados de publicidad. Por ello, me decid\u00ed a empezar por este proyecto. Mi primera idea hab\u00eda sido realizar una aplicaci\u00f3n nativa para Android pero, en realidad, el desarrollo de una aplicaci\u00f3n web encajaba perfectamente con mi objetivo, y permit\u00eda, adem\u00e1s, la utilizaci\u00f3n en otro tipo de entornos.<\/p>\n<p>El primer prototipo de la aplicaci\u00f3n fue completamente\u00a0<em>online<\/em>, realizando tanto el desarrollo como el despliegue en Vercel. Si bien los resultados fueron enormemente positivos, una de mis principales obsesiones era no tener una dependencia con un entorno cloud (y menos a\u00fan de uno de acceso de tipo gratuito, que en cualquier momento puede desaparecer, o pasar a ser de pago), sobre todo porque en mi casa cuento con un entorno de virtualizaci\u00f3n excelente, merced al <a href=\"https:\/\/bitacora.eniac2000.com\/?p=5650\">despliegue del servidor que realic\u00e9 hace cosa de un a\u00f1o<\/a>.<\/p>\n<p>Por ello, tras validar que el concepto era viable, realic\u00e9 una evoluci\u00f3n en el proyecto para que el despliegue se realizara en un servidor de backend con Node.js.<\/p>\n<figure id=\"attachment_11165\" aria-describedby=\"caption-attachment-11165\" style=\"width: 534px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/11\/lista-compra.png\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-11165 \" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/11\/lista-compra.png\" alt=\"Aplicaci\u00f3n de lista de la compra\" width=\"534\" height=\"536\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/11\/lista-compra.png 922w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/11\/lista-compra-300x300.png 300w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/11\/lista-compra-150x150.png 150w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2025\/11\/lista-compra-768x770.png 768w\" sizes=\"auto, (max-width: 534px) 100vw, 534px\" \/><\/a><figcaption id=\"caption-attachment-11165\" class=\"wp-caption-text\">Aplicaci\u00f3n de lista de la compra<\/figcaption><\/figure>\n<p>Como parte del proceso de desarrollo, v0 permite realizar descripciones t\u00e9cnicas e integraciones de la aplicaci\u00f3n en repositorios como Github, lo que da una potencia espectacular a la hora de documentar y detallar las caracter\u00edsticas de la aplicaci\u00f3n desarrollada, aunque en el proceso no se escriba ni una l\u00ednea de c\u00f3digo por parte del desarrollador. Ojo, esto no implica que el proceso se pueda hacer sin ning\u00fan tipo de conocimiento, m\u00e1s bien al contrario: los mejores desarrollos se obtienen cuando conoces las implicaciones t\u00e9cnicas de lo que est\u00e1s pidiendo, puedes entender los resultados, y guiar a la IA para que el resultado tenga una calidad m\u00ednima, tanto en arquitectura, desarrollo, y aspectos de seguridad.<\/p>\n<p>Con estos puntos como partida, la aplicaci\u00f3n desarrollada, una <strong>Lista de Compras Colaborativa e Interactiva<\/strong>, se desarroll\u00f3 como una aplicaci\u00f3n full-stack que integra React, Next.js, TypeScript, PostgreSQL y t\u00e9cnicas avanzadas de estado compartido en tiempo real.<\/p>\n<p>Antes de continuar, quiero incluir algunos puntos clave en el dise\u00f1o de la aplicaci\u00f3n:<\/p>\n<ul>\n<li>Dado que la aplicaci\u00f3n est\u00e1 pensada para ser utilizada en mi entorno familiar, contempla que s\u00f3lo existe una lista \u00fanica de la compra, compartida por los usuarios. No contempla un sistema avanzado de listas m\u00faltiples, ya que ello exceder\u00eda el objetivo de este desarrollo, pero no ser\u00eda especialmente complicado extenderla en este sentido.<\/li>\n<li>Dado que la aplicaci\u00f3n est\u00e1 publicada en una intranet a la que s\u00f3lo se puede acceder por VPN (incluyendo en ello a los m\u00f3viles), no he incluido autenticaci\u00f3n de usuarios ni despliegue en HTTPS, en aras de la sencillez. Pero dado que existe un sistema de usuarios y la aplicaci\u00f3n es susceptible de ser publicada tras un NGINX, la capa de securizaci\u00f3n ser\u00eda bien sencilla de aplicar.<\/li>\n<li>El despliegue en el Proxmox se ha realizado mediante un contenedor. Simplemente porque ten\u00eda ganas de probarlo.<\/li>\n<li>Qued\u00e9 gratamente sorprendido de c\u00f3mo la IA, a partir de unas indicaciones b\u00e1sicas, iba m\u00e1s all\u00e1 a la hora de a\u00f1adir aspectos de valor en la implementaci\u00f3n propuesta. Sirva como ejemplo la categorizaci\u00f3n de \u00edtems de la lista de la compra: yo no hab\u00eda pedido nada al respecto, y fue algo que la IA contempl\u00f3 ya desde un primer momento. Luego tuve que refinar la implementaci\u00f3n inicial, indicando que estas categor\u00edas pod\u00edan ser editables, a\u00f1adiendo iconos, etc&#8230; Pero lo que fue el proceso inicial de incluir esta caracter\u00edstica fue algo completamente llevado a cabo por la IA.<\/li>\n<\/ul>\n<p>Y tras estas consideraciones, vayamos con la descripci\u00f3n de la aplicaci\u00f3n.<\/p>\n<hr \/>\n<h2>Tabla de Contenidos<\/h2>\n<ol>\n<li>Stack Tecnol\u00f3gico<\/li>\n<li>Arquitectura General del Sistema<\/li>\n<li>Capa de Presentaci\u00f3n (Frontend)<\/li>\n<li>Capa de L\u00f3gica de Negocio (Server Actions)<\/li>\n<li>Capa de Persistencia (Base de Datos)<\/li>\n<li>Patrones de Dise\u00f1o y Mejores Pr\u00e1cticas<\/li>\n<li>Manejo de Estados y Sincronizaci\u00f3n en Tiempo Real<\/li>\n<li>Seguridad y Optimizaci\u00f3n<\/li>\n<li>Estrategia de Despliegue<\/li>\n<li>Lecciones Aprendidas y Conclusiones<\/li>\n<\/ol>\n<hr \/>\n<h2>1. Stack Tecnol\u00f3gico<\/h2>\n<h3>Frontend<\/h3>\n<ul>\n<li><strong>React 18:<\/strong> Biblioteca principal para construcci\u00f3n de interfaces de usuario<\/li>\n<li><strong>Next.js 14:<\/strong> Framework full-stack que proporciona renderizado del lado del servidor (SSR), Server Actions y API routes<\/li>\n<li><strong>TypeScript:<\/strong> Lenguaje tipado para mayor seguridad en tiempo de compilaci\u00f3n<\/li>\n<li><strong>Tailwind CSS:<\/strong> Framework de CSS utilitario para estilos r\u00e1pidos y consistentes<\/li>\n<li><strong>shadcn\/ui:<\/strong> Componentes accesibles pre-construidos basados en Radix UI<\/li>\n<li><strong>Lucide React:<\/strong> Biblioteca de iconos SVG moderna<\/li>\n<\/ul>\n<h3>Backend<\/h3>\n<ul>\n<li><strong>Next.js Server Actions:<\/strong> Funciones del lado del servidor sin necesidad de crear rutas API expl\u00edcitas<\/li>\n<li><strong>PostgreSQL:<\/strong> Base de datos relacional robusta para persistencia de datos<\/li>\n<li><strong>Neon Database:<\/strong> Servicio PostgreSQL serverless con conexiones pooling<\/li>\n<\/ul>\n<h3>DevOps y Despliegue<\/h3>\n<ul>\n<li><strong>PM2:<\/strong> Process Manager para Node.js que mantiene la aplicaci\u00f3n en ejecuci\u00f3n<\/li>\n<li><strong>LXC\/Linux Containers:<\/strong> Contenedores ligeros para aislamiento de entornos<\/li>\n<li><strong>Bash Scripts:<\/strong> Automatizaci\u00f3n completa del despliegue<\/li>\n<li><strong>PostgreSQL 14+:<\/strong> Base de datos relacional instalada localmente<\/li>\n<\/ul>\n<hr \/>\n<h2>2. Arquitectura General del Sistema<\/h2>\n<p>La aplicaci\u00f3n sigue una <strong>arquitectura de tres capas cl\u00e1sica<\/strong> optimizada para Next.js 14:<\/p>\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502          CAPA DE PRESENTACI\u00d3N (Frontend)        \u2502\n\u2502  React Components + Tailwind CSS + shadcn\/ui   \u2502\n\u2502         app\/page.tsx + components\/               \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                 \u2502 HTTP\/JSON\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  CAPA DE L\u00d3GICA DE NEGOCIO (Next.js Server)   \u2502\n\u2502  Server Actions (lib\/actions.ts)               \u2502\n\u2502  Validaci\u00f3n, procesamiento de datos            \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                 \u2502 SQL Queries\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502   CAPA DE PERSISTENCIA (PostgreSQL Database)   \u2502\n\u2502  Tables: grocery_items, categories              \u2502\n\u2502  Indexes, Triggers, Funciones                  \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n<h3>Ventajas de esta Arquitectura<\/h3>\n<ul>\n<li><strong>Separaci\u00f3n de responsabilidades:<\/strong> Cada capa tiene un prop\u00f3sito claramente definido<\/li>\n<li><strong>Reusabilidad:<\/strong> Las Server Actions pueden invocarse desde m\u00faltiples componentes<\/li>\n<li><strong>Testabilidad:<\/strong> Facilita testing unitario de cada capa<\/li>\n<li><strong>Escalabilidad:<\/strong> Permite escalar cada capa independientemente<\/li>\n<li><strong>Seguridad:<\/strong> L\u00f3gica sensible ejecutada en el servidor<\/li>\n<\/ul>\n<hr \/>\n<h2>3. Capa de Presentaci\u00f3n (Frontend)<\/h2>\n<h3>Estructura de Componentes<\/h3>\n<p>La interfaz se construye con una jerarqu\u00eda clara de componentes:<\/p>\n<pre><code>app\/page.tsx (Componente Principal)\n\u251c\u2500\u2500 Header (Logo, estado de conexi\u00f3n, usuarios en l\u00ednea)\n\u251c\u2500\u2500 AddItemForm (Formulario para agregar productos)\n\u251c\u2500\u2500 ActiveItemsList (Lista de productos pendientes)\n\u2502   \u2514\u2500\u2500 ItemCard (Componente individual)\n\u2502       \u251c\u2500\u2500 CheckButton\n\u2502       \u251c\u2500\u2500 ItemInfo\n\u2502       \u2514\u2500\u2500 ActionButtons (Editar, Eliminar)\n\u251c\u2500\u2500 CompletedItemsList (Lista de productos completados)\n\u2502   \u2514\u2500\u2500 CompletedItemCard\n\u2514\u2500\u2500 EmptyState (Cuando no hay productos)\n<\/code><\/pre>\n<h3>Gesti\u00f3n de Estado Local<\/h3>\n<p>El componente principal utiliza <strong>hooks de React<\/strong> para gestionar estado:<\/p>\n<pre><code class=\"language-typescript\">\nconst [items, setItems] = useState&lt;GroceryItem[]&gt;([])\nconst [categories, setCategories] = useState&lt;Category[]&gt;([])\nconst [editingItem, setEditingItem] = useState(null)\nconst [isLoading, setIsLoading] = useState(false)\nconst [isConnected, setIsConnected] = useState(true)\nconst [lastUpdated, setLastUpdated] = useState(new Date())\n<\/code><\/pre>\n<h3>Sincronizaci\u00f3n con el Servidor<\/h3>\n<p>Se implementa un mecanismo de <strong>polling peri\u00f3dico<\/strong> cada 5 segundos:<\/p>\n<pre><code class=\"language-typescript\">\nuseEffect(() =&gt; {\n    loadData()\n    const interval = setInterval(loadData, 5000)\n    return () =&gt; clearInterval(interval)\n}, [])\n\nconst loadData = async () =&gt; {\n    try {\n        setIsLoading(true)\n        const data = await getGroceryItems()\n        setItems(data.items)\n        setCategories(data.categories)\n        setLastUpdated(new Date(data.lastUpdated))\n        setIsConnected(true)\n    } catch (error) {\n        setIsConnected(false)\n    } finally {\n        setIsLoading(false)\n    }\n}\n<\/code><\/pre>\n<h3>Componentes de UI Reutilizables<\/h3>\n<p>La aplicaci\u00f3n utiliza componentes de <strong>shadcn\/ui<\/strong> como base:<\/p>\n<ul>\n<li><strong>Button:<\/strong> Variantes: default, destructive, outline, secondary, ghost<\/li>\n<li><strong>Card:<\/strong> Contenedor principal para secciones<\/li>\n<li><strong>Input:<\/strong> Campos de entrada de texto y n\u00fameros<\/li>\n<li><strong>Badge:<\/strong> Etiquetas para categor\u00edas y cantidades<\/li>\n<li><strong>Icons (Lucide):<\/strong> Plus, Trash2, Edit2, Check, RotateCcw, etc.<\/li>\n<\/ul>\n<hr \/>\n<h2>4. Capa de L\u00f3gica de Negocio (Server Actions)<\/h2>\n<h3>\u00bfQu\u00e9 son las Server Actions?<\/h3>\n<p>Las Server Actions son funciones TypeScript que se ejecutan en el servidor, permitiendo una comunicaci\u00f3n cliente-servidor fluida sin crear rutas API expl\u00edcitas. Se declaran con la directiva <code>\"use server\"<\/code>.<\/p>\n<h3>Operaciones CRUD Implementadas<\/h3>\n<p><strong>1. Obtener Productos (getGroceryItems)<\/strong><\/p>\n<pre><code class=\"language-typescript\">\nexport async function getGroceryItems(): Promise {\n    try {\n        if (sql &amp;&amp; (await testConnection())) {\n            const items = await sql`\n                SELECT id, name, category, completed, quantity, \n                       edited_by, edited_at, created_at, updated_at\n                FROM grocery_items \n                ORDER BY created_at DESC\n            `\n            return { items, categories, lastUpdated: Date.now() }\n        }\n    } catch (error) {\n        return { items: memoryItems, categories: memoryCategories }\n    }\n}\n<\/code><\/pre>\n<p><strong>2. Agregar Producto (addGroceryItem)<\/strong><\/p>\n<pre><code class=\"language-typescript\">\nexport async function addGroceryItem(\n    item: Omit&lt;GroceryItem, \"id\" | \"created_at\" | \"updated_at\"&gt;\n): Promise {\n    const newItem = {\n        ...item,\n        id: generateId(),\n        created_at: Date.now(),\n        updated_at: Date.now()\n    }\n    \n    await sql`\n        INSERT INTO grocery_items (...)\n        VALUES (${newItem.id}, ${newItem.name}, ...)\n    `\n    return await getGroceryItems()\n}\n<\/code><\/pre>\n<p><strong>3. Actualizar Producto (updateGroceryItem)<\/strong><\/p>\n<pre><code class=\"language-typescript\">\nexport async function updateGroceryItem(\n    id: string,\n    updates: Partial,\n    editedBy?: string\n): Promise {\n    await sql`\n        UPDATE grocery_items \n        SET ${updateData}, updated_at = ${Date.now()}\n        WHERE id = ${id}\n    `\n    return await getGroceryItems()\n}\n<\/code><\/pre>\n<p><strong>4. Eliminar Producto (deleteGroceryItem)<\/strong><\/p>\n<pre><code class=\"language-typescript\">\nexport async function deleteGroceryItem(id: string): Promise {\n    await sql`DELETE FROM grocery_items WHERE id = ${id}`\n    return await getGroceryItems()\n}\n<\/code><\/pre>\n<p><strong>5. Operaciones Masivas<\/strong><\/p>\n<pre><code class=\"language-typescript\">\nexport async function clearCompletedItems(): Promise {\n    await sql`DELETE FROM grocery_items WHERE completed = true`\n    return await getGroceryItems()\n}\n\nexport async function reactivateCompletedItems(): Promise {\n    await sql`\n        UPDATE grocery_items \n        SET completed = false, updated_at = ${Date.now()}\n        WHERE completed = true\n    `\n    return await getGroceryItems()\n}\n<\/code><\/pre>\n<h3>Manejo de Errores y Fallback<\/h3>\n<p>La aplicaci\u00f3n implementa un <strong>patr\u00f3n de fallback en memoria<\/strong>:<\/p>\n<pre><code class=\"language-typescript\">\n\/\/ En memoria como respaldo\nlet memoryItems: GroceryItem[] = []\nconst memoryCategories: Category[] = [...]\n\n\/\/ Intenta usar DB, fallback a memoria si falla\nif (sql &amp;&amp; (await testConnection())) {\n    \/\/ Usar PostgreSQL\n} else {\n    \/\/ Usar almacenamiento en memoria\n    return { items: memoryItems, categories: memoryCategories }\n}\n<\/code><\/pre>\n<hr \/>\n<h2>5. Capa de Persistencia (Base de Datos)<\/h2>\n<h3>Esquema de la Base de Datos<\/h3>\n<p><strong>Tabla: grocery_items<\/strong><\/p>\n<pre><code class=\"language-sql\">\nCREATE TABLE grocery_items (\n    id TEXT PRIMARY KEY,\n    name TEXT NOT NULL,\n    category TEXT NOT NULL DEFAULT 'Otros',\n    completed BOOLEAN NOT NULL DEFAULT FALSE,\n    quantity TEXT,\n    edited_by TEXT,\n    edited_at BIGINT,\n    created_at BIGINT NOT NULL,\n    updated_at BIGINT NOT NULL\n)\n\nCREATE INDEX idx_grocery_items_completed ON grocery_items(completed)\nCREATE INDEX idx_grocery_items_category ON grocery_items(category)\nCREATE INDEX idx_grocery_items_updated_at ON grocery_items(updated_at)\n<\/code><\/pre>\n<p><strong>Tabla: categories<\/strong><\/p>\n<pre><code class=\"language-sql\">\nCREATE TABLE categories (\n    id SERIAL PRIMARY KEY,\n    name VARCHAR(100) NOT NULL UNIQUE,\n    icon VARCHAR(10) DEFAULT '\ud83d\udce6',\n    color VARCHAR(7) DEFAULT '#6B7280',\n    created_at BIGINT,\n    updated_at BIGINT\n)\n\nINSERT INTO categories (name, icon, color) VALUES\n    ('Frutas y Verduras', '\ud83e\udd55', '#10B981'),\n    ('L\u00e1cteos', '\ud83e\udd5b', '#3B82F6'),\n    ('Carne y Pescado', '\ud83e\udd69', '#EF4444'),\n    ('Panader\u00eda', '\ud83c\udf5e', '#F59E0B'),\n    ('Despensa', '\ud83e\udd6b', '#8B5CF6'),\n    ('Otros', '\ud83d\udce6', '#6B7280')\n<\/code><\/pre>\n<h3>Optimizaciones de Base de Datos<\/h3>\n<p><strong>1. \u00cdndices para Rendimiento<\/strong><\/p>\n<ul>\n<li><code>idx_grocery_items_completed<\/code> &#8211; Acelera consultas de items completados vs. pendientes<\/li>\n<li><code>idx_grocery_items_category<\/code> &#8211; Optimiza filtros por categor\u00eda<\/li>\n<li><code>idx_grocery_items_updated_at<\/code> &#8211; Facilita obtener items m\u00e1s recientes<\/li>\n<\/ul>\n<p><strong>2. Triggers para Auditor\u00eda<\/strong><\/p>\n<pre><code class=\"language-sql\">\nCREATE OR REPLACE FUNCTION update_updated_at_column()\nRETURNS TRIGGER AS $\nBEGIN\n    NEW.updated_at = EXTRACT(EPOCH FROM NOW()) * 1000;\n    RETURN NEW;\nEND;\n$ language 'plpgsql';\n\nCREATE TRIGGER update_grocery_items_updated_at\n    BEFORE UPDATE ON grocery_items\n    FOR EACH ROW\n    EXECUTE FUNCTION update_updated_at_column();\n<\/code><\/pre>\n<p>Este trigger autom\u00e1ticamente actualiza <code>updated_at<\/code> cada vez que se modifica un registro, sin necesidad de gesti\u00f3n manual.<\/p>\n<h3>Seguridad: Row Level Security (RLS)<\/h3>\n<p>Para aplicaciones con m\u00faltiples usuarios, se pueden implementar pol\u00edticas RLS:<\/p>\n<pre><code class=\"language-sql\">\nALTER TABLE grocery_items ENABLE ROW LEVEL SECURITY;\n\nCREATE POLICY \"Users can only see their own items\" ON grocery_items\n    FOR SELECT\n    USING (auth.uid() = user_id OR is_public)\n<\/code><\/pre>\n<hr \/>\n<h2>6. Patrones de Dise\u00f1o y Mejores Pr\u00e1cticas<\/h2>\n<h3>Patr\u00f3n: Servidor de Acciones (Server Actions Pattern)<\/h3>\n<p>Las Server Actions de Next.js 14 representan la evoluci\u00f3n del patr\u00f3n MVC tradicional:<\/p>\n<pre><code>\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  React Form  \u2502 handleSubmit()\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n       \u2502 await serverAction(data)\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Server Action (Node.js)\u2502 Validaci\u00f3n, L\u00f3gica\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n       \u2502 SQL Query\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  PostgreSQL Database \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n<h3>Identificaci\u00f3n \u00danica de Registros<\/h3>\n<p>Se utiliza una combinaci\u00f3n de timestamp + random para generar IDs \u00fanicos:<\/p>\n<pre><code class=\"language-typescript\">\nfunction generateId(): string {\n    return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`\n}\n\/\/ Ejemplo: 1234567890123-a1b2c3d4e\n<\/code><\/pre>\n<h3>Serializaci\u00f3n de Timestamps<\/h3>\n<p>Los timestamps se almacenan como milisegundos desde la \u00e9poca Unix:<\/p>\n<pre><code class=\"language-typescript\">\nconst now = Date.now() \/\/ milisegundos\n\/\/ Almacenado como: EXTRACT(EPOCH FROM NOW()) * 1000\n\n\/\/ Al recuperar:\nnew Date(item.created_at).toLocaleTimeString()\n<\/code><\/pre>\n<h3>Validaci\u00f3n de Entrada<\/h3>\n<pre><code class=\"language-typescript\">\nif (!newItemName.trim()) return \/\/ Evita items vac\u00edos\nif (!editName.trim()) return   \/\/ Valida antes de actualizar\n\n\/\/ Trimming de whitespace\nname: newItemName.trim()\n<\/code><\/pre>\n<hr \/>\n<h2>7. Manejo de Estados y Sincronizaci\u00f3n en Tiempo Real<\/h2>\n<h3>Polling vs. WebSockets<\/h3>\n<p>La aplicaci\u00f3n utiliza <strong>polling<\/strong> (solicitud peri\u00f3dica) cada 5 segundos:<\/p>\n<table border=\"1\" cellpadding=\"10\">\n<tbody>\n<tr>\n<th>Aspecto<\/th>\n<th>Polling (Actual)<\/th>\n<th>WebSockets<\/th>\n<\/tr>\n<tr>\n<td>Latencia<\/td>\n<td>Hasta 5 segundos<\/td>\n<td>Instant\u00e1neo (&lt;100ms)<\/td>\n<\/tr>\n<tr>\n<td>Consumo de Ancho de Banda<\/td>\n<td>Medio (cada 5s)<\/td>\n<td>Alto (conexi\u00f3n persistente)<\/td>\n<\/tr>\n<tr>\n<td>Escalabilidad<\/td>\n<td>Alta (HTTP stateless)<\/td>\n<td>Media (conexiones persistentes)<\/td>\n<\/tr>\n<tr>\n<td>Complejidad<\/td>\n<td>Baja<\/td>\n<td>Alta<\/td>\n<\/tr>\n<tr>\n<td>Mejor para<\/td>\n<td>Aplicaciones CRUD cl\u00e1sicas<\/td>\n<td>Chat, Games, Colaboraci\u00f3n en tiempo real<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Indicadores de Conectividad<\/h3>\n<p>La UI proporciona retroalimentaci\u00f3n visual del estado:<\/p>\n<pre><code class=\"language-jsx\">\n{\/* Indicador de conexi\u00f3n *\/}<\/code><\/pre>\n<div><\/div>\n<pre><code class=\"language-jsx\">\n\n{\/* Indicador de carga *\/}\n{isLoading &amp;&amp;<\/code><\/pre>\n<div><\/div>\n<pre><code class=\"language-jsx\">}\n<\/code><\/pre>\n<h3>Estrategia de Reintento<\/h3>\n<p>Para operaciones que fallan, se implementan reintentos autom\u00e1ticos:<\/p>\n<pre><code class=\"language-typescript\">\nasync function testConnection() {\n    if (!sql) return false\n    try {\n        await sql`SELECT 1 as test`\n        return true\n    } catch (error) {\n        console.error(\"Connection test failed:\", error)\n        return false\n    }\n}\n\n\/\/ Utilizado en cada operaci\u00f3n\nif (sql &amp;&amp; (await testConnection())) {\n    \/\/ Usar DB\n} else {\n    \/\/ Fallback\n}\n<\/code><\/pre>\n<hr \/>\n<h2>8. Seguridad y Optimizaci\u00f3n<\/h2>\n<h3>Seguridad en Server Actions<\/h3>\n<p><strong>1. Ejecuci\u00f3n en el Servidor<\/strong><\/p>\n<ul>\n<li>Las credenciales de base de datos NUNCA se exponen al cliente<\/li>\n<li>La l\u00f3gica sensible se ejecuta en el servidor<\/li>\n<li>Las consultas SQL se parametrizan autom\u00e1ticamente<\/li>\n<\/ul>\n<p><strong>2. Validaci\u00f3n en Servidor<\/strong><\/p>\n<pre><code class=\"language-typescript\">\n\/\/ El servidor SIEMPRE valida\nif (!newItemName.trim()) {\n    throw new Error(\"Item name cannot be empty\")\n}\n\n\/\/ No confiar en validaci\u00f3n del cliente\n<\/code><\/pre>\n<p><strong>3. Protecci\u00f3n contra SQL Injection<\/strong><\/p>\n<p>La librer\u00eda <code>@neondatabase\/serverless<\/code> proporciona parametrizaci\u00f3n autom\u00e1tica:<\/p>\n<pre><code class=\"language-typescript\">\n\/\/ SEGURO: Parametrizado autom\u00e1ticamente\nawait sql`SELECT * FROM grocery_items WHERE name = ${userInput}`\n\n\/\/ INSEGURO: Nunca hacer esto\nawait sql(`SELECT * FROM grocery_items WHERE name = '${userInput}'`)\n<\/code><\/pre>\n<h3>Optimizaci\u00f3n de Rendimiento<\/h3>\n<p><strong>1. Caching de Componentes<\/strong><\/p>\n<pre><code class=\"language-typescript\">\n\/\/ Componente cliente\n\"use client\"\n\n\/\/ Componente servidor (cached por defecto)\n\/\/ No llevar la directiva \"use client\"\n<\/code><\/pre>\n<p><strong>2. Lazy Loading de Componentes<\/strong><\/p>\n<pre><code class=\"language-typescript\">\nimport dynamic from 'next\/dynamic'\n\nconst AdminPanel = dynamic(() =&gt; import('@\/components\/admin-panel'), {\n    loading: () =&gt;<\/code><\/pre>\n<p>Cargando&#8230;<\/p>\n<pre><code class=\"language-typescript\">,\n})\n<\/code><\/pre>\n<p><strong>3. Queries Optimizadas<\/strong><\/p>\n<pre><code class=\"language-sql\">\n-- \u00d3PTIMO: Solo columnas necesarias\nSELECT id, name, category, completed FROM grocery_items\n\n-- SUB-\u00d3PTIMO: Todas las columnas\nSELECT * FROM grocery_items\n<\/code><\/pre>\n<p><strong>4. Paginaci\u00f3n para Listas Grandes<\/strong><\/p>\n<p>Para aplicaciones con miles de items, implementar paginaci\u00f3n:<\/p>\n<pre><code class=\"language-typescript\">\nexport async function getGroceryItems(page = 1, limit = 50) {\n    const offset = (page - 1) * limit\n    const items = await sql`\n        SELECT * FROM grocery_items\n        ORDER BY created_at DESC\n        LIMIT ${limit} OFFSET ${offset}\n    `\n    return items\n}\n<\/code><\/pre>\n<h3>Monitoreo y Logging<\/h3>\n<p>La aplicaci\u00f3n registra todas las operaciones para debugging:<\/p>\n<pre><code class=\"language-typescript\">\nconsole.log(\"\ud83d\udcca Fetching items from database...\")\nconsole.log(`\u2705 Fetched ${items.length} items from database`)\nconsole.log(\"\u274c Error fetching items:\", error)\n<\/code><\/pre>\n<hr \/>\n<h2>9. Estrategia de Despliegue<\/h2>\n<h3>Pipeline de Despliegue Automatizado<\/h3>\n<p>La aplicaci\u00f3n se despliega a trav\u00e9s de una serie de scripts bash en LXC containers:<\/p>\n<pre><code>\n01-create-lxc-container.sh     \u2192 Crea el contenedor Linux\n    \u2193\n02-setup-environment.sh         \u2192 Instala dependencias\n    \u2193\n03-deploy-application.sh        \u2192 Despliega la app con PM2\n    \u2193\n04-verify-installation.sh       \u2192 Verifica que todo funciona\n<\/code><\/pre>\n<h3>Componentes del Despliegue<\/h3>\n<p><strong>1. LXC Container<\/strong><\/p>\n<pre><code class=\"language-bash\">\n# Crear container Ubuntu 20.04\nlxc create ubuntu:20.04 grocery-app\nlxc start grocery-app\nlxc exec grocery-app -- bash\n<\/code><\/pre>\n<p><strong>2. Base de Datos PostgreSQL<\/strong><\/p>\n<pre><code class=\"language-bash\">\n# Instalar PostgreSQL en el container\napt-get install postgresql postgresql-contrib\n\n# Crear usuario y base de datos\nsudo -u postgres psql &lt;&lt; 'EOF'\nCREATE USER grocery WITH PASSWORD '********'\nCREATE DATABASE grocery_list OWNER grocery\nGRANT ALL PRIVILEGES ON DATABASE grocery_list TO grocery\nEOF\n<\/code><\/pre>\n<p><strong>3. Aplicaci\u00f3n Next.js<\/strong><\/p>\n<pre><code class=\"language-bash\">\n# Instalar dependencias\nnpm install\n\n# Compilar\nnpm run build\n\n# Iniciar con PM2\npm2 start \"npm start\" --name \"grocery-app\"\npm2 startup\npm2 save\n<\/code><\/pre>\n<h3>Environment Variables<\/h3>\n<pre><code>\nDATABASE_URL=postgresql:\/\/grocery:*******@localhost:5432\/grocery_list\nNEXT_PUBLIC_APP_URL=http:\/\/localhost:3000\nNODE_ENV=production\n<\/code><\/pre>\n<hr \/>\n<h2>10. Lecciones Aprendidas y Conclusiones<\/h2>\n<h3>Ventajas de esta Arquitectura<\/h3>\n<ul>\n<li><strong>Desarrollo R\u00e1pido:<\/strong> Server Actions eliminan la necesidad de crear API routes<\/li>\n<li><strong>TypeScript end-to-end:<\/strong> Tipado en cliente y servidor<\/li>\n<li><strong>Renderizado Eficiente:<\/strong> Next.js maneja SSR autom\u00e1ticamente<\/li>\n<li><strong>Seguridad Mejorada:<\/strong> Secrets del servidor nunca se exponen<\/li>\n<li><strong>Experiencia de Usuario Fluida:<\/strong> Interactividad inmediata con polling<\/li>\n<li><strong>Escalabilidad:<\/strong> F\u00e1cil de expandir con nuevas caracter\u00edsticas<\/li>\n<\/ul>\n<h3>Mejoras Futuras<\/h3>\n<p><strong>1. WebSockets en Tiempo Real<\/strong><\/p>\n<pre><code class=\"language-typescript\">\n\/\/ Migrar a Socket.IO o ws para colaboraci\u00f3n en tiempo real\nio.on('connection', (socket) =&gt; {\n    socket.on('item-added', (item) =&gt; {\n        io.emit('items-updated', getAllItems())\n    })\n})\n<\/code><\/pre>\n<p><strong>2. Autenticaci\u00f3n de Usuarios<\/strong><\/p>\n<pre><code class=\"language-typescript\">\n\/\/ Agregar NextAuth.js o Clerk\nimport { getServerSession } from \"next-auth\"\n\nexport async function addGroceryItem(item) {\n    const session = await getServerSession()\n    if (!session) throw new Error(\"Unauthorized\")\n    \/\/ ...\n}\n<\/code><\/pre>\n<p><strong>3. Persistencia de Estado Local (Offline-First)**<\/strong><\/p>\n<pre><code class=\"language-typescript\">\n\/\/ Usar IndexedDB para funcionamiento offline\nimport { openDB } from 'idb'\n\nconst db = await openDB('grocery-list')\ndb.put('items', item)\n<\/code><\/pre>\n<p><strong>4. Testing Automatizado<\/strong><\/p>\n<pre><code class=\"language-typescript\">\n\/\/ Jest + React Testing Library\ndescribe('GroceryListApp', () =&gt; {\n    test('should add item when form is submitted', async () =&gt; {\n        const { getByPlaceholderText, getByRole } = render()\n        \/\/ ...\n    })\n})\n<\/code><\/pre>\n<p><strong>5. Monitoreo y Observabilidad**<\/strong><\/p>\n<ul>\n<li>Integraci\u00f3n con Sentry para error tracking<\/li>\n<li>Prometheus para m\u00e9tricas de rendimiento<\/li>\n<li>ELK Stack para centralizaci\u00f3n de logs<\/li>\n<\/ul>\n<h3>Lecciones Clave<\/h3>\n<ol>\n<li><strong>Elige el nivel de complejidad adecuado:<\/strong> Polling es suficiente para muchas aplicaciones; no todas necesitan WebSockets<\/li>\n<li><strong>Prioriza la seguridad:<\/strong> Nunca expongas credenciales, valida siempre en el servidor<\/li>\n<li><strong>Documenta tu arquitectura:<\/strong> Los futuros mantenedores lo agradecer\u00e1n<\/li>\n<li><strong>Optimiza para el usuario final:<\/strong> UX suave es m\u00e1s importante que arquitectura perfecta<\/li>\n<li><strong>Automatiza todo:<\/strong> Despliegue, testing, monitoring &#8211; es inversi\u00f3n que se paga<\/li>\n<li><strong>Monitorea en producci\u00f3n:<\/strong> Los problemas reales aparecen con usuarios reales<\/li>\n<\/ol>\n<hr \/>\n<h2>Conclusi\u00f3n<\/h2>\n<p>La arquitectura de la Lista de Compras Colaborativa demuestra c\u00f3mo Next.js 14, React 18 y PostgreSQL pueden combinarse para crear aplicaciones web modernas, escalables y seguras. Los patrones presentados\u2014Server Actions, gesti\u00f3n de estado con hooks, indexaci\u00f3n de base de datos y despliegue automatizado\u2014son transferibles a muchos otros proyectos.<\/p>\n<p>El c\u00f3digo est\u00e1 optimizado para el balance entre simplicidad y robustez, permitiendo tanto desarrolladores principiantes como expertos entender y mantener el sistema. Con las mejoras futuras sugeridas, esta aplicaci\u00f3n podr\u00eda escalar a manejar miles de usuarios concurrentes.<\/p>\n<p><strong>Stack recomendado para aplicaciones similares en 2025:<\/strong><\/p>\n<ul>\n<li>Next.js 14+ con App Router<\/li>\n<li>TypeScript para safety<\/li>\n<li>PostgreSQL o Neon para persistencia<\/li>\n<li>Tailwind + shadcn\/ui para UI<\/li>\n<li>Vercel, Railway o Render para hosting<\/li>\n<li>PM2 u otro orchestrator para production<\/li>\n<\/ul>\n<hr \/>\n<h2>Recursos Adicionales<\/h2>\n<ul>\n<li><a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/data-fetching\/server-actions\">Next.js Server Actions Documentation<\/a><\/li>\n<li><a href=\"https:\/\/www.postgresql.org\/docs\/\">PostgreSQL Official Documentation<\/a><\/li>\n<li><a href=\"https:\/\/ui.shadcn.com\/\">shadcn\/ui Components<\/a><\/li>\n<li><a href=\"https:\/\/pm2.keymetrics.io\/\">PM2 Documentation<\/a><\/li>\n<li><a href=\"https:\/\/neon.tech\/\">Neon Database<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Introducci\u00f3n La primera de las aproximaciones que realic\u00e9 a una<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"advanced_seo_description":"","jetpack_seo_html_title":"","jetpack_seo_noindex":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[1845,13],"tags":[106,1851,824,1178,1310,1846],"series":[],"class_list":["post-11164","post","type-post","status-publish","format-standard","hentry","category-generado-con-ia","category-informatica","tag-android","tag-desarrollo","tag-ia","tag-node-js","tag-proxmox","tag-v0"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11164","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11164"}],"version-history":[{"count":3,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11164\/revisions"}],"predecessor-version":[{"id":11168,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11164\/revisions\/11168"}],"wp:attachment":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11164"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11164"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11164"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=11164"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}