Renewal avisos left behind only a `RenewalNotice` row, whose sole job is gating: a row with `sentAt` drops the policy off the pending list. It cannot represent a failed send or a customer with no address, so the Pólizas tab had no "Registro de envíos" to show and a sent notice simply vanished from the list. Renewals now write `email_notification_log` — the same table the four bulk jobs write — as `RENEWAL_NOTICE` / `POLICIES`, with rows for failures and no-email skips too. `RenewalNotice` keeps its gating role unchanged; the two are complementary, not redundant. - extend `EmailNotificationType` (+RENEWAL_NOTICE) and `EmailNotificationServicio` (+POLICIES); `level` now carries the aviso generation on renewal rows, so every reader must branch on the type first (`notificationLevelLabel()` is the one place that lives) - backfill emailed notices (`channel = 'EMAIL'`) into the log; MAIL-channel rows are legacy printed letters and are deliberately left out - extract `NotificationLogService`/`NotificationLogModule` as the single writer, so a feature that sends mail records it without pulling the bulk-job pipelines into its module - `GET /notifications/log` and `/stats` take a comma-separated `servicio` list; each tab reads its own slice. This also fixes the "Omitidos" view, which mapped to no filter at all and showed every row - share one `NotificationLogPanel` between both tabs - pass SES_* / NOTIFICATION_ADMIN_EMAILS through the galactus compose, which was missing them entirely — mail is runtime config, not a CI secret, and the prod image sets NODE_ENV=production so a blank config fails loudly instead of falling back to stdout Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
7.0 KiB
YAML
143 lines
7.0 KiB
YAML
# NestJS API + Next.js web on galactus (standalone Docker, Portainer endpoint 3).
|
|
#
|
|
# Standalone port of deploy/jorgecuadros-app.stack.yml — see the header of
|
|
# deploy/galactus/jorgecuadros-db.compose.yml for the Swarm keys plain compose
|
|
# silently ignores. The one that matters most here: without
|
|
# `restart: unless-stopped` neither service returns after a host reboot.
|
|
#
|
|
# Cross-stack traffic still goes over the HOST, not service DNS. db and minio
|
|
# are separate Portainer stacks, so they are on separate compose networks and
|
|
# their service names do not resolve from here. DATABASE_URL / S3_ENDPOINT must
|
|
# name galactus's own address and the published port — exactly as on cubex
|
|
# today. Do not "simplify" them to `mysql:3306`.
|
|
#
|
|
# ...which means these containers have to resolve galactus's MagicDNS name, and
|
|
# by default they CANNOT. The host runs systemd-resolved, whose 127.0.0.53 stub
|
|
# is unreachable from a container, so Docker falls back to the upstream resolver
|
|
# in /run/systemd/resolve/resolv.conf — the LAN router, which knows nothing
|
|
# about the tailnet. Routing to 100.x works fine; only the lookup fails, and the
|
|
# API dies with Prisma P1001 "can't reach database server". Pointing the
|
|
# containers at Tailscale's own resolver fixes it. 100.100.100.100 is Tailscale's
|
|
# fixed anycast MagicDNS address (identical on every tailnet); the search domain
|
|
# is this tailnet's suffix.
|
|
#
|
|
# The web image is NOT URL-baked: the browser's API origin is injected at
|
|
# runtime from API_ORIGIN (apps/web/src/app/layout.tsx), so the same image works
|
|
# for any deployment. APP_VERSION / GIT_SHA / BUILD_DATE come baked in from
|
|
# build.yml and are surfaced at GET /version (api) and in the web footer.
|
|
#
|
|
# Keep in sync with deploy/jorgecuadros-app.stack.yml when either changes.
|
|
|
|
services:
|
|
api:
|
|
image: git.mancinas.io/rmancinas/jorgecuadros-api:${APP_TAG:-latest}
|
|
restart: unless-stopped
|
|
# Stable handle for deploy/scripts/pre-migrate-backup.sh, which finds this
|
|
# container by label to run mysqldump into the backup volume. A label
|
|
# survives stack renames; the compose service name does not.
|
|
labels:
|
|
io.jorgecuadros.role: "api"
|
|
dns:
|
|
# MagicDNS first, then a public resolver. Listing ONLY 100.100.100.100
|
|
# costs the container public name resolution — apk/npm/any outbound
|
|
# hostname stops resolving — because MagicDNS does not forward to an
|
|
# upstream unless the tailnet is configured with global nameservers.
|
|
- ${TAILSCALE_DNS:-100.100.100.100}
|
|
- ${FALLBACK_DNS:-1.1.1.1}
|
|
dns_search:
|
|
- ${TAILNET_SUFFIX:-tail01aa2.ts.net}
|
|
environment:
|
|
DATABASE_URL: ${DATABASE_URL:?DATABASE_URL must be set}
|
|
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set}
|
|
# This deployment is HTTP, so a Secure session cookie would never be sent
|
|
# and login would silently never establish a session (express-session
|
|
# declines to emit a Secure cookie over a plain connection). Acceptable
|
|
# here ONLY because galactus is reachable exclusively over Tailscale, so
|
|
# WireGuard already encrypts the wire. Set this back to "true" the moment
|
|
# the app is served over TLS or exposed off-tailnet.
|
|
SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-false}
|
|
WEB_ORIGIN: ${WEB_ORIGIN:?WEB_ORIGIN must be set}
|
|
PORT: "3001"
|
|
INGEST_DIR: /data/ingest
|
|
BACKUP_DIR: /data/backups
|
|
MIGRATION_ENV: prod
|
|
# Credentials the "Operaciones" screen runs mysqldump/mysql as. NOT the
|
|
# application user: --single-transaction needs the global RELOAD privilege
|
|
# and the app user has only ALL ON jorgecuadros.*, so every backup, sync
|
|
# and re-import fails without this. Host/port/database still come from
|
|
# DATABASE_URL — this only changes who logs in. See opsConn() in
|
|
# apps/api/src/ops/ops.service.ts.
|
|
OPS_DB_ADMIN_USER: ${OPS_DB_ADMIN_USER:-root}
|
|
OPS_DB_ADMIN_PASSWORD: ${OPS_DB_ADMIN_PASSWORD:?OPS_DB_ADMIN_PASSWORD must be set}
|
|
S3_ENDPOINT: ${S3_ENDPOINT:?S3_ENDPOINT must be set}
|
|
S3_BUCKET: ${S3_BUCKET:-jorgecuadros-documents}
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER must be set}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD must be set}
|
|
# Outbound mail (SES). Runtime config, never baked into the image and
|
|
# never a CI secret — the build does not send mail, this container does.
|
|
# The image sets NODE_ENV=production, which disables MailService's
|
|
# stdout dev fallback: leave these blank and every notification and
|
|
# renewal aviso fails with "El envío de correo no está configurado."
|
|
# rather than silently going nowhere. Values live in this stack's env
|
|
# file on galactus (deploy/.env.prod), same as DATABASE_URL.
|
|
SES_REGION: ${SES_REGION:-}
|
|
SES_FROM: ${SES_FROM:-}
|
|
SES_FROM_NAME: ${SES_FROM_NAME:-}
|
|
SES_ACCESS_KEY: ${SES_ACCESS_KEY:-}
|
|
SES_SECRET_KEY: ${SES_SECRET_KEY:-}
|
|
SES_CONFIGURATION_SET: ${SES_CONFIGURATION_SET:-}
|
|
# Who gets the per-job summary mail. Falls back to the two hardcoded
|
|
# defaults in NotificationsService when unset.
|
|
NOTIFICATION_ADMIN_EMAILS: ${NOTIFICATION_ADMIN_EMAILS:-}
|
|
ports:
|
|
- "${API_PORT:-3001}:3001"
|
|
volumes:
|
|
# Uploaded Access files and DB backups. Named, so they survive every
|
|
# redeploy — and so the pre-migrate dump the deploy takes is the same
|
|
# file the "Operaciones" restore screen lists.
|
|
- ingest_data:/data/ingest
|
|
- backup_data:/data/backups
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 30s
|
|
|
|
web:
|
|
image: git.mancinas.io/rmancinas/jorgecuadros-web:${APP_TAG:-latest}
|
|
restart: unless-stopped
|
|
labels:
|
|
io.jorgecuadros.role: "web"
|
|
# Next server-side rendering can call the API by API_ORIGIN, which is the
|
|
# same MagicDNS name — so the web container needs the resolver too.
|
|
dns:
|
|
# MagicDNS first, then a public resolver. Listing ONLY 100.100.100.100
|
|
# costs the container public name resolution — apk/npm/any outbound
|
|
# hostname stops resolving — because MagicDNS does not forward to an
|
|
# upstream unless the tailnet is configured with global nameservers.
|
|
- ${TAILSCALE_DNS:-100.100.100.100}
|
|
- ${FALLBACK_DNS:-1.1.1.1}
|
|
dns_search:
|
|
- ${TAILNET_SUFFIX:-tail01aa2.ts.net}
|
|
environment:
|
|
# Public API URL the browser calls (injected at runtime, see layout.tsx).
|
|
API_ORIGIN: ${API_ORIGIN:?API_ORIGIN must be set}
|
|
ports:
|
|
- "${WEB_PORT:-3000}:3000"
|
|
depends_on:
|
|
# Unlike Swarm — which ignores depends_on entirely — plain compose honours
|
|
# this, so web waits for the API to pass its healthcheck.
|
|
api:
|
|
condition: service_healthy
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/ >/dev/null 2>&1 || exit 1"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 10
|
|
start_period: 30s
|
|
|
|
volumes:
|
|
ingest_data:
|
|
backup_data:
|