Alfred es un gestor personal de tareas (kanban + recordatorios + foco) pensado para usar uno mismo, sin equipos, sin SaaS. Dos clientes hablan con el mismo backend:
- Web SPA instalable (React + Vite + PWA) — la interfaz principal. Se puede instalar como app desde Chrome / Edge / Safari en escritorio y móvil; Web Push para recordatorios funciona con la pestaña cerrada.
- CLI
alfred(Python + Typer) — automatización, scripting, agentes IA.
Despliegue en Docker o detrás de Tailscale. Autenticación con Google, email/contraseña, passkeys (WebAuthn) o Personal Access Tokens.
- Jerarquía Usuario → Contexto → Tablero → Columna → Tarea para separar vida personal y trabajo. Cada contexto tiene su color y su cuenta de Google asociada (opcional).
- Tableros con icono emoji personalizable, drag-and-drop de columnas, ancho de columna dinámico (1 lista → 2× ancho; muchas listas → scroll horizontal con
minWidth: 0a través del árbol de flex). - Columnas renombrables in-place, reordenables por drag-and-drop.
- Tareas con título, descripción Markdown (GFM), prioridad (tinta de fondo por nivel), fecha límite, tags, requester ("Encargada por"), assignees ("Asignado a"), subtareas anidadas, recordatorios y adjuntos.
- Subtareas como tareas de pleno derecho con
parent_task_id. Para anidar tareas existentes: Shift+drag sobre otra tarjeta, o la fila "Subtarea de" en el modal. - Mover tareas entre tableros del mismo contexto desde el detalle.
- Archivado por tarea o batch por columna; vista de archivadas accesible desde el menú "⋯".
- Búsqueda / filtros por texto (título, descripción, requester, assignees), tags, fechas y personas. Chips activos visibles bajo la barra.
- Sincronización con Google People por cuenta conectada (scope
contacts.readonly). Los contactos quedan scoped algoogle_account_idpara que un mismo email pueda existir en cuentas Personal y Trabajo. - Contacto "Yo mismo" auto-creado por usuario, con foto del primer Google account, no eliminable, assignee por defecto.
- Resync idempotente: deduplica por
google_contact_idy reclama huérfanos. - Buscador y filtro de favoritos en la pantalla de gestión (útil con >400 contactos).
- Selector con búsqueda (dropdown + dialog) al asignar contactos a tareas — favoritos al tope, búsqueda por nombre / email.
- Web Push con VAPID + Service Worker. Las claves VAPID se persisten como base64url DER PKCS8 (formato a prueba de cambios de py_vapid) en
data/vapid_keys.jsony se autogeneran si no existen. - El dispatcher es robusto: una suscripción muerta no impide la entrega al resto. Mantiene también un canal opcional FCM (legado del cliente Android nativo, ahora descartado — el código sigue en el backend por si vuelve un cliente móvil).
- Recordatorios con presets ("dentro de X minutos", "mañana 09:00") o picker custom. Funcionan cross-device: lo creas en cualquier cliente, llega a todos los demás.
- Web: drag-and-drop, paste desde portapapeles, click-to-pick. Imágenes con thumbnail + lightbox; documentos con badge 📎.
- Móvil: file picker del sistema desde el detalle de tarea.
- Almacenamiento local en
data/attachments/<uuid>.<ext>. Cap 15 MB por archivo.
- Pomodoro (15/25/50 min) vinculado a tarea concreta. Floating overlay minimizable.
- Estadísticas: tiempo total + sesiones completadas + gráfica de barras diarias. Agregadas server-side, consistentes entre dispositivos.
- Email + contraseña local (bcrypt + JWT).
- OAuth2 con Google (sign-in + connect-additional-account).
- Passkeys / WebAuthn (discoverable credentials — sin email; el navegador ofrece directamente las passkeys registradas).
- Personal Access Tokens (
alfred_pat_*) para clientes headless (CLI, automations, agentes IA). - Auto-redirect a login al caducar sesión: cualquier 401 mid-session en endpoints autenticados limpia el token y vuelve al formulario, sin pantallas zombies ni hard refresh.
- Manifest + Service Worker hacen que el SPA se instale como app desde la barra de direcciones de Chrome / Edge (escritorio o móvil) y "Add to Home Screen" en Safari.
- Web Push entrega recordatorios incluso con la pestaña cerrada.
- Quick Capture con
Alt + Qpara crear una tarea desde cualquier pantalla. - Auto-save del modal de tarea (sin botón "Guardar");
Ctrl/Cmd + Enterconfirma y cierra. - Esc cierra cualquier diálogo.
- Back del navegador integrado: la flecha atrás / gesto edge-swipe en Chrome mueven entre contextos y tableros previos en lugar de salir del SPA.
- Avatar + nombre real del usuario en la sidebar, tomados del primer Google account conectado (fallback al email).
- Python 3.12, FastAPI, Pydantic v2 (
UtcDatetimeannotated type → siempre serializa con sufijoZ). - SQLAlchemy 2 async con aiosqlite. Migraciones idempotentes en lifespan (PRAGMA + ALTER TABLE + table rebuild cuando hace falta quitar constraints anónimos).
- PyJWT + bcrypt para JWT y password hashing.
- pywebpush + VAPID para notificaciones del navegador.
- firebase-admin para FCM (push al móvil).
- webauthn (
>=2.5) para passkeys. - google-auth + verificación remota de ID tokens.
- uv para gestión de deps y empaquetado.
- React 19, Vite 8, TypeScript estricto.
- CSS vanilla (variables HSL + glassmorphism).
- react-markdown + remark-gfm para descripciones.
- Service Worker en
frontend/public/sw.js(Web Push + clic en notificación + fetch passthrough para installability). - PWA:
frontend/public/manifest.webmanifest+ iconos 192/512px. - WebAuthn helpers propios en
src/services/webauthn.ts(base64url ↔ ArrayBuffer + flujos).
- Python 3.11+, uv, Typer, httpx, questionary (pickers), tomli-w.
- Docker Compose para dev y producción.
- nginx + certbot delante para HTTPS público (script
deploy/deploy.sh). - Tailscale serve para HTTPS dentro del tailnet.
- systemd unit en
systemd/para arranque automático.
alfred/
├── backend/ FastAPI + SQLAlchemy
│ ├── app/
│ │ ├── api/ Endpoints (auth, boards, tasks, attachments, passkeys, ...)
│ │ ├── core/ OAuth, push (Web/FCM), time, self_contact
│ │ ├── models/ SQLAlchemy mappers
│ │ └── schemas/ Pydantic v2 in/out
│ ├── data/ SQLite + attachments + vapid_keys.json + fcm_service_account.json (gitignored)
│ ├── scripts/ Mantenimiento (cleanup_google_contacts.py)
│ └── tests/ 124 tests (pytest-asyncio)
├── frontend/ React + Vite SPA (PWA)
│ ├── src/components/ Sidebar, KanbanBoard, Auth, TokensSettings, ...
│ ├── src/hooks/ useEscapeKey, useAuthedImage
│ ├── src/services/ api.ts (single-origin /api/*), webauthn.ts, push.ts
│ └── public/ manifest.webmanifest + sw.js + logo-{192,512}.png
├── cli/ Cliente CLI (uv project)
│ ├── pyproject.toml
│ ├── src/alfred_cli/
│ └── README.md
├── deploy/ Despliegue público
│ ├── deploy.sh Script idempotente (nginx + certbot + compose up)
│ └── nginx/ Vhost de ejemplo
├── data/ Bind-mount para attachments/db en producción
├── docker-compose.yml
├── systemd/ Unit + script de instalación
├── AGENTS.md Notas para agentes IA
└── README.md
Ruta más corta para tener Alfred corriendo en tu portátil. No necesitas cuenta de Google, ni dominio, ni nada externo. Sólo Docker.
- Docker + Docker Compose instalados. En Linux:
En macOS / Windows: instalar Docker Desktop.
curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER && newgrp docker
- git y openssl (vienen ya en casi todo).
git clone https://github.com/diegosuarez/alfred.git
cd alfred
cp backend/.env.example backend/.envÁbrelo y rellena una sola línea — el resto se puede dejar como está para arrancar en local:
# Genera un secreto aleatorio y reemplaza el valor de JWT_SECRET
openssl rand -hex 32
# Pega el resultado en backend/.env → JWT_SECRET=<lo-que-salga>
JWT_SECRETes obligatorio: si falta, el backend se niega a arrancar (es lo que firma las sesiones).
docker compose up --buildLa primera vez tarda 1-2 minutos compilando las imágenes.
Cuando veas Application startup complete, abre:
Pulsa "Registrarse", mete un email y contraseña cualquiera, y ya estás dentro. Crea tu primer tablero y a usarlo.
| Servicio | URL | Puerto contenedor |
|---|---|---|
| Frontend | http://localhost:30005 | 80 (nginx) |
| Backend | http://localhost:30004 | 8000 (FastAPI) |
| Swagger | http://localhost:30004/docs | 8000 |
El SPA habla con /api/* en el mismo origen: el nginx del contenedor
proxya esas rutas al backend, así que cookies y CORS funcionan solos.
Para parar: Ctrl+C y luego docker compose down. Tus datos
viven en backend/data/ (gitignored): la base SQLite, los adjuntos y
las claves VAPID/FCM.
docker-compose.yml es el stack de producción: el código va dentro
de las imágenes y solo se monta backend/data. Para trabajar en el
código, añade las sobrecargas de desarrollo:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --buildEso monta el árbol de fuentes, arranca uvicorn con --reload y sirve el
frontend con el dev server de Vite en el 5173 del contenedor.
⚠️ El dev server de Vite no debe exponerse a Internet: sirve todo el árbol de fuentes (incluidosDockerfile,vite.config.tsysrc/) e interpreta como módulos JS cualquier ruta sin extensión. Por eso las sobrecargas viven endocker-compose.dev.ymly no endocker-compose.override.yml, que Compose aplicaría por su cuenta. Además, el dev server no comprueba tipos: pasanpm run build(que ejecutatsc -b) antes de desplegar.
Todo lo de esta sección es opcional. Alfred funciona sin nada de esto — sólo abre puertas extra (login con Google, passkeys, notificaciones del navegador).
- Ve a Google Cloud Console → Credentials.
- Crea un proyecto si no tienes ninguno → Create credentials → OAuth client ID → Web application.
- Authorized JavaScript origins:
http://localhost:30005. - Authorized redirect URIs:
http://localhost:30004/api/auth/google/callback. - Copia el Client ID y el Client Secret a
backend/.env:GOOGLE_CLIENT_ID=...apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-...
docker compose restart backendy el botón "Entrar con Google" pasa a funcionar.
Funciona out-of-the-box: el backend autogenera las claves VAPID
y las persiste en backend/data/vapid_keys.json (formato
base64url DER PKCS8, a prueba de actualizaciones de py_vapid).
Solo tienes que aceptar el prompt del navegador la primera vez
que crees un recordatorio.
Si prefieres tus propias claves, ponlas en backend/.env:
VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
VAPID_SUBJECT=mailto:tu@email.comFuncionan solas en local (el hostname localhost cuenta como
contexto seguro para el navegador). Si quieres ajustar el RP
explícitamente:
WEBAUTHN_RP_ID=alfred.example.com
WEBAUTHN_ORIGIN=https://alfred.example.com
WEBAUTHN_RP_NAME=AlfredSi los dejas vacíos, el backend cae al hostname de APP_URL.
backend/.env.example las lista todas con comentarios. Las
relevantes:
JWT_SECRET=<openssl rand -hex 32> # obligatoria
DATABASE_URL=sqlite+aiosqlite:///./data/alfred.db # default OK
APP_URL=http://localhost:30004 # backend público
FRONTEND_URL=http://localhost:30005 # SPA pública
CORS_ORIGINS=http://localhost:30005 # coma-separados
CORS_ORIGIN_REGEX=^https://.*\.tail.*\.ts\.net$ # opcional (Tailscale)
GOOGLE_CLIENT_ID= # opcional
GOOGLE_CLIENT_SECRET= # opcional
GOOGLE_NATIVE_AUDIENCES= # opcional (cliente móvil)
WEBAUTHN_RP_ID= # opcional
WEBAUTHN_ORIGIN= # opcional
WEBAUTHN_RP_NAME=Alfred # opcional
# FCM_SERVICE_ACCOUNT_JSON_PATH=data/fcm_service_account.json # legacy, ignorarPara sacar Alfred a internet con tu propio dominio. Necesitas: una VPS Linux, un dominio apuntado a su IP, nginx + certbot.
# 1. Clonar en /opt/alfred (o donde quieras)
sudo git clone https://github.com/diegosuarez/alfred.git /opt/alfred
cd /opt/alfred
# 2. Crear backend/.env con JWT_SECRET y (opcional) credenciales de Google
sudo cp backend/.env.example backend/.env
sudo $EDITOR backend/.env
# 3. Lanzar el deploy idempotente
sudo ALFRED_DOMAIN=alfred.tudominio.com \
LE_EMAIL=tu@email.com \
bash deploy/deploy.shEl script:
- valida el DNS,
- renderiza
deploy/nginx/alfred.confcon tu dominio, - pide cert Let's Encrypt vía webroot,
- ajusta
APP_URL/FRONTEND_URL/CORS_*en tu.env, - levanta el
docker compose, - hace smoke test.
Re-ejecutable cuantas veces quieras. Si ya tienes un vhost que sirve
ese dominio, o has editado a mano el que instaló el script, no lo
sobrescribe: avisa y sigue, para que integres el cambio tú. Fuerza el
reemplazo con ALFRED_FORCE_NGINX=1 si de verdad lo quieres.
Para que arranque al reiniciar la máquina, instala el systemd unit:
sudo bash systemd/install.sh
sudo systemctl enable --now alfredVariables opcionales: ALFRED_DIR (default /opt/alfred),
ALFRED_SSL_CERT_DIR (apuntar a un wildcard existente en lugar
de pedir cert por dominio), ALFRED_FORCE_NGINX=1 (sobrescribir un
vhost modificado a mano).
Cinco formas en la web:
- Email + contraseña local.
- Google OAuth2 (web flow con redirect).
- Passkeys / WebAuthn: en "Tokens API → Passkeys" registras una; en la pantalla de login pulsas "🔐 Entrar con passkey". El navegador te ofrece las passkeys de este dispositivo, autenticas con biometría/PIN, el backend verifica la firma y emite el JWT. Sin email previo.
- Personal Access Tokens (
alfred_pat_*) para clientes headless. - Sesión persistente (JWT en
localStorage). Caduca a las 24h; en cualquier 401 mid-session, el SPA vuelve automáticamente al login.
Vive en cli/. Ver cli/README.md.
cd cli
uv sync
uv tool install . # opcional: alfred en el PATH
alfred config set-api-url https://alfred.example.com
alfred config set-token alfred_pat_...
alfred set-default context Trabajo
alfred set-default board Backlog
alfred task add "Comprar pan"
alfred task list
alfred task edit 42 --json '{"priority": "high", "tag_ids": [3, 7]}'
alfred task add-reminder 42 1780000000 # acepta epoch, ISO o local
alfred add-completions # bash / fish / zsh, autodetectadoSi no configuras defaults, los comandos preguntan con un picker de flechas.
El CLI ignora certs self-signed por defecto (verify_ssl = false en cli/config.toml) para que funcione contra Tailscale sin más.
El frontend es una PWA. Con la web abierta:
- Chrome / Edge (escritorio o móvil): icono "Instalar" en la barra de direcciones → "Instalar Alfred". Queda como app nativa con su propio dock/launcher.
- Safari iOS / iPadOS: botón Compartir → "Añadir a pantalla de inicio".
- Firefox móvil: menú ⋯ → "Instalar".
Notificaciones push funcionan con la pestaña cerrada gracias al Service Worker (acepta el prompt cuando salga, o lánzalo desde Perfil → tu primer recordatorio).
Hubo una app Android nativa en una rama anterior; quedó descartada porque la PWA cubre el caso de uso con bastante menos mantenimiento.
cd backend
uv run pytest # 124 tests, ~80sNo hay suite del frontend; verificación manual + typecheck (tsc -b).
Si tras una migración aparecen duplicados:
docker compose exec backend uv run python scripts/cleanup_google_contacts.py --list
docker compose exec backend uv run python scripts/cleanup_google_contacts.py --account-id 1 --dry-run
docker compose exec backend uv run python scripts/cleanup_google_contacts.py --account-id 1
# Variante nuclear (borra TODO menos "Yo mismo"):
docker compose exec backend uv run python scripts/cleanup_google_contacts.py --allLuego vuelve a sincronizar desde la UI. El upsert idempotente no volverá a duplicar.
Síntoma: el loop de reminders crashea con ValueError: Could not deserialize key data desde py_vapid. Fix:
docker compose exec backend uv run python -c "
import sqlite3; db = sqlite3.connect('/app/data/alfred.db')
print('Borradas', db.execute('DELETE FROM push_subscriptions').rowcount, 'suscripciones')
db.commit()
"
sudo rm /opt/alfred/backend/data/vapid_keys.json
docker compose restart backendDespués re-acepta las notificaciones en el navegador. El backend genera la nueva clave en formato base64url DER PKCS8 (a prueba de cambios entre versiones de py_vapid).
Swagger interactivo en /docs. Endpoints principales:
# Auth
POST /api/auth/register POST /api/auth/login
GET /api/auth/google/login GET /api/auth/google/callback
POST /api/auth/google/native # móvil: intercambia ID token por JWT
# Passkeys (WebAuthn)
POST /api/passkeys/register/begin POST /api/passkeys/register/finish
POST /api/passkeys/login/begin POST /api/passkeys/login/finish
GET /api/passkeys DELETE /api/passkeys/{id}
# Contextos / Tableros / Columnas
GET /api/contexts POST /api/contexts
PUT /api/contexts/{id} DELETE /api/contexts/{id}
GET /api/boards?context_id= POST /api/boards
GET /api/boards/{id} PUT /api/boards/{id} DELETE /api/boards/{id}
POST /api/boards/{id}/columns POST /api/boards/{id}/columns/reorder
# Tareas / Subtareas / Adjuntos / Recordatorios
POST /api/columns/{id}/tasks POST /api/columns/{id}/tasks/reorder
POST /api/columns/{id}/archive-all GET /api/columns/{id}/archived-tasks
GET /api/tasks/{id} PUT /api/tasks/{id} DELETE /api/tasks/{id}
POST /api/tasks/{id}/move POST /api/tasks/{id}/subtasks
POST /api/tasks/{id}/reminders POST /api/tasks/{id}/attachments
GET /api/attachments/{id} DELETE /api/attachments/{id}
GET /api/reminders/pending DELETE /api/reminders/{id}
# Contactos / Cuentas Google
GET /api/contacts POST /api/contacts
PUT /api/contacts/{id} DELETE /api/contacts/{id}
GET /api/google-accounts POST /api/google-accounts/connect
DELETE /api/google-accounts/{id} POST /api/google-accounts/{id}/sync-contacts
# Foco / Push / Tags / PATs
POST /api/focus GET /api/focus/stats
GET /api/push/vapid-public-key POST /api/push/subscribe POST /api/push/unsubscribe
POST /api/push/fcm/subscribe POST /api/push/fcm/unsubscribe
GET /api/tags POST /api/tags ...
GET /api/pats POST /api/pats DELETE /api/pats/{id}
Personal — un solo usuario, sin compromisos de soporte. Úsalo, haz fork, rómpelo.
