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
This commit is contained in:
@@ -27,3 +27,7 @@ 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.
|
- 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.
|
- 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.
|
- 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 <noreply@anthropic.com>
|
||||||
126
Jenkinsfile
vendored
Normal file
126
Jenkinsfile
vendored
Normal file
@@ -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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
342
resumen/2026-03-22-1942-resumen.md
Normal file
342
resumen/2026-03-22-1942-resumen.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
431
resumen/2026-03-22-2000-resumen.md
Normal file
431
resumen/2026-03-22-2000-resumen.md
Normal file
@@ -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 | ✅ |
|
||||||
@@ -98,6 +98,16 @@ function ConnectionGroup({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deduplicate notes by id, keeping first occurrence
|
||||||
|
function deduplicateById<T extends { id: string }>(items: T[]): T[] {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return items.filter(item => {
|
||||||
|
if (seen.has(item.id)) return false
|
||||||
|
seen.add(item.id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function NoteConnections({
|
export function NoteConnections({
|
||||||
noteId,
|
noteId,
|
||||||
backlinks,
|
backlinks,
|
||||||
@@ -123,6 +133,13 @@ export function NoteConnections({
|
|||||||
const hasAnyConnections =
|
const hasAnyConnections =
|
||||||
backlinks.length > 0 || outgoingLinks.length > 0 || relatedNotes.length > 0 || coUsedNotes.length > 0
|
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) => {
|
const toggleCollapsed = (key: string) => {
|
||||||
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }))
|
setCollapsed((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||||
}
|
}
|
||||||
@@ -144,11 +161,7 @@ export function NoteConnections({
|
|||||||
<ConnectionGroup
|
<ConnectionGroup
|
||||||
title="Enlaces entrantes"
|
title="Enlaces entrantes"
|
||||||
icon={ExternalLink}
|
icon={ExternalLink}
|
||||||
notes={backlinks.map((bl) => ({
|
notes={uniqueBacklinks}
|
||||||
id: bl.sourceNote.id,
|
|
||||||
title: bl.sourceNote.title,
|
|
||||||
type: bl.sourceNote.type,
|
|
||||||
}))}
|
|
||||||
emptyMessage="Ningún otro documento enlaza a esta nota"
|
emptyMessage="Ningún otro documento enlaza a esta nota"
|
||||||
isCollapsed={collapsed['backlinks']}
|
isCollapsed={collapsed['backlinks']}
|
||||||
onToggle={() => toggleCollapsed('backlinks')}
|
onToggle={() => toggleCollapsed('backlinks')}
|
||||||
@@ -158,11 +171,7 @@ export function NoteConnections({
|
|||||||
<ConnectionGroup
|
<ConnectionGroup
|
||||||
title="Enlaces salientes"
|
title="Enlaces salientes"
|
||||||
icon={ArrowRight}
|
icon={ArrowRight}
|
||||||
notes={outgoingLinks.map((ol) => ({
|
notes={uniqueOutgoing}
|
||||||
id: ol.sourceNote.id,
|
|
||||||
title: ol.sourceNote.title,
|
|
||||||
type: ol.sourceNote.type,
|
|
||||||
}))}
|
|
||||||
emptyMessage="Esta nota no enlaza a ningún otro documento"
|
emptyMessage="Esta nota no enlaza a ningún otro documento"
|
||||||
isCollapsed={collapsed['outgoing']}
|
isCollapsed={collapsed['outgoing']}
|
||||||
onToggle={() => toggleCollapsed('outgoing')}
|
onToggle={() => toggleCollapsed('outgoing')}
|
||||||
@@ -172,11 +181,7 @@ export function NoteConnections({
|
|||||||
<ConnectionGroup
|
<ConnectionGroup
|
||||||
title="Relacionadas"
|
title="Relacionadas"
|
||||||
icon={RefreshCw}
|
icon={RefreshCw}
|
||||||
notes={relatedNotes.map((rn) => ({
|
notes={uniqueRelated}
|
||||||
id: rn.id,
|
|
||||||
title: rn.title,
|
|
||||||
type: rn.type,
|
|
||||||
}))}
|
|
||||||
emptyMessage="No hay notas relacionadas"
|
emptyMessage="No hay notas relacionadas"
|
||||||
isCollapsed={collapsed['related']}
|
isCollapsed={collapsed['related']}
|
||||||
onToggle={() => toggleCollapsed('related')}
|
onToggle={() => toggleCollapsed('related')}
|
||||||
@@ -186,11 +191,7 @@ export function NoteConnections({
|
|||||||
<ConnectionGroup
|
<ConnectionGroup
|
||||||
title="Co-usadas"
|
title="Co-usadas"
|
||||||
icon={Users}
|
icon={Users}
|
||||||
notes={coUsedNotes.map((cu) => ({
|
notes={uniqueCoUsed}
|
||||||
id: cu.noteId,
|
|
||||||
title: cu.title,
|
|
||||||
type: cu.type,
|
|
||||||
}))}
|
|
||||||
emptyMessage="No hay notas co-usadas"
|
emptyMessage="No hay notas co-usadas"
|
||||||
isCollapsed={collapsed['coused']}
|
isCollapsed={collapsed['coused']}
|
||||||
onToggle={() => toggleCollapsed('coused')}
|
onToggle={() => toggleCollapsed('coused')}
|
||||||
@@ -214,15 +215,11 @@ export function NoteConnections({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Navigation history */}
|
{/* Navigation history */}
|
||||||
{navigationHistory.length > 0 && (
|
{uniqueHistory.length > 0 && (
|
||||||
<ConnectionGroup
|
<ConnectionGroup
|
||||||
title="Vista recientemente"
|
title="Vista recientemente"
|
||||||
icon={Clock}
|
icon={Clock}
|
||||||
notes={navigationHistory.slice(0, 5).map((entry) => ({
|
notes={uniqueHistory}
|
||||||
id: entry.noteId,
|
|
||||||
title: entry.title,
|
|
||||||
type: entry.type,
|
|
||||||
}))}
|
|
||||||
emptyMessage="No hay historial de navegación"
|
emptyMessage="No hay historial de navegación"
|
||||||
isCollapsed={collapsed['history']}
|
isCollapsed={collapsed['history']}
|
||||||
onToggle={() => toggleCollapsed('history')}
|
onToggle={() => toggleCollapsed('history')}
|
||||||
|
|||||||
@@ -816,7 +816,8 @@ export function NoteForm({ initialData, isEdit = false }: NoteFormProps) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
|
|
||||||
const noteData = {
|
// Build payload, explicitly excluding id and any undefined values
|
||||||
|
const noteData: Record<string, unknown> = {
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
type,
|
type,
|
||||||
@@ -825,6 +826,13 @@ export function NoteForm({ initialData, isEdit = false }: NoteFormProps) {
|
|||||||
tags,
|
tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove undefined values before sending
|
||||||
|
Object.keys(noteData).forEach(key => {
|
||||||
|
if (noteData[key] === undefined) {
|
||||||
|
delete noteData[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = isEdit && initialData ? `/api/notes/${initialData.id}` : '/api/notes'
|
const url = isEdit && initialData ? `/api/notes/${initialData.id}` : '/api/notes'
|
||||||
const method = isEdit ? 'PUT' : 'POST'
|
const method = isEdit ? 'PUT' : 'POST'
|
||||||
|
|||||||
@@ -9,13 +9,22 @@ export function WorkModeToggle() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEnabled(getWorkMode())
|
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 toggle = () => {
|
||||||
const newValue = !enabled
|
const newValue = !enabled
|
||||||
setEnabled(newValue)
|
setEnabled(newValue)
|
||||||
setWorkMode(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 (
|
return (
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function useGlobalShortcuts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle ? for help
|
// Handle ? for help
|
||||||
if (e.key === '?' && !e.shiftKey) {
|
if (e.key === '?' && e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setShowHelp(true)
|
setShowHelp(true)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ export function getNavigationHistory(): NavigationEntry[] {
|
|||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(NAVIGATION_HISTORY_KEY)
|
const stored = localStorage.getItem(NAVIGATION_HISTORY_KEY)
|
||||||
if (!stored) return []
|
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<string>()
|
||||||
|
return entries.filter(entry => {
|
||||||
|
if (seen.has(entry.noteId)) return false
|
||||||
|
seen.add(entry.noteId)
|
||||||
|
return true
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,12 +176,18 @@ export async function getCoUsedNotes(
|
|||||||
updatedAt: { gte: since },
|
updatedAt: { gte: since },
|
||||||
},
|
},
|
||||||
orderBy: { weight: 'desc' },
|
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<string>()
|
||||||
const result: { noteId: string; title: string; type: string; weight: number }[] = []
|
const result: { noteId: string; title: string; type: string; weight: number }[] = []
|
||||||
|
|
||||||
for (const cu of coUsages) {
|
for (const cu of coUsages) {
|
||||||
const relatedNoteId = cu.fromNoteId === noteId ? cu.toNoteId : cu.fromNoteId
|
const relatedNoteId = cu.fromNoteId === noteId ? cu.toNoteId : cu.fromNoteId
|
||||||
|
if (seenIds.has(relatedNoteId)) continue
|
||||||
|
seenIds.add(relatedNoteId)
|
||||||
|
|
||||||
const note = await prisma.note.findUnique({
|
const note = await prisma.note.findUnique({
|
||||||
where: { id: relatedNoteId },
|
where: { id: relatedNoteId },
|
||||||
select: { id: true, title: true, type: true },
|
select: { id: true, title: true, type: true },
|
||||||
@@ -194,6 +200,7 @@ export async function getCoUsedNotes(
|
|||||||
weight: cu.weight,
|
weight: cu.weight,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (result.length >= limit) break
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ export const NoteTypeEnum = z.enum(['command', 'snippet', 'decision', 'recipe',
|
|||||||
|
|
||||||
export const CreationSourceEnum = z.enum(['form', 'quick', 'import'])
|
export const CreationSourceEnum = z.enum(['form', 'quick', 'import'])
|
||||||
|
|
||||||
export const noteSchema = z.object({
|
// Base note schema without transform - for use with partial()
|
||||||
id: z.string().optional(),
|
const baseNoteSchema = z.object({
|
||||||
|
id: z.string().optional().nullable(),
|
||||||
title: z.string().min(1, 'Title is required').max(200),
|
title: z.string().min(1, 'Title is required').max(200),
|
||||||
content: z.string().min(1, 'Content is required'),
|
content: z.string().min(1, 'Content is required'),
|
||||||
type: NoteTypeEnum.default('note'),
|
type: NoteTypeEnum.default('note'),
|
||||||
@@ -15,10 +16,18 @@ export const noteSchema = z.object({
|
|||||||
creationSource: CreationSourceEnum.default('form'),
|
creationSource: CreationSourceEnum.default('form'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateNoteSchema = noteSchema.partial().extend({
|
// Transform to remove id if null/undefined (for creation)
|
||||||
id: z.string(),
|
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({
|
export const searchSchema = z.object({
|
||||||
q: z.string().optional(),
|
q: z.string().optional(),
|
||||||
type: NoteTypeEnum.optional(),
|
type: NoteTypeEnum.optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user