From 366d51ca0d076db18b6483cd97ce9c827880de6d Mon Sep 17 00:00:00 2001 From: gnacho Date: Sat, 3 Oct 2026 22:27:19 +0200 Subject: [PATCH 1/2] fix(server): create the decision ledger index outside SCHEMA v2.6.44 shipped the index in the SCHEMA template, which runs before migrateSchema; on existing databases the table has no decision_id yet, so startup crash-looped with SQLITE_ERROR. Create it in migrateSchema instead (idempotent, covers fresh and migrated installs) and add a regression test asserting SCHEMA never contains that index. --- server/src/db.js | 13 +++++++++++-- server/tests/decision-gamification.test.js | 6 +++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/server/src/db.js b/server/src/db.js index a56613f..9e4d3d2 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -9,7 +9,7 @@ const log = logger.child({ component: 'db' }) // Esquema completo: base común (users/sessions/login_attempts/kv) + dominio Deltos. // Las fechas son epoch ms (INTEGER) salvo due_date, que es 'YYYY-MM-DD'. -const SCHEMA = ` +export const SCHEMA = ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, @@ -403,7 +403,9 @@ CREATE TABLE IF NOT EXISTS gam_points_ledger ( ); CREATE INDEX IF NOT EXISTS idx_gam_ledger_user ON gam_points_ledger(user_id, created_at); CREATE INDEX IF NOT EXISTS idx_gam_ledger_task ON gam_points_ledger(task_id, created_at); -CREATE INDEX IF NOT EXISTS idx_gam_ledger_decision ON gam_points_ledger(decision_id, created_at); +-- SIN indice por decision aqui: el SCHEMA corre ANTES de migrateSchema y en +-- BD viejas la tabla aun no tiene esa columna (leccion #246, issue #282). Lo +-- crea migrateSchema siempre. -- Recompensas canjeables con puntos (borrado lógico con active=0). CREATE TABLE IF NOT EXISTS gam_rewards ( @@ -627,6 +629,13 @@ export function migrateSchema(db) { db.exec('CREATE INDEX IF NOT EXISTS idx_gam_ledger_reverted ON gam_points_ledger(task_id, reverted_at)') log.info('schema_migrated', { table: 'gam_points_ledger', change: 'decision_id + task_id nullable' }) } + // Indices del ledger SIEMPRE aqui, fuera del SCHEMA (issue #282, leccion + // #246): el SCHEMA corre antes de la migracion y en BD viejas la tabla aun + // no tiene decision_id. IF NOT EXISTS: barato y cubre nuevas y migradas. + db.exec('CREATE INDEX IF NOT EXISTS idx_gam_ledger_user ON gam_points_ledger(user_id, created_at)') + db.exec('CREATE INDEX IF NOT EXISTS idx_gam_ledger_task ON gam_points_ledger(task_id, created_at)') + db.exec('CREATE INDEX IF NOT EXISTS idx_gam_ledger_decision ON gam_points_ledger(decision_id, created_at)') + db.exec('CREATE INDEX IF NOT EXISTS idx_gam_ledger_reverted ON gam_points_ledger(task_id, reverted_at)') const gamLedgerIndexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='gam_points_ledger'").all().map((r) => r.name) if (!gamLedgerIndexes.includes('idx_gam_ledger_reverted')) { db.exec('CREATE INDEX IF NOT EXISTS idx_gam_ledger_reverted ON gam_points_ledger(task_id, reverted_at)') diff --git a/server/tests/decision-gamification.test.js b/server/tests/decision-gamification.test.js index eca432b..84cf76a 100644 --- a/server/tests/decision-gamification.test.js +++ b/server/tests/decision-gamification.test.js @@ -4,7 +4,7 @@ // nullable, task_id deja de ser NOT NULL sin perder filas). import { describe, it, expect } from 'vitest' import { makeInstance, loginAdmin, loginUser, jsonReq } from './helpers.js' -import { migrateSchema } from '../src/db.js' +import { migrateSchema, SCHEMA } from '../src/db.js' const PASS = 'passwd1234567' @@ -412,4 +412,8 @@ describe('migración gam_points_ledger (decision_id + task_id nullable)', () => expect(me.balance).toBe(5 + 1 + 5) // task1 + decision_vote + task2 expect(me.tasks_done_total).toBe(2) // solo las dos tareas }) + + it('el índice de decisión NO vive en el SCHEMA (en BD viejas el CREATE INDEX revienta antes de migrar, lección #246 / issue #282)', () => { + expect(SCHEMA.includes('idx_gam_ledger_decision')).toBe(false) + }) }) From 6ca096a0c2ac63933e7581ce461119b4f570d5be Mon Sep 17 00:00:00 2001 From: gnacho Date: Sat, 3 Oct 2026 22:27:19 +0200 Subject: [PATCH 2/2] fix(app): neutral fallback icon for unknown project icon names Unknown stored names (e.g. folder from old installs) rendered as raw overlapping text; legacy emoji values still render as text, anything else falls back to a neutral folder icon. --- app/src/components/ProjectIcon.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/components/ProjectIcon.tsx b/app/src/components/ProjectIcon.tsx index 726f689..9aee650 100644 --- a/app/src/components/ProjectIcon.tsx +++ b/app/src/components/ProjectIcon.tsx @@ -13,6 +13,7 @@ import { Cake, Calendar, Camera, + Folder, Car, ChefHat, Clipboard, @@ -192,8 +193,10 @@ const ICON_MAP: Record = { /** * Icono de proyecto. Si `name` es un icono Lucide del catálogo lo pinta como - * SVG (sigue el tema); si no (emoji legado guardado antes del selector) lo - * renderiza como texto para no perder datos. + * SVG (sigue el tema); si es un emoji legado guardado antes del selector lo + * renderiza como texto (corto y sin letras: no destroza el layout); y si es + * cualquier otro valor desconocido (p.ej. "folder" de instalaciones antiguas) + * cae a un icono neutro en vez de pintar el nombre crudo solapado (#272). */ export function ProjectIcon({ name, @@ -206,9 +209,15 @@ export function ProjectIcon({ if (Icon) { return