feat(notificaciones): edit summary recipients in the UI
Build and Push Images / Build jorgecuadros-web (push) Successful in 2m32s
Build and Push Images / Build jorgecuadros-api (push) Successful in 3m28s

NOTIFICATION_ADMIN_EMAILS made "add Beto to the summaries" a redeploy —
the wrong unit of work for a list that changes when office staff change.

Adds `app_settings`, a key/value table for the configuration staff must
be able to change without a deploy, and `SettingsService`, which resolves
every key db -> env -> default and reports which of the three a value
came from. That ladder is what makes the move safe: a deployment behaves
exactly as before until somebody saves in the UI, and the screen can say
"this is still coming from the deployment" rather than implying somebody
chose it.

- new ability `setting:manage` (ADMIN) — deliberately above
  `notification:send`, since redirecting the audit summaries is how
  someone would quietly stop them being read
- GET/PUT /notifications/settings/admin-emails; read is open to any
  logged-in user so the UI can display the list, write is gated
- resolved per job, not cached at boot, or we would reintroduce exactly
  the restart-to-apply behaviour being removed
- a saved empty list means "nobody" and does NOT fall through to the env,
  or clearing the field would keep mailing the people just removed

Credentials stay in env — see the model doc for where the line is drawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 11:58:42 -07:00
co-authored by Claude Opus 5
parent f4b92fa7a5
commit a491ef3eed
16 changed files with 604 additions and 26 deletions
@@ -0,0 +1,19 @@
-- Operator-editable configuration.
--
-- First tenant: the notification summary recipients, which were
-- NOTIFICATION_ADMIN_EMAILS in the environment. That list changes when office
-- staff change — a redeploy is the wrong unit of work for "Ana left, add
-- Beto" — so it belongs in the database with a UI, not in a stack env var.
--
-- Credentials stay in env. See the model doc in schema.prisma for where the
-- line is drawn.
-- CreateTable
CREATE TABLE `app_settings` (
`key` VARCHAR(191) NOT NULL,
`value` TEXT NOT NULL,
`updatedAt` DATETIME(3) NOT NULL,
`updatedById` VARCHAR(191) NULL,
PRIMARY KEY (`key`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+27
View File
@@ -1125,3 +1125,30 @@ model ScheduledJobState {
@@map("scheduled_job_states")
}
/// Operator-editable configuration — the settings that staff must be able to
/// change without a redeploy.
///
/// Deliberately NOT a home for everything in the environment. Credentials and
/// endpoints (SES keys, DATABASE_URL, S3) stay in env: they are deployment
/// identity, they must exist before the app can talk to its own database, and
/// putting a secret in a table only widens who can read it. What belongs here
/// is the opposite kind of value — no secret, changes on office business
/// rhythm rather than deploy rhythm, and wrong far more often than the
/// deployment is.
///
/// `value` is TEXT holding whatever encoding the owning feature defines
/// (a comma-separated list, a JSON blob). Each setting has exactly one reader,
/// which owns parsing and validation; there is no generic typed accessor,
/// because a schema-less bag with a typed façade is just a schema with the
/// checks moved somewhere easier to forget.
model AppSetting {
key String @id
value String @db.Text
updatedAt DateTime @updatedAt
/// Who last changed it. Null for rows written before the UI existed or by
/// a migration. Not an FK: a setting must outlive the user who set it.
updatedById String?
@@map("app_settings")
}