From ece8163d15c5cc86ea2dc9ddc3a7cd2ee4cad5f3 Mon Sep 17 00:00:00 2001 From: Daniel Arroyo Date: Mon, 23 Mar 2026 22:25:36 -0300 Subject: [PATCH] chore: Various improvements and CI setup - Add Jenkinsfile for CI/CD pipeline - Fix keyboard shortcut '?' handling for help dialog - Update note form and connections components - Add work mode toggle improvements - Update navigation history and usage tracking - Improve validators - Add session summaries --- CLAUDE.md | 6 +- Jenkinsfile | 126 ++++++++ prisma/dev.db | Bin 143360 -> 143360 bytes resumen/2026-03-22-1942-resumen.md | 342 ++++++++++++++++++++++ resumen/2026-03-22-2000-resumen.md | 431 ++++++++++++++++++++++++++++ src/components/note-connections.tsx | 49 ++-- src/components/note-form.tsx | 10 +- src/components/work-mode-toggle.tsx | 11 +- src/hooks/use-global-shortcuts.ts | 2 +- src/lib/navigation-history.ts | 9 +- src/lib/usage.ts | 9 +- src/lib/validators.ts | 17 +- 12 files changed, 976 insertions(+), 36 deletions(-) create mode 100644 Jenkinsfile create mode 100644 resumen/2026-03-22-1942-resumen.md create mode 100644 resumen/2026-03-22-2000-resumen.md diff --git a/CLAUDE.md b/CLAUDE.md index 0b4e9cf..ea61514 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,4 +26,8 @@ Build, test, and lint commands will be documented here once the project structur - Cuando te pida realizar un resumen del proyecto debes crear un archivo con el siguiente formato de nombre yyyy-mm-dd-HHMM-resumen.md en la carpeta resumen. - Si no existe crea una carpeta resumen en la raiz del proyecto. -- Crearemos resumenes de forma incremental y el primero debe contener todo lo existente hasta el momento. \ No newline at end of file +- Crearemos resumenes de forma incremental y el primero debe contener todo lo existente hasta el momento. +- El archivo debe ser creado con el horario local. + +## Commit +- evitar agregar lo siguiente: Co-Authored-By: Claude Opus 4.6 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..fc24bb0 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,126 @@ +pipeline { + agent { + node { + label 'java-springboot' + } + } + environment { + URL_REGISTRY = 'gitea.danielarroyo.cl' + PROJECT = 'home-projects' + REMOTE_USER = 'root' + REMOTE_HOST = '10.5.0.116' + REMOTE_PATH = '/compose' + DOCKER_CREDENTIALS = credentials('gitea-docker-registry') + } + stages { + stage('Obtener Nombre del Repositorio') { + steps { + script { + sh 'env | sort' + echo "GIT_URL: ${env.GIT_URL}" + echo "GIT_URL_1: ${env.GIT_URL_1}" + def gitUrl = env.GIT_URL ?: env.GIT_URL_1 + if (gitUrl) { + def repoName = gitUrl.tokenize('/').last().replace('.git', '') + echo "Nombre extraído del repositorio: ${repoName}" + env.NAME_SERVICE = repoName + echo "El nombre del repositorio asignado a NAME_SERVICE: ${env.NAME_SERVICE}" + } else { + echo "No se pudo obtener la URL del repositorio. GIT_URL y GIT_URL_1 no están definidos." + env.NAME_SERVICE = 'unknown' + } + } + } + } + stage('Build') { + steps { + echo "El nombre del repositorio es: ${env.NAME_SERVICE}" + script { + try { + sh """ + ls -la + ls -la src || echo "Directorio src no encontrado" + ls -la src/main/docker || echo "Directorio src/main/docker no encontrado" + cat .dockerignore || echo ".dockerignore no encontrado" + docker -v + docker build \ + -t ${URL_REGISTRY}/${PROJECT}/${NAME_SERVICE}:${BUILD_NUMBER} \ + -f . + docker tag ${URL_REGISTRY}/${PROJECT}/${NAME_SERVICE}:${BUILD_NUMBER} \ + ${URL_REGISTRY}/${PROJECT}/${NAME_SERVICE}:latest + """ + } catch (Exception e) { + error "Build failed: ${e.message}" + } + } + } + } + stage('Push to Registry') { + steps { + script { + try { + docker.withRegistry("https://${URL_REGISTRY}", 'gitea-docker-registry') { + sh """ + docker push ${URL_REGISTRY}/${PROJECT}/${NAME_SERVICE}:${BUILD_NUMBER} + docker push ${URL_REGISTRY}/${PROJECT}/${NAME_SERVICE}:latest + """ + } + } catch (Exception e) { + error "Push to registry failed: ${e.message}" + } + } + } + } + stage('Deploy') { + steps { + script { + def dockerComposeTemplate = """ +services: + ${NAME_SERVICE}: + image: ${URL_REGISTRY}/${PROJECT}/${NAME_SERVICE}:${BUILD_NUMBER} + container_name: ${NAME_SERVICE} + labels: + - "traefik.enable=true" + - "traefik.http.services.${NAME_SERVICE}.loadbalancer.server.port=3000" + - "traefik.http.routers.${NAME_SERVICE}.entrypoints=web" + - "traefik.http.routers.${NAME_SERVICE}.rule=Host(`recall.vodorod.cl`)" + environment: + - TZ=America/Santiago + - DATABASE_URL=file:./data/dev.db + volumes: + - ./data:/app/data + networks: + - homelab-net + mem_limit: 32m + mem_reservation: 16m + restart: unless-stopped +networks: + homelab-net: + external: true +""" + writeFile file: 'docker-compose.yaml', text: dockerComposeTemplate + + sshagent(credentials: ['ssh-virtual-machine']) { + withCredentials([usernamePassword(credentialsId: 'gitea-docker-registry', usernameVariable: 'REG_USR', passwordVariable: 'REG_PSW')]) { + sh ''' + ssh -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST} "docker login ${URL_REGISTRY} -u ${REG_USR} -p ${REG_PSW}" + ssh -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST} "mkdir -p ${REMOTE_PATH}/${PROJECT}/${NAME_SERVICE}" + scp docker-compose.yaml ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/${PROJECT}/${NAME_SERVICE}/docker-compose.yaml + ssh -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST} "cd ${REMOTE_PATH}/${PROJECT}/${NAME_SERVICE} && docker compose down && docker compose pull && docker compose up -d" + ssh -o StrictHostKeyChecking=no ${REMOTE_USER}@${REMOTE_HOST} "docker system prune -f" + ''' + } + } + } + } + } + } + post { + always { + cleanWs() + } + failure { + echo "Pipeline failed. Check logs for details." + } + } +} \ No newline at end of file diff --git a/prisma/dev.db b/prisma/dev.db index f7d2634fcc1878dd6fdf91bbd6df4753f0cdc78f..a464858a5ab70e8a9bf28047a48d642cbf2a6fb7 100644 GIT binary patch delta 7134 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 delta 938 zcmb7CTSyd97(VCBj59mCvu8|JT{D_Q5X8&qqKg=f6=g)=CD4b6-Enu#ZQTp&stG~s zL(jQQF%YRM-M|;i!G@j;!Uvxsq9_Rz_Dv#R>ZK4mvzwa~2EBae!uNgu_y6alx485c zH&cbcmrV63g3tU5&2|6)!fODp;ha$_y}+7sg3}+)K^KFnDaEGsDQ=M`Bp_MUPWY8J=`^7~fLZwoOheI*y8v=-0_P^4C!)8` z8Xsg8Qi)*#(O9c@BAN=zvfR=-k?;hf5p{g3eW;kU8Nu0#)3s;wc!4zNBYpf9Mu^a3 z3B4`D;TCyvLeI_>T5QZK&gIdE{-}4b%w4Eg(Df81N}hlJ)IC<>zZ~-4;lO^LKZW1$ zp+=JbSQP;YTh21C>g-FX8Y5i{Hpzd{Wy%h&t*~ep*O!4sfP0+xg?EDJ=(eZ$$F}#} z53YgjD;Z`Y^c3}mszqLS6)a{JuA7S@>Zi?E0~$bPe+63N1maH#+W?O^UkTe}?4JZY zJs>-j>ALB*W2sOq9E?k38;wh2q2j70)GMkq9{a@!1>;Fc o3dRGGP*NI8#S{DP|KF_XA8sT_RE-7UZ29N!Z3Q$}o6t%46EUF|2mk;8 diff --git a/resumen/2026-03-22-1942-resumen.md b/resumen/2026-03-22-1942-resumen.md new file mode 100644 index 0000000..ce6b859 --- /dev/null +++ b/resumen/2026-03-22-1942-resumen.md @@ -0,0 +1,342 @@ +# Recall - Resumen del Proyecto + +## Fecha +2026-03-22 + +## Descripción +Recall es una aplicación de gestión de conocimiento personal (PKM) para captura y recuperación de notas, comandos, snippets y conocimiento técnico. + +## Stack Tecnológico +- **Framework**: Next.js 16.2.1 con App Router + Turbopack +- **Base de datos**: SQLite via Prisma ORM +- **Lenguaje**: TypeScript +- **UI**: TailwindCSS + shadcn/ui components +- **Testing**: Jest (226 tests) +- **Notificaciones**: Sonner (toasts) + +## Estructura del Proyecto + +``` +src/ +├── app/ +│ ├── api/ +│ │ ├── notes/ # CRUD, versions, quick, backlinks, links, suggest +│ │ ├── tags/ # Tags y sugerencias +│ │ ├── search/ # Búsqueda avanzada +│ │ ├── usage/ # Tracking de uso y co-uso +│ │ ├── metrics/ # Métricas internas +│ │ ├── centrality/ # Notas centrales +│ │ ├── export-import/ # Import/export JSON, Markdown, HTML +│ │ ├── import-markdown/ # Importador Markdown mejorado +│ │ └── capture/ # Captura externa (bookmarklet) +│ ├── notes/[id]/ # Detalle de nota +│ ├── edit/[id]/ # Edición de nota +│ ├── new/ # Nueva nota +│ ├── capture/ # Página de confirmación de captura +│ └── settings/ # Configuración +├── components/ +│ ├── ui/ # shadcn/ui components +│ ├── dashboard.tsx # Dashboard inteligente +│ ├── quick-add.tsx # Captura rápida +│ ├── note-form.tsx # Formulario de nota +│ ├── note-connections.tsx # Panel de conexiones +│ ├── note-list.tsx # Lista de notas +│ ├── keyboard-navigable-note-list.tsx # Lista con navegación teclado +│ ├── keyboard-hint.tsx # Hint de atajos +│ ├── related-notes.tsx # Notas relacionadas +│ ├── version-history.tsx # Historial de versiones +│ ├── track-note-view.tsx # Tracking de vistas +│ ├── search-bar.tsx # Búsqueda en tiempo real +│ ├── command-palette.tsx # Command palette (Ctrl+K) +│ ├── keyboard-shortcuts-dialog.tsx # Diálogo de atajos +│ ├── shortcuts-provider.tsx # Provider de shortcuts +│ ├── work-mode-toggle.tsx # Toggle modo trabajo +│ ├── draft-recovery-banner.tsx # Banner de recuperación +│ ├── backup-restore-dialog.tsx # Restore con preview +│ ├── backup-list.tsx # Lista de backups +│ ├── bookmarklet-instructions.tsx # Instrucciones del bookmarklet +│ ├── recent-context-list.tsx # Historial de navegación +│ ├── track-navigation-history.tsx # Tracking de historial +│ └── preferences-panel.tsx # Panel de preferencias +├── hooks/ +│ ├── use-global-shortcuts.ts # Atajos globales +│ ├── use-note-list-keyboard.ts # Navegación teclado en listas +│ ├── use-unsaved-changes.ts # Guard de cambios sin guardar +│ └── ... +└── lib/ + ├── prisma.ts # Cliente Prisma + ├── usage.ts # Tracking de uso y co-uso + ├── search.ts # Búsqueda con scoring + ├── query-parser.ts # Parser de queries avanzadas + ├── versions.ts # Historial de versiones + ├── related.ts # Notas relacionadas + ├── backlinks.ts # Sistema de enlaces [[wiki]] + ├── tags.ts # Normalización y sugerencias + ├── metrics.ts # Métricas de dashboard + ├── centrality.ts # Cálculo de centralidad + ├── type-inference.ts # Detección automática de tipo + ├── link-suggestions.ts # Sugerencias de enlaces + ├── features.ts # Feature flags + ├── validators.ts # Zod schemas + ├── errors.ts # Manejo de errores + ├── backup.ts # Snapshot de backup + ├── backup-storage.ts # IndexedDB storage + ├── backup-policy.ts # Política de retención + ├── backup-validator.ts # Validación de backups + ├── restore.ts # Restore de backups + ├── drafts.ts # Borradores locales + ├── work-mode.ts # Modo trabajo + ├── navigation-history.ts # Historial de navegación + ├── export-markdown.ts # Exportación Markdown + ├── export-html.ts # Exportación HTML + ├── import-markdown.ts # Importador Markdown + └── external-capture.ts # Captura externa +``` + +## Modelos de Datos + +### Note +```prisma +model Note { + id String @id @default(cuid()) + title String + content String + type String @default("note") + isFavorite Boolean @default(false) + isPinned Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + creationSource String @default("form") +} +``` + +### NoteUsage +```prisma +model NoteUsage { + id String @id @default(cuid()) + noteId String + eventType String + query String? + createdAt DateTime @default(now()) +} +``` + +### NoteCoUsage +```prisma +model NoteCoUsage { + id String @id @default(cuid()) + fromNoteId String + toNoteId String + weight Int @default(1) +} +``` + +### NoteVersion +```prisma +model NoteVersion { + id String @id @default(cuid()) + noteId String + title String + content String + createdAt DateTime @default(now()) +} +``` + +### Backlink +```prisma +model Backlink { + sourceNoteId String + targetNoteId String +} +``` + +## APIs Principales + +| Endpoint | Método | Descripción | +|----------|--------|-------------| +| `/api/notes` | GET, POST | Listar/crear notas | +| `/api/notes/[id]` | GET, PUT, DELETE | CRUD de nota | +| `/api/notes/[id]/versions` | GET, POST | Listar/crear versiones | +| `/api/notes/[id]/versions/[vid]` | GET, PUT | Ver/restaurar versión | +| `/api/notes/quick` | POST | Creación rápida | +| `/api/notes/links` | GET | Sugerencias de enlaces | +| `/api/search` | GET | Búsqueda con scoring | +| `/api/tags` | GET | Listar/buscar tags | +| `/api/tags/suggest` | GET | Sugerencias automáticas | +| `/api/usage` | GET | Estadísticas de uso | +| `/api/usage/co-usage` | GET | Notas co-usadas | +| `/api/metrics` | GET | Métricas de dashboard | +| `/api/centrality` | GET | Notas más centrales | +| `/api/export-import` | GET, POST | Export/Import (JSON, Markdown, HTML) | +| `/api/import-markdown` | POST | Importador Markdown mejorado | +| `/api/capture` | POST | Captura externa segura | + +## Features Implementadas + +### MVP-1 +- CRUD completo de notas +- Sistema de tags +- Búsqueda básica + +### MVP-2 +- Búsqueda avanzada con scoring +- Quick Add con prefijos (cmd:, snip:, etc.) +- Backlinks con sintaxis [[wiki]] +- Formularios guiados por tipo de nota + +### MVP-3 +- Usage tracking (vistas, clics, copias) +- Dashboard inteligente +- Scoring boost basado en uso +- Sugerencias automáticas de tags +- Panel "Conectado con" +- Quick Add multilínea +- Pegado inteligente con detección de tipo +- Sugerencia automática de tipo de nota +- Sugerencia de enlaces internos +- Registro de co-uso entre notas +- Métricas internas +- Cálculo de notas centrales +- Feature flags configurables + +### MVP-4 +- Query parser para búsquedas avanzadas (`type:`, `tag:`, `is:favorite`, `is:pinned`) +- Búsqueda en tiempo real con 300ms debounce +- Navegación por teclado (↑↓ Enter ESC) estilo Spotlight +- Dropdown de resultados con cache +- Sidebar contextual con co-uso +- Historial de versiones de notas + +### MVP-5 (Completo) + +**Sprint 1 - Confianza total:** +- Sistema de backup automático (IndexedDB) +- Política de retención (max 10 backups, 30 días) +- Restore con preview y validación +- Backup previo automático pre-destructivo +- Guard de cambios sin guardar + +**Sprint 2 - Flujo diario desde teclado:** +- Command Palette global (Ctrl+K / Cmd+K) +- Modelo de acciones para palette +- Shortcuts globales (g h, g n, n, /, ?) +- Navegación de listas por teclado (↑↓ Enter E F P) + +**Sprint 3 - Contexto y continuidad:** +- Sidebar contextual persistente mejorada +- Modo trabajo con toggle +- Autosave de borradores locales +- Historial de navegación contextual + +**Sprint 4 - Captura ubicua:** +- Bookmarklet para capturar desde cualquier web +- Página de confirmación de captura +- Endpoint seguro /api/capture con rate limiting + +**P2 - Exportación, Importación y Settings:** +- Exportación Markdown con frontmatter +- Exportación HTML legible +- Importador Markdown mejorado (frontmatter, tags, wiki links) +- Importador Obsidian-compatible +- Centro de respaldo en Settings +- Panel de preferencias (backup on/off, retención, work mode) +- Tests de command palette y captura +- Validaciones y límites (50MB backup, 10K notas, etc) + +## Algoritmo de Scoring + +```typescript +// Search scoring +score = baseScore + favoriteBoost(+2) + pinnedBoost(+1) + usageBoost + +// Related notes scoring +score = sameType(+3) + sharedTags(×3) + titleKeywords(max+3) + contentKeywords(max+2) + usageBoost + +// Centrality score +centrality = backlinks(×3) + outboundLinks(×1) + usageViews(×0.5) + coUsageWeight(×2) +``` + +## Atajos de Teclado + +| Atajo | Acción | +|-------|--------| +| `Ctrl+K` / `Cmd+K` | Command Palette | +| `g h` | Ir al Dashboard | +| `g n` | Ir a Notas | +| `n` | Nueva nota | +| `/` | Enfocar búsqueda | +| `?` | Mostrar ayuda | +| `↑↓` | Navegar listas | +| `Enter` | Abrir nota | +| `E` | Editar nota (en lista) | +| `F` | Favoritar nota (en lista) | +| `P` | Fijar nota (en lista) | + +## Tests + +**226 tests** cubriendo: +- API routes (CRUD, search, tags, versions) +- Search y scoring +- Query parser +- Notas relacionadas +- Backlinks +- Type inference +- Link suggestions +- Usage tracking +- Dashboard +- Version history +- Command items +- External capture +- Navigation history + +## Comandos + +```bash +npm run dev # Desarrollo +npm run build # Build producción +npm test # Tests (usar npx jest) +npx prisma db push # Sync schema +npx prisma studio # UI de BD +``` + +## Configuración de Feature Flags + +```bash +FLAG_CENTRALITY=true +FLAG_PASSIVE_RECOMMENDATIONS=true +FLAG_TYPE_SUGGESTIONS=true +FLAG_LINK_SUGGESTIONS=true +``` + +## Estados de Implementación + +| Feature | Estado | +|---------|--------| +| CRUD notas | ✅ | +| Tags | ✅ | +| Búsqueda avanzada | ✅ | +| Quick Add | ✅ | +| Backlinks [[wiki]] | ✅ | +| Usage tracking | ✅ | +| Dashboard inteligente | ✅ | +| Versiones de notas | ✅ | +| Command Palette | ✅ | +| Shortcuts globales | ✅ | +| Modo trabajo | ✅ | +| Backup/Restore | ✅ | +| Bookmarklet capture | ✅ | +| Export Markdown/HTML | ✅ | +| Import Markdown | ✅ | +| Settings completo | ✅ | +| Feature flags UI | ✅ | +| Tests | ✅ (226) | + +## Commits Recientes + +``` +e66a678 feat: MVP-5 P2 - Export/Import, Settings, Tests y Validaciones +8d56f34 feat: MVP-5 Sprint 4 - External Capture via Bookmarklet +a40ab18 feat: MVP-5 Sprint 3 - Sidebar, Work Mode, and Drafts +cde0a14 feat: MVP-5 Sprint 2 - Command Palette and Global Shortcuts +8c80a12 feat: MVP-5 Sprint 1 - Backup/Restore system +``` diff --git a/resumen/2026-03-22-2000-resumen.md b/resumen/2026-03-22-2000-resumen.md new file mode 100644 index 0000000..c24ad53 --- /dev/null +++ b/resumen/2026-03-22-2000-resumen.md @@ -0,0 +1,431 @@ +# Recall - Resumen Técnico Detallado + +## Información General + +**Nombre:** Recall +**Descripción:** Sistema de gestión de conocimiento personal (PKM) para captura y recuperación de notas, comandos, snippets y conocimiento técnico. +**Fecha de creación:** 2026-03-22 +**Estado:** MVP-5 Completo + +## Stack Tecnológico + +| Componente | Tecnología | Versión | +|------------|-------------|---------| +| Framework | Next.js + App Router + Turbopack | 16.2.1 | +| Base de datos | SQLite via Prisma ORM | 5.22.0 | +| Lenguaje | TypeScript | 5.x | +| UI | TailwindCSS + shadcn/ui | 4.x / latest | +| Testing | Jest | 30.3.0 | +| Notificaciones | Sonner (toasts) | latest | +| IDE | VSCode / Cursor | + +## Estructura del Proyecto + +``` +src/ +├── app/ +│ ├── api/ +│ │ ├── notes/ +│ │ │ ├── route.ts # GET, POST /api/notes +│ │ │ ├── [id]/route.ts # GET, PUT, DELETE /api/notes/:id +│ │ │ ├── quick/route.ts # POST /api/notes/quick +│ │ │ ├── links/route.ts # GET /api/notes/links +│ │ │ ├── suggest/route.ts # GET /api/notes/suggest +│ │ │ ├── backlinks/route.ts # GET /api/notes/:id/backlinks +│ │ │ └── versions/ +│ │ │ ├── route.ts # GET, POST /api/notes/:id/versions +│ │ │ └── [versionId]/route.ts # GET, PUT +│ │ ├── tags/ +│ │ │ ├── route.ts # GET /api/tags +│ │ │ └── suggest/route.ts # GET /api/tags/suggest +│ │ ├── search/route.ts # GET /api/search +│ │ ├── usage/ +│ │ │ ├── route.ts # GET /api/usage +│ │ │ └── co-usage/route.ts # GET /api/usage/co-usage +│ │ ├── metrics/route.ts # GET /api/metrics +│ │ ├── centrality/route.ts # GET /api/centrality +│ │ ├── export-import/route.ts # GET, POST +│ │ ├── import-markdown/route.ts # POST +│ │ └── capture/route.ts # POST /api/capture +│ ├── notes/[id]/page.tsx # Detalle de nota +│ ├── edit/[id]/page.tsx # Edición de nota +│ ├── new/page.tsx # Nueva nota +│ ├── capture/page.tsx # Confirmación de captura +│ ├── settings/page.tsx # Configuración +│ └── page.tsx # Dashboard (raíz) +├── components/ +│ ├── ui/ # Componentes shadcn/ui +│ ├── dashboard.tsx # Dashboard inteligente +│ ├── note-form.tsx # Formulario de notas +│ ├── note-card.tsx # Tarjeta de nota +│ ├── note-list.tsx # Lista de notas (grid) +│ ├── keyboard-navigable-note-list.tsx # Lista con navegación teclado +│ ├── note-connections.tsx # Panel de conexiones +│ ├── related-notes.tsx # Notas relacionadas +│ ├── version-history.tsx # Historial de versiones +│ ├── search-bar.tsx # Búsqueda en tiempo real +│ ├── command-palette.tsx # Command palette (Ctrl+K) +│ ├── keyboard-shortcuts-dialog.tsx # Diálogo de atajos +│ ├── shortcuts-provider.tsx # Provider de atajos +│ ├── keyboard-hint.tsx # Hint de atajos +│ ├── work-mode-toggle.tsx # Toggle modo trabajo +│ ├── draft-recovery-banner.tsx # Banner de recuperación +│ ├── backup-restore-dialog.tsx # Restore con preview +│ ├── backup-list.tsx # Lista de backups +│ ├── bookmarklet-instructions.tsx # Instrucciones bookmarklet +│ ├── recent-context-list.tsx # Historial de navegación +│ ├── track-navigation-history.tsx # Tracking de historial +│ ├── preferences-panel.tsx # Panel de preferencias +│ ├── markdown-content.tsx # Contenido con highlight +│ ├── quick-add.tsx # Captura rápida +│ └── track-note-view.tsx # Tracking de vistas +├── hooks/ +│ ├── use-global-shortcuts.ts # Atajos globales +│ ├── use-note-list-keyboard.ts # Navegación teclado +│ └── use-unsaved-changes.ts # Guard de cambios sin guardar +└── lib/ + ├── prisma.ts # Cliente Prisma + ├── search.ts # Búsqueda con scoring + ├── query-parser.ts # Parser de queries + ├── related.ts # Notas relacionadas + ├── backlinks.ts # Sistema de enlaces [[wiki]] + ├── tags.ts # Normalización y sugerencias + ├── usage.ts # Tracking de uso + ├── metrics.ts # Métricas de dashboard + ├── centrality.ts # Cálculo de centralidad + ├── type-inference.ts # Detección automática de tipo + ├── link-suggestions.ts # Sugerencias de enlaces + ├── features.ts # Feature flags + ├── validators.ts # Zod schemas + ├── errors.ts # Manejo de errores + ├── versions.ts # Historial de versiones + ├── backup.ts # Snapshot de backup + ├── backup-storage.ts # IndexedDB storage + ├── backup-policy.ts # Política de retención + ├── backup-validator.ts # Validación de backups + ├── restore.ts # Restore de backups + ├── drafts.ts # Borradores locales + ├── work-mode.ts # Modo trabajo + ├── navigation-history.ts # Historial de navegación + ├── export-markdown.ts # Exportación Markdown + ├── export-html.ts # Exportación HTML + ├── import-markdown.ts # Importador Markdown + ├── external-capture.ts # Captura externa + ├── templates.ts # Templates por tipo + ├── command-items.ts # Items de command palette + └── command-groups.ts # Grupos de comandos +``` + +## Modelos de Datos (Prisma) + +### Note +```prisma +model Note { + id String @id @default(cuid()) + title String + content String + type String @default("note") + isFavorite Boolean @default(false) + isPinned Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + creationSource String @default("form") +} +``` + +### NoteUsage +```prisma +model NoteUsage { + id String @id @default(cuid()) + noteId String + eventType String + query String? + createdAt DateTime @default(now()) +} +``` + +### NoteCoUsage +```prisma +model NoteCoUsage { + id String @id @default(cuid()) + fromNoteId String + toNoteId String + weight Int @default(1) +} +``` + +### NoteVersion +```prisma +model NoteVersion { + id String @id @default(cuid()) + noteId String + title String + content String + createdAt DateTime @default(now()) +} +``` + +### Backlink +```prisma +model Backlink { + sourceNoteId String + targetNoteId String +} +``` + +## APIs REST + +| Endpoint | Método | Descripción | +|----------|--------|-------------| +| `/api/notes` | GET, POST | Listar/crear notas | +| `/api/notes/[id]` | GET, PUT, DELETE | CRUD nota | +| `/api/notes/quick` | POST | Creación rápida | +| `/api/notes/links` | GET | Sugerencias de enlaces | +| `/api/notes/suggest` | GET | Sugerencias automática | +| `/api/notes/[id]/versions` | GET, POST | Versiones | +| `/api/notes/[id]/backlinks` | GET | Backlinks | +| `/api/search` | GET | Búsqueda avanzada | +| `/api/tags` | GET | Tags | +| `/api/tags/suggest` | GET | Sugerencias de tags | +| `/api/usage` | GET | Uso de notas | +| `/api/usage/co-usage` | GET | Co-uso entre notas | +| `/api/metrics` | GET | Métricas dashboard | +| `/api/centrality` | GET | Notas centrales | +| `/api/export-import` | GET, POST | Export/Import | +| `/api/import-markdown` | POST | Importar Markdown | +| `/api/capture` | POST | Captura externa | + +## Features Implementadas + +### MVP-1: Fundamentos +- [x] CRUD completo de notas +- [x] Sistema de tags +- [x] Búsqueda básica + +### MVP-2: Captura Inteligente +- [x] Búsqueda avanzada con scoring +- [x] Quick Add con prefijos (cmd:, snip:, etc.) +- [x] Backlinks con sintaxis [[wiki]] +- [x] Formularios guiados por tipo +- [x] Templates inteligentes por tipo +- [x] Vista command con copiar +- [x] Vista snippet con syntax highlight +- [x] Checklist interactivo en procedure + +### MVP-3: Uso y Contexto +- [x] Usage tracking (vistas, clics, copias) +- [x] Dashboard inteligente +- [x] Scoring boost basado en uso +- [x] Sugerencias automáticas de tags +- [x] Panel "Conectado con" +- [x] Quick Add multilínea +- [x] Pegado inteligente con detección de tipo +- [x] Sugerencia automática de tipo +- [x] Sugerencia de enlaces internos +- [x] Registro de co-uso entre notas +- [x] Métricas internas +- [x] Cálculo de notas centrales +- [x] Feature flags configurables + +### MVP-4: Query Parser y Navegación +- [x] Query parser (`type:`, `tag:`, `is:favorite`, `is:pinned`) +- [x] Búsqueda en tiempo real (300ms debounce) +- [x] Navegación por teclado (↑↓ Enter ESC) +- [x] Dropdown de resultados con cache +- [x] Sidebar contextual con co-uso +- [x] Historial de versiones +- [x] Preload de notas en hover + +### MVP-5: Flujo Diario y Portabilidad + +**Sprint 1 - Confianza:** +- [x] Sistema de backup automático (IndexedDB) +- [x] Política de retención (max 10 backups, 30 días) +- [x] Restore con preview y validación +- [x] Backup previo automático +- [x] Guard de cambios sin guardar + +**Sprint 2 - Shortcuts Globales:** +- [x] Command Palette (Ctrl+K / Cmd+K) +- [x] Shortcuts: g h, g n, n, /, ? +- [x] Navegación de listas por teclado + +**Sprint 3 - Contexto y Continuidad:** +- [x] Sidebar contextual persistente +- [x] Modo trabajo con toggle +- [x] Autosave de borradores locales +- [x] Historial de navegación contextual + +**Sprint 4 - Captura Ubicua:** +- [x] Bookmarklet para capturar desde web +- [x] Página de confirmación +- [x] Endpoint seguro con rate limiting + +**P2 - Exportación e Importación:** +- [x] Exportación Markdown con frontmatter +- [x] Exportación HTML legible +- [x] Importador Markdown mejorado +- [x] Centro de respaldo en Settings +- [x] Panel de preferencias +- [x] Validaciones y límites + +## Algoritmos de Scoring + +### Búsqueda +``` +score = baseScore + favoriteBoost(+2) + pinnedBoost(+1) + usageBoost +``` + +### Notas Relacionadas +``` +score = sameType(+3) + sharedTags(×3) + titleKeywords(max+3) + contentKeywords(max+2) + usageBoost +``` + +### Centralidad +``` +centrality = backlinks(×3) + outboundLinks(×1) + usageViews(×0.5) + coUsageWeight(×2) +``` + +## Atajos de Teclado + +| Atajo | Acción | +|-------|--------| +| `Ctrl+K` / `Cmd+K` | Command Palette | +| `g h` | Ir al Dashboard | +| `g n` | Ir a Notas | +| `n` | Nueva nota | +| `/` | Enfocar búsqueda | +| `?` | Mostrar ayuda | +| `↑↓` | Navegar listas | +| `Enter` | Abrir nota | +| `E` | Editar nota | +| `F` | Favoritar nota | +| `P` | Fijar nota | + +## Tipos de Nota + +| Tipo | Descripción | Color | +|------|-------------|-------| +| `note` | Nota general | Slate | +| `command` | Comando o snippet ejecutable | Green | +| `snippet` | Fragmento de código | Blue | +| `decision` | Decisión tomada | Purple | +| `recipe` | Receta o procedimiento | Orange | +| `procedure` | Procedimiento con checkboxes | Yellow | +| `inventory` | Inventario o lista | Gray | + +## Comandos npm + +```bash +npm run dev # Desarrollo (Turbopack) +npm run build # Build producción +npm run start # Iniciar producción +npm test # Tests (Jest) +npx jest --watch # Tests en watch mode +npx prisma db push # Sync schema a BD +npx prisma studio # UI de base de datos +npx prisma generate # Generar tipos +``` + +## Tests + +**226 tests** organizados en: +- `__tests__/api.*.test.ts` - Tests de integración de APIs +- `__tests__/search.test.ts` - Búsqueda y scoring +- `__tests__/query-parser.test.ts` - Parser de queries +- `__tests__/related.test.ts` - Notas relacionadas +- `__tests__/backlinks.test.ts` - Sistema de enlaces +- `__tests__/tags.test.ts` - Tags y sugerencias +- `__tests__/usage.test.ts` - Tracking de uso +- `__tests__/versions.test.ts` - Historial de versiones +- `__tests__/dashboard.test.ts` - Dashboard +- `__tests__/command-items.test.ts` - Command palette +- `__tests__/external-capture.test.ts` - Captura externa +- `__tests__/navigation-history.test.ts` - Historial +- `__tests__/link-suggestions.test.ts` - Sugerencias de enlaces +- `__tests__/type-inference.test.ts` - Inferencia de tipo +- `__tests__/quick-add.test.ts` - Quick Add + +## Feature Flags + +Configurables via `localStorage` o `.env`: + +```bash +FLAG_CENTRALITY=true # Habilitar centralidad +FLAG_PASSIVE_RECOMMENDATIONS=true # Recomendaciones pasivas +FLAG_TYPE_SUGGESTIONS=true # Sugerencias de tipo +FLAG_LINK_SUGGESTIONS=true # Sugerencias de enlaces +``` + +## Límites del Sistema + +| Recurso | Límite | +|---------|--------| +| Tamaño de backup | 50MB | +| Cantidad de notas | 10,000 | +| Longitud de título (captura) | 500 chars | +| Longitud de URL (captura) | 2000 chars | +| Longitud de selección (captura) | 10,000 chars | +| Backups retenidos | 10 máximo | +| Días de retención | 30 días | + +## Commits del Proyecto + +``` +33a4705 feat: MVP-4 P2 - Preload notes on hover +e66a678 feat: MVP-5 P2 - Export/Import, Settings, Tests y Validaciones +8d56f34 feat: MVP-5 Sprint 4 - External Capture via Bookmarklet +a40ab18 feat: MVP-5 Sprint 3 - Sidebar, Work Mode, and Drafts +cde0a14 feat: MVP-5 Sprint 2 - Command Palette and Global Shortcuts +8c80a12 feat: MVP-5 Sprint 1 - Backup/Restore system +6694bce mvp +af0910f feat: initial commit +f2e4706 Initial commit +``` + +## Dependencias Principales + +```json +{ + "next": "16.2.1", + "@prisma/client": "5.22.0", + "prisma": "5.22.0", + "sonner": "latest", + "zod": "latest", + "tailwindcss": "4.x", + "@radix-ui/react-*": "latest" +} +``` + +## Patrones de Diseño + +- **Server Components** para páginas estáticas +- **Client Components** para interactividad +- **Hooks personalizados** para lógica reutilizable +- **Feature Flags** para features opcionales +- **IndexedDB** para persistencia local de backups +- **localStorage** para preferencias y borradores + +## Estado de Implementación + +| Feature | Estado | +|---------|--------| +| CRUD notas | ✅ | +| Tags | ✅ | +| Búsqueda avanzada | ✅ | +| Quick Add | ✅ | +| Backlinks [[wiki]] | ✅ | +| Usage tracking | ✅ | +| Dashboard inteligente | ✅ | +| Versiones de notas | ✅ | +| Command Palette | ✅ | +| Shortcuts globales | ✅ | +| Modo trabajo | ✅ | +| Backup/Restore | ✅ | +| Bookmarklet capture | ✅ | +| Export Markdown/HTML | ✅ | +| Import Markdown | ✅ | +| Settings completo | ✅ | +| Feature flags UI | ✅ | +| Tests | ✅ (226) | +| Preload on hover | ✅ | diff --git a/src/components/note-connections.tsx b/src/components/note-connections.tsx index 2e5fa0a..bd3f10d 100644 --- a/src/components/note-connections.tsx +++ b/src/components/note-connections.tsx @@ -98,6 +98,16 @@ function ConnectionGroup({ ) } +// Deduplicate notes by id, keeping first occurrence +function deduplicateById(items: T[]): T[] { + const seen = new Set() + return items.filter(item => { + if (seen.has(item.id)) return false + seen.add(item.id) + return true + }) +} + export function NoteConnections({ noteId, backlinks, @@ -123,6 +133,13 @@ export function NoteConnections({ const hasAnyConnections = backlinks.length > 0 || outgoingLinks.length > 0 || relatedNotes.length > 0 || coUsedNotes.length > 0 + // Deduplicate all lists to prevent React key warnings + const uniqueBacklinks = deduplicateById(backlinks.map((bl) => ({ id: bl.sourceNote.id, title: bl.sourceNote.title, type: bl.sourceNote.type }))) + const uniqueOutgoing = deduplicateById(outgoingLinks.map((ol) => ({ id: ol.sourceNote.id, title: ol.sourceNote.title, type: ol.sourceNote.type }))) + const uniqueRelated = deduplicateById(relatedNotes.map((rn) => ({ id: rn.id, title: rn.title, type: rn.type }))) + const uniqueCoUsed = deduplicateById(coUsedNotes.map((cu) => ({ id: cu.noteId, title: cu.title, type: cu.type }))) + const uniqueHistory = deduplicateById(navigationHistory.slice(0, 5).map((entry) => ({ id: entry.noteId, title: entry.title, type: entry.type }))) + const toggleCollapsed = (key: string) => { setCollapsed((prev) => ({ ...prev, [key]: !prev[key] })) } @@ -144,11 +161,7 @@ export function NoteConnections({ ({ - id: bl.sourceNote.id, - title: bl.sourceNote.title, - type: bl.sourceNote.type, - }))} + notes={uniqueBacklinks} emptyMessage="Ningún otro documento enlaza a esta nota" isCollapsed={collapsed['backlinks']} onToggle={() => toggleCollapsed('backlinks')} @@ -158,11 +171,7 @@ export function NoteConnections({ ({ - id: ol.sourceNote.id, - title: ol.sourceNote.title, - type: ol.sourceNote.type, - }))} + notes={uniqueOutgoing} emptyMessage="Esta nota no enlaza a ningún otro documento" isCollapsed={collapsed['outgoing']} onToggle={() => toggleCollapsed('outgoing')} @@ -172,11 +181,7 @@ export function NoteConnections({ ({ - id: rn.id, - title: rn.title, - type: rn.type, - }))} + notes={uniqueRelated} emptyMessage="No hay notas relacionadas" isCollapsed={collapsed['related']} onToggle={() => toggleCollapsed('related')} @@ -186,11 +191,7 @@ export function NoteConnections({ ({ - id: cu.noteId, - title: cu.title, - type: cu.type, - }))} + notes={uniqueCoUsed} emptyMessage="No hay notas co-usadas" isCollapsed={collapsed['coused']} onToggle={() => toggleCollapsed('coused')} @@ -214,15 +215,11 @@ export function NoteConnections({ )} {/* Navigation history */} - {navigationHistory.length > 0 && ( + {uniqueHistory.length > 0 && ( ({ - id: entry.noteId, - title: entry.title, - type: entry.type, - }))} + notes={uniqueHistory} emptyMessage="No hay historial de navegación" isCollapsed={collapsed['history']} onToggle={() => toggleCollapsed('history')} diff --git a/src/components/note-form.tsx b/src/components/note-form.tsx index 50100b3..39a5ea2 100644 --- a/src/components/note-form.tsx +++ b/src/components/note-form.tsx @@ -816,7 +816,8 @@ export function NoteForm({ initialData, isEdit = false }: NoteFormProps) { e.preventDefault() setIsSubmitting(true) - const noteData = { + // Build payload, explicitly excluding id and any undefined values + const noteData: Record = { title, content, type, @@ -825,6 +826,13 @@ export function NoteForm({ initialData, isEdit = false }: NoteFormProps) { tags, } + // Remove undefined values before sending + Object.keys(noteData).forEach(key => { + if (noteData[key] === undefined) { + delete noteData[key] + } + }) + try { const url = isEdit && initialData ? `/api/notes/${initialData.id}` : '/api/notes' const method = isEdit ? 'PUT' : 'POST' diff --git a/src/components/work-mode-toggle.tsx b/src/components/work-mode-toggle.tsx index a0186a1..1973303 100644 --- a/src/components/work-mode-toggle.tsx +++ b/src/components/work-mode-toggle.tsx @@ -9,13 +9,22 @@ export function WorkModeToggle() { useEffect(() => { setEnabled(getWorkMode()) + + const handlePreferencesChange = () => { + // Re-read work mode state when preferences change + setEnabled(getWorkMode()) + } + + window.addEventListener('preferences-updated', handlePreferencesChange) + return () => window.removeEventListener('preferences-updated', handlePreferencesChange) }, []) const toggle = () => { const newValue = !enabled setEnabled(newValue) setWorkMode(newValue) - // Could dispatch custom event for other components to listen + // Dispatch event so other components know work mode changed + window.dispatchEvent(new CustomEvent('work-mode-changed', { detail: { enabled: newValue } })) } return ( diff --git a/src/hooks/use-global-shortcuts.ts b/src/hooks/use-global-shortcuts.ts index 1916479..49d4561 100644 --- a/src/hooks/use-global-shortcuts.ts +++ b/src/hooks/use-global-shortcuts.ts @@ -27,7 +27,7 @@ export function useGlobalShortcuts() { } // Handle ? for help - if (e.key === '?' && !e.shiftKey) { + if (e.key === '?' && e.shiftKey) { e.preventDefault() setShowHelp(true) return diff --git a/src/lib/navigation-history.ts b/src/lib/navigation-history.ts index 2dc6d11..606cdca 100644 --- a/src/lib/navigation-history.ts +++ b/src/lib/navigation-history.ts @@ -13,7 +13,14 @@ export function getNavigationHistory(): NavigationEntry[] { try { const stored = localStorage.getItem(NAVIGATION_HISTORY_KEY) if (!stored) return [] - return JSON.parse(stored) + const entries: NavigationEntry[] = JSON.parse(stored) + // Deduplicate by noteId, keeping the first occurrence (most recent) + const seen = new Set() + return entries.filter(entry => { + if (seen.has(entry.noteId)) return false + seen.add(entry.noteId) + return true + }) } catch { return [] } diff --git a/src/lib/usage.ts b/src/lib/usage.ts index 900ac5b..0cf2b8a 100644 --- a/src/lib/usage.ts +++ b/src/lib/usage.ts @@ -176,12 +176,18 @@ export async function getCoUsedNotes( updatedAt: { gte: since }, }, orderBy: { weight: 'desc' }, - take: limit, + take: limit * 2, // Fetch more to account for duplicates we'll filter }) + // Deduplicate by relatedNoteId - only keep highest weight per note + const seenIds = new Set() const result: { noteId: string; title: string; type: string; weight: number }[] = [] + for (const cu of coUsages) { const relatedNoteId = cu.fromNoteId === noteId ? cu.toNoteId : cu.fromNoteId + if (seenIds.has(relatedNoteId)) continue + seenIds.add(relatedNoteId) + const note = await prisma.note.findUnique({ where: { id: relatedNoteId }, select: { id: true, title: true, type: true }, @@ -194,6 +200,7 @@ export async function getCoUsedNotes( weight: cu.weight, }) } + if (result.length >= limit) break } return result } catch { diff --git a/src/lib/validators.ts b/src/lib/validators.ts index 7c95744..750b739 100644 --- a/src/lib/validators.ts +++ b/src/lib/validators.ts @@ -4,8 +4,9 @@ export const NoteTypeEnum = z.enum(['command', 'snippet', 'decision', 'recipe', export const CreationSourceEnum = z.enum(['form', 'quick', 'import']) -export const noteSchema = z.object({ - id: z.string().optional(), +// Base note schema without transform - for use with partial() +const baseNoteSchema = z.object({ + id: z.string().optional().nullable(), title: z.string().min(1, 'Title is required').max(200), content: z.string().min(1, 'Content is required'), type: NoteTypeEnum.default('note'), @@ -15,10 +16,18 @@ export const noteSchema = z.object({ creationSource: CreationSourceEnum.default('form'), }) -export const updateNoteSchema = noteSchema.partial().extend({ - id: z.string(), +// Transform to remove id if null/undefined (for creation) +export const noteSchema = baseNoteSchema.transform(data => { + if (data.id == null) { + const { id, ...rest } = data + return rest + } + return data }) +// For update, use partial of base schema with optional id (id comes from URL path, not body) +export const updateNoteSchema = baseNoteSchema.partial() + export const searchSchema = z.object({ q: z.string().optional(), type: NoteTypeEnum.optional(),