Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions app/src/components/ProjectIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Cake,
Calendar,
Camera,
Folder,
Car,
ChefHat,
Clipboard,
Expand Down Expand Up @@ -192,8 +193,10 @@ const ICON_MAP: Record<string, LucideIcon> = {

/**
* 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,
Expand All @@ -206,9 +209,15 @@ export function ProjectIcon({
if (Icon) {
return <Icon className={className} aria-hidden="true" />;
}
return (
<span className={className} aria-hidden="true">
{name}
</span>
);
// Emoji legado: grafemas cortos sin letras ni números se pintan tal cual.
const emojiLike = name.length > 0 && name.length <= 8 && !/[\p{L}\p{N}]/u.test(name);
if (emojiLike) {
return (
<span className={className} aria-hidden="true">
{name}
</span>
);
}
const Fallback = Folder;
return <Fallback className={className} aria-hidden="true" />;
}
13 changes: 11 additions & 2 deletions server/src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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)')
Expand Down
6 changes: 5 additions & 1 deletion server/tests/decision-gamification.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
})
})
Loading