World Vibe Web
The distributed app store for vibe-coded projects. Browse, discover, and install open-source apps from across GitHub.
翻译准备中
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
POS para tiendas de abarrotes y comercios de barrio. Multi-tenant, caja, catálogo e impresión de tickets. Stack: Vue.js (frontend), Express.js (backend) y MongoDB. • Frontend: http://localhost:5173 • API: http://localhost:8081 Copia backend/.env.example → backend/.env. Flujo de la tienda 1. Vender (/pos) — buscador/escáner + categorías → ticket → Cobrar (ver abajo) 2. Caja (/orders) — abrir turno, cobrar, imprimir ticket, corte 3. Productos (/products) — categorías y artículos (solo admin) 4. Más — resumen, empleados, facturación SaaS, configuración查看完整 README ↗
POS para tiendas de abarrotes y comercios de barrio. Multi-tenant, caja, catálogo e impresión de tickets.
Stack: Vue.js (frontend), Express.js (backend) y MongoDB.
npm install
npm run dev
Copia backend/.env.example → backend/.env.
/pos) — buscador/escáner + categorías → ticket → Cobrar (ver abajo)/orders) — abrir turno, cobrar, imprimir ticket, corte/products) — categorías y artículos (solo admin)Rutas antiguas de restaurante (/main, /kitchen, /waitlist) redirigen al POS.
Pensada para abarrotes, farmacia y ferretería. A la izquierda el buscador y el catálogo (mosaicos por categoría con existencias); a la derecha el ticket con totales y Cobrar. En el celular el ticket se abre desde la barra inferior.
para 500 encuentra Paracetamol 500 mg). ↑ ↓ eligen y Enter agrega.3* antes del código agrega 3 piezas; F3 (o tocar la cantidad) acepta decimales para kilos o metros; + / − suman o quitan una pieza.Cuando se publica una versión nueva, la app la descarga sola en segundo plano y muestra arriba el aviso «Hay una nueva versión… Actualizar ahora»; no se aplica hasta que la persona lo decide, para no cortar una venta a medias. Busca versiones nuevas al volver a la pestaña, al recuperar internet y cada 30 minutos (así también se entera la computadora de la caja que se deja abierta todo el día). En Configuración → Ayuda se ve la versión instalada y hay un botón «Buscar actualizaciones». Código en frontend/src/appUpdate.js; el service worker se registra en modo prompt (vite.config.js).
La caja sigue cobrando sin conexión: las ventas se guardan en el dispositivo y se suben solas al volver la red, sin duplicarse. Las que el servidor rechace quedan en Caja → «Ventas de este dispositivo sin subir». Cómo funciona y cómo probarlo: docs/modo-sin-internet.md.
En Configuración → Impresora cada caja elige cómo sale el ticket (se guarda en ese dispositivo):
| Modo | Para qué | Navegador |
|---|---|---|
| Navegador (actual) | Cualquier impresora, con el diálogo de imprimir | Todos |
| Térmica USB | ESC/POS por WebUSB, sin diálogo | Chrome/Edge en PC; Chrome Android con cable OTG |
| Térmica USB (puerto COM) | Impresoras que Windows instala como puerto COM (Web Serial) | Chrome/Edge en PC |
| Térmica Bluetooth | Impresoras Bluetooth LE (Web Bluetooth) | Chrome en Android y PC |
frontend/src/escpos.js; conexión en frontend/src/thermalPrinter.js.Por omisión cada dispositivo imprime lo que cobra. Para que el celular mande el ticket a la impresora de la computadora de la caja:
Al cobrar, el celular manda el ticket y espera hasta 10 s la confirmación; si la computadora no está conectada o no pudo imprimir, avisa con «Reintentar» e «Imprimir con el navegador». Detalles: la computadora recibe los tickets con una petición larga (POST /print-jobs/claim), un ticket lo imprime una sola estación, y uno que espera más de 10 minutos ya no se imprime. Ventas hechas sin internet y reimpresiones desde Caja siguen imprimiendo en el dispositivo. Lógica en backend/services/print-queue.service.js y frontend/src/printStation.js.
En Configuración → Báscula se conecta la báscula de la caja (Web Serial: Chrome/Edge en PC). Se elige cómo manda el peso (continuo, o pedirlo con «P», «W» o ENQ), la velocidad del puerto (9600 casi siempre) y en qué unidad viene si no la dice. Ahí mismo se ve el peso en vivo para probar. Lectura en frontend/src/scale.js; la regla de los 2 segundos en frontend/src/bulk.js (createStableWeigh).
Modelos probados (ir llenando al probar en tienda; formato: modelo · conexión · sistema y navegador · resultado):
| Modelo | Conexión | Sistema / navegador | Resultado |
|---|---|---|---|
| pendiente | USB ESC/POS genérica 80 mm | Windows · Chrome | por probar |
| pendiente | Bluetooth 80 mm | Android · Chrome | por probar |
Cada tienda recibe 181 productos de abarrotes, frescos y conveniencia (Tijuana) ya cargados, sin precio: nombre, presentación, código, unidad de venta y categoría por proveedor (Sabritas, Bimbo, Coca-Cola…). Reemplaza al catálogo de ejemplo.
needsPrice: true: no sale en los mosaicos de la caja (sí al buscar o escanear), no cuenta para el tope de productos del plan ni para las alertas de stock bajo, y el servidor rechaza venderlo (409). El marcador se apaga solo en cuanto recibe un precio mayor a 0 (utils/needs-price.js). En Productos hay un filtro «Sin precio».other; ahí caen las carnicerías, que no tienen giro propio). Se carga al terminar el asistente de configuración y al arrancar el servidor en las tiendas que ya existían, una sola vez por tienda: lo que el dueño borre no vuelve. Ferretería, farmacia, restaurante, café, bar y hotel no lo reciben; si una tienda cambia a uno de esos giros, se quita lo que siga sin precio y lo que ya tiene precio se conserva.3001, no PLU-3001).backend/data/catalogo-maestro.json; para actualizarlos se edita ese archivo y se publica (si hay que volver a cargarlo en todas las tiendas, se sube VERSION en services/master-catalog.service.js). La lista de giros está ahí y en frontend/src/masterCatalog.js (una prueba comprueba que coincidan).Venta a crédito por cliente. En Clientes el administrador le da un límite de fiado (0 = no se le fía); en la caja, Fiado pide el cliente y un anticipo opcional en efectivo. El saldo se mueve con operaciones atómicas y cada cargo/abono queda en un libro (credit_entries): el tope se valida en la base, así dos cajas no pueden rebasarlo, y reenviar una venta no fía dos veces. Abonos (POST /customers/:id/payments) entran a la caja abierta y suman al efectivo esperado del corte; lo fiado no cuenta como dinero en caja. Anular una venta fiada cancela lo que quedó a deber y devuelve solo el anticipo. Por cobrar lista quién debe, cuánto y desde cuándo, con recordatorio por WhatsApp. Solo con internet. Lógica en services/credit.service.js.
En Inventario → Conteo: se cuenta lo que hay en el estante (todo o una categoría) sin ver lo que dice el sistema; al terminar se revisan las diferencias (faltantes y sobrantes, al costo) y el administrador decide aplicarlas. La diferencia se mide contra lo que decía el sistema en el momento de contar cada producto, así las ventas hechas durante el conteo no se pierden ni se restan dos veces. Aplicar es atómico, ajusta los lotes de productos con caducidad y deja cada ajuste en la bitácora. Lógica en services/stocktake.service.js.
Varias tiendas bajo un dueño, con cambio de sucursal sin volver a iniciar sesión y reporte consolidado. Solo en plan Pro (hasta 5). Ver docs/SUCURSALES.md.
Al subir el logo (en el asistente de configuración o en Ajustes → Mi negocio), la app saca la paleta de la propia imagen, muestra unos segundos el aviso «Preparando tu experiencia» y aplica los colores a toda la app. Se guardan en primaryColor y accentColor de los ajustes de la tienda.
frontend/src/brandColors.js), así que la misma imagen da siempre la misma paleta. Ignora el fondo blanco y la transparencia.#timber-brand con las variables --timber-primary, --timber-accent, --timber-brand…#1E5AA8 y #E08A1E, o el verde viejo que guardaba el servidor) no se inyecta nada: se ve igual que siempre. «Usar el de Mi Tiendita» en Ajustes los restablece. Solo funciona con imágenes subidas desde el equipo (no con una URL de otro sitio, que el navegador no deja leer).Tras registrarte, el wizard pide nombre de tienda, tipo (abarrotes / conveniencia / farmacia / ferretería) y logo. Si el giro usa el catálogo maestro, la tienda queda con sus 181 productos cargados (ver arriba). Términos: /terminos · Privacidad: /privacidad.
SaaS 100% nube (sin instalar). Prueba 3 días. Básico $349 · Crecimiento $750 · Pro $1,350 /mes (anual: 10 meses: $3,490 · $7,500 · $13,500). Incluyen Inventario Mágico y Precio Mágico (50 / 150 / 500 usos al mes). Promoción de lanzamiento (clientes nuevos, aparte de los 3 días): los primeros 3 cobros mensuales a 1/3 del precio ($116 · $250 · $450) y después precio normal; solo para tiendas creadas después de activarla y solo en plan mensual. Al terminar el 3er cobro se sube el monto de la suscripción en Mercado Pago (PUT /preapproval); si ese aviso no llega, se sube solo unos días después de la fecha del 3er cobro (backend/services/promo.service.js). La prueba siempre se respeta: si contratas durante los 3 días, la suscripción se crea con free_trial por los días que quedan y el primer cobro (ya con la promoción) es al terminar la prueba. En /billing se paga, se ve el historial y se cancela la renovación (sigues activo hasta el fin del periodo).
Límites: Básico 2 usuarios / 250 productos · Crecimiento 6 / 1,500 · Pro 20 / ilimitados.
Hay dos perfiles. El seed crea el admin:
cd backend && npm run seed:platform-admin
platform / Platform123! → /platform. Números, licencias (incluida Perpetua), gastos y Equipo./platform → Finanzas, solo los 3 socios, es decir los admins permanentes de backend/configurations/platform-owners.js; ni el soporte ni otros admins ven la pestaña, y el servidor lo exige): lista los cobros reales del mes (de Mercado Pago o registrados a mano, sin repetirse), lo comparan con el estimado por planes y muestran la parte de cada socio en partes iguales —del ingreso cobrado y de la utilidad después de IA y gastos—. Se puede descargar en CSV. El número de socios es el de admins permanentes; no descuenta comisiones de vendedores ni de Mercado Pago.middleware/refuse-branch.js); todo eso es de la matriz./platform/equipo. Solo ve clientes y responde correo.HttpOnly (mt_rt, 30 días, rota en cada uso). El front lo renueva solo cuando el access vence. Si alguien reusa un refresh ya rotado, se cierran todas las sesiones de esa persona. Cambiar o restablecer la contraseña cierra las demás sesiones al instante.backend/utils/password-policy.js = frontend/src/passwordPolicy.js). Las cuentas existentes siguen entrando; la regla aplica al crear o cambiar contraseña.admin (dueño) y cashier; los roles de restaurante (recepción, vendedor, almacén) pasaron a cajero y ya no se pueden pedir por la API. En Empleados → Acceso a la app el dueño ve quién entra (con su último acceso) y puede desactivar, reactivar o cambiar el rol de cada cuenta. Cada cambio cierra las sesiones de esa cuenta al instante (aunque tenga un token vigente), cuenta en el plan (desactivar libera su lugar; reactivar respeta el límite) y queda en la bitácora (activity_log). Blindajes: no se puede cambiar o desactivar la propia cuenta, ni cuentas de otra tienda o de la plataforma, y la tienda nunca se queda sin dueño activo (ni aunque dos dueños se quiten el acceso al mismo tiempo: el cambio se deshace con un 409). Código en backend/services/team.service.js./mesas, /tables, lista de espera) y GET /usuarios/find, que devolvía la cuenta completa (con hash de contraseña) de cualquier correo, también de otras tiendas. «Personal y turnos» (/waiters) sigue.platform_admin y platform_support (al entrar se les pide activarla con un QR). Da 8 códigos de respaldo de un solo uso. El secreto se guarda cifrado (MFA_ENCRYPTION_KEY o SECRET_KEY).rate_limits, con TTL) para que valgan entre instancias/serverless: global, login, registro, recuperar contraseña, 2FA, refresh y rutas públicas (factura, invitaciones).Cada tienda conecta su propia cuenta de Mercado Pago y elige su terminal en Configuración → Terminal de cobro. Al cobrar con tarjeta, el monto aparece solo en la terminal y la venta se registra únicamente si el pago sale aprobado.
Configuración del servidor (una vez, ver backend/.env.example):
MP_CLIENT_ID y MP_CLIENT_SECRET.MP_OAUTH_REDIRECT = {API_PUBLIC_URL}/point/oauth/callback.{API_PUBLIC_URL}/point/webhook, con los eventos Order (Mercado Pago) y Planes y suscripciones (incluye los pagos recurrentes); los avisos de suscripciones se atienden en facturación. La clave secreta va en MP_WEBHOOK_SECRET.OAUTH_STATE_SECRET y TOKEN_ENC_KEY (32 bytes en hex): node -e "console.log(require('crypto').randomBytes(32).toString('hex'))".
Los tokens de cada tienda se guardan cifrados (AES-256-GCM); si cambias TOKEN_ENC_KEY hay que reconectar las cuentas.Garantías: el cobro se crea en la terminal antes de registrar la venta; la venta lo "consume" una sola vez, solo si está aprobado, es de la misma tienda y por el mismo monto. Si el monto aprobado no coincide, queda en revisión y no se usa. Si la terminal o la cuenta se desconectan, se avisa antes de cobrar y se puede cobrar manual.
Personas que venden Mi Tiendita a tiendas (por ejemplo, en otro estado) y ganan comisión por cada cobro de las tiendas que traen. Se manejan en Admin → Vendedores (solo el admin de la plataforma).
Alta: el admin crea al vendedor y se le genera su número de referencia, con formato MT-XXXXXX.
Cómo llegan las tiendas: escriben el código en el registro (o entran con el enlace /register?ref=MT-XXXXXX, que lo llena solo). El admin también puede asignar o quitar el vendedor desde la ficha del cliente.
Comisión por cobro: empieza en 15% y sube con las ventas cerradas (tiendas referidas que ya pagaron al menos una vez):
| Ventas cerradas | Comisión |
|---|---|
| 0–4 | 15% |
| 5–9 | 17% |
| 10–19 | 19% |
| 20–34 | 21% |
| 35–49 | 23% |
| 50 o más | 25% (máximo) |
La tasa de cada cobro se guarda al generarse: subir de nivel no cambia lo ya ganado. La escalera está en backend/services/referral.tiers.js.
Qué cobros cuentan: la activación de la suscripción, cada cobro recurrente de Mercado Pago y los cobros que el admin registra a mano (efectivo, transferencia, licencia perpetua). Un mismo cobro nunca paga dos veces.
Pagos al vendedor: en la pestaña Cobros el admin ve lo pendiente, liquida todo junto (queda el historial) o anula un cobro pendiente (por ejemplo, un reembolso).
Mercado Pago: en los Webhooks de tu aplicación activa también el evento Pagos recurrentes (subscription_authorized_payment), además de las suscripciones; sin él solo se comisiona la activación y no las renovaciones.
Tres áreas separadas, cada una con sus propios roles:
| Área | Roles | Ve |
|---|---|---|
| Tienda | admin (dueño), cashier | Solo su tienda |
| Socio (vendedor/proveedor de Mi Tiendita) | partner_admin (dueño del socio), partner_staff (asesor) | Solo las tiendas que llegaron con su código |
| Plataforma | platform_admin, platform_support | Todo |
frontend/src/seo/site.mjs (títulos, descripciones, precios, páginas por giro, datos estructurados).frontend/seo-plugin.mjs): cada página pública sale con su <title>, descripción, canónica, Open Graph/Twitter (/og.png), hreflang es-MX, JSON-LD (Organization, SoftwareApplication con precios, WebSite, FAQPage, BreadcrumbList) y su contenido en texto, así buscadores y vistas previas de WhatsApp/Facebook no dependen de JavaScript./punto-de-venta-para-abarrotes, -para-farmacia, -para-ferreteria, -para-papeleria, /punto-de-venta-con-bascula, /punto-de-venta-sin-internet.sitemap.xml y robots.txt se generan en cada build; el panel, la caja y las cuentas llevan noindex y están bloqueados en robots.site.mjs y su regla en vercel.json (el build falla si falta).Los precios están escritos en el código, no en variables de entorno (las MP_PLAN_*_PRICE ya no se usan; si quedaron en Vercel se ignoran). Para cambiarlos edita backend/services/plans.catalog.js y frontend/src/seo/site.mjs (PRICES); una prueba falla si no coinciden. La landing, Facturación, el simulador de proveedores y el SEO se actualizan solos.
docker compose up --build -d
Cada push a main ejecuta .github/workflows/deploy.yml y despliega a Vercel. Cuando el job de deploy termina (éxito, fallo o cancelación) un segundo job envía un correo HTML del sistema interno minegocio (verde bosque) con el resultado. Si armar o enviar el correo falla, el estado del workflow sigue siendo el del deploy. Hasta que existan MAIL_USERNAME y MAIL_PASSWORD, el envío se omite y el log deja una advertencia; el deploy no cambia de resultado.
Vista previa local:
node backend/scripts/render-deploy-mail.js --preview
Esos secretos tienen que estar en GitHub (no en Vercel): entorno production o secretos del repositorio en Actions. El job de correo usa environment: production, así que lee los secretos de ese entorno.
El mensaje incluye resultado, mensaje del commit, SHA corto, autor, quién hizo push, rama, fecha y hora en America/Tijuana, enlace al commit, enlace a la ejecución de Actions y la URL de producción https://www.mitiendita.software/.
Destinatarios por defecto:
Secretos en Settings → Environments → production → Environment secrets (o en Settings → Secrets and variables → Actions si los quieres a nivel de repositorio):
MAIL_USERNAME (obligatorio) — dirección de Gmail que envía, por ejemplo [email protected].MAIL_PASSWORD (obligatorio) — contraseña de aplicación de esa cuenta. No uses la contraseña normal ni la subas al repositorio.MAIL_TO (opcional) — destinatarios separados por comas. Si no existe, se usan los tres correos de arriba.Para crear la contraseña de aplicación hace falta la verificación en 2 pasos en la cuenta de Google:
MAIL_PASSWORD.MAIL_USERNAME pon la misma dirección de Gmail.