CreaRack-SL

Cuaderno — Multiusuario y Sharing Colaborativo

Cuaderno — Multiusuario y Sharing Colaborativo

Introducida en el commit ab1c02f (2026-05-20), esta feature convierte el Cuaderno de un módulo mono-usuario a un módulo colaborativo con control de acceso por nota, detección de edición concurrente y adjuntos de imagen.

Modelo de acceso (sharing)

Cada notebook_note tiene el campo shared_with TEXT NOT NULL DEFAULT '' con tres semánticas:

Valor de shared_withSignificado
'' (cadena vacía)Privada — solo el owner_id puede leer y editar
'ALL'Todo el equipo — cualquier miembro del staff con sesión activa
'a@x.com,b@y.com'CSV de emails — solo los destinatarios explícitos

Seguridad de la búsqueda LIKE: el filtro shared_with LIKE '%email%' es seguro con emails @esfericlabs.com porque no hay colisiones de substrings en el staff actual. Si se incorporara staff externo, revisar.

Reglas de autorización

  • Lectura: owner OR shared_with = 'ALL' OR shared_with LIKE '%email%'.
  • Edición (PUT): owner OR cualquier collaborador listado. Los collaboradores no pueden cambiar el shared_with — campo restringido al owner.
  • Borrado (DELETE): exclusivo del owner. Los collaboradores solo leen y editan.
  • Upload de imágenes: owner OR collaborador.

Cambios de esquema (Migración 0032)

Archivo: migrations/0032_notebook_sharing.sql

ALTER TABLE notebook_notes ADD COLUMN shared_with TEXT NOT NULL DEFAULT '';
ALTER TABLE notebook_notes ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
CREATE INDEX IF NOT EXISTS idx_notebook_shared_with ON notebook_notes(shared_with);

El campo version se incrementa con version = version + 1 en cada PUT, lo que permite al frontend detectar saves concurrentes mediante polling ligero.

Endpoints afectados / nuevos

GET /api/notebook (modificado)

  • Antes: devolvía solo notas propias (WHERE owner_id = ?).
  • Ahora: devolvía propias + compartidas (WHERE owner_id = ?1 OR shared_with = 'ALL' OR shared_with LIKE ?2).
  • Nuevos campos en la respuesta: shared_with, version.

GET /api/notebook/[id] (refactorizado)

Movido de functions/api/notebook/[id].ts a functions/api/notebook/[id]/index.ts.
loadOwned → loadAccessible: usa hasAccess() en lugar de filtrar solo por owner_id.

PUT /api/notebook/[id] (modificado)

  • Acepta nuevo campo shared_with (solo ejecutado si me === existing.owner_id).
  • Incrementa version en cada save.
  • El log de actividad ahora también registra cambios de shared_with.

GET /api/notebook/[id]/version (nuevo)

Polling endpoint ligero. Devuelve solo { version, updated_at }. Usado por el editor cada 30 s en notas compartidas para mostrar el banner de edición concurrente.

POST /api/notebook/[id]/images (nuevo)

Upload multipart a R2. Restricciones: 5 MB máx, tipos permitidos image/png | jpeg | webp | gif. Guarda fila en notebook_attachments y devuelve { id, url, filename, content_type }.

GET /api/notebook-images/[id] (nuevo)

Stream del objeto R2. Verifica acceso a la nota padre antes de servir. Headers: Cache-Control: private, max-age=3600.

Cambios en frontend

Toolbar — 7 nuevas funciones

BotónAcción
↶ Undoundo de @codemirror/commands
↷ Redoredo de @codemirror/commands
AaToggle spellcheck/autocorrect/autocapitalize vía Compartment (persistido en localStorage)
⚠Inserta !!texto!! → renderizado como <mark class="urgent"> (bg rojo translúcido + border izq rojo)
🖼File input → POST /api/notebook/[id]/images → inserta ![alt](/api/notebook-images/N)
😊Abre EmojiPicker custom (~100 emojis, 6 categorías, búsqueda, sin deps externas)
CompartirShareDropdown (Privada / Todo el equipo / multi-select). Solo visible al owner.

En notas compartidas el editor hace polling cada 30 s a /api/notebook/[id]/version. Si version remota > version local, muestra banner amarillo:
“X editó esta nota hace Nm. Refrescar / Ignorar”

NoteRow en sidebar

  • Badge ”👤 Nombre” con border izquierdo cyan si la nota es ajena (shared recibida).
  • Badge ”🔗” si es propia y está compartida.
  • Acciones duplicar/borrar solo visibles para el owner.

NoteEditor meta header

Muestra "Compartida por X" (notas ajenas) o el label "Compartida · Edu, Dani" (propias compartidas).

Dependencias técnicas

  • R2 bucket: TASK_ATTACHMENTS (shared con task-attachments, prefijo notebook/).
  • D1: tabla notebook_attachments (ver [[entity—cuaderno—endpoint—notebook-attachments]]).
  • resolveActorEmail: todos los handlers migran de request.headers.get('cf-access-authenticated-user-email') a resolveActorEmail(request) de _lib/staff.
  • @codemirror/commands: ya era dependencia del editor; se exponen undo/redo.

Limitaciones conocidas

  • El filtro LIKE '%email%' no escala a equipos grandes o con emails con substrings comunes. Solución futura: tabla pivot notebook_shares.
  • No hay resolución de conflictos merge: el último en guardar gana. El banner es solo aviso.
  • EmojiPicker busca solo por nombre de categoría, no por nombre de emoji.

Véase también

  • [[entity—cuaderno—endpoint—notebook-attachments]]
  • [[entity—cuaderno—endpoint—notebook-version]]