Me hice un diario para el iPad que parece un cuaderno de verdad (y se guarda en mi Raspberry Pi)
Una web app tipo Moleskine para escribir con el Apple Pencil o el teclado: tinta con presión, pasar página en 3D y copia de cada página en la Pi, sin App Store.
Llevo tiempo queriendo escribir un diario a mano, pero en papel lo pierdo, no tengo copia y acabo con tres cuadernos empezados. Tengo un iPad con Apple Pencil, así que la idea era obvia: un cuaderno digital que se sienta como uno de papel, no otra app de notas con mil menús. Lo he montado como una web app servida desde mi Raspberry Pi, y cada página que escribo acaba también en el NAS. Te cuento cómo está hecho y qué decisiones tomé.
Lo he programado con Claude Code a partir de lo que yo le iba pidiendo y probando en el iPad. El código es pequeño: una página HTML, unas 700 líneas de JavaScript y un servidor de Python de 100 líneas.
1. Por qué una web app y no una app de iOS
Lo primero fue descartar la app nativa. Para instalar en un iPad una app hecha por ti necesitas Xcode, que solo existe en Mac, y si no pagas la cuenta de desarrollador de Apple (99 € al año), la app caduca a los 7 días. Yo no tengo Mac ni quiero pagar para escribir en mi propio cuaderno.
Una PWA (web app progresiva) se salta todo eso: la abres en Safari, le das a “Compartir > Añadir a pantalla de inicio” y se queda con su icono, a pantalla completa y sin barra del navegador. Para eso basta con un manifest.webmanifest y unas cuantas etiquetas en el HTML:
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="icon-180.png">
<link rel="manifest" href="manifest.webmanifest">
2. Lo que tenía que tener para parecer un cuaderno
La regla que me puse fue que no se pueda hacer nada que no se pueda hacer con un cuaderno de verdad. Nada de deshacer infinito, ni de borrar la tinta, ni de reordenar páginas. Lo que sí tiene:
- Tapa negra con goma que se abre al tocarla, y dentro la página de “En caso de pérdida, por favor devolver a:”, como los Moleskine.
- 240 páginas rayadas de proporción 13 x 21 cm. En horizontal se ve el pliego doble y en vertical una sola página.
- Pasar página en 3D: arrastras con el dedo y la hoja te sigue. Si sueltas a medias, vuelve a su sitio.
- Tres tintas: negra, azul y lápiz. Solo el lápiz se borra, con una goma; lo que escribes con tinta se queda.
- La fecha se escribe sola en la esquina cuando empiezas una página nueva, con la misma tinta que estés usando.
- Una cinta de marcapáginas que te lleva a la última página escrita.
- Un modo “Aa” para escribir con teclado, con una letra manuscrita (Caveat) que tiembla un poco en cada letra para que no parezca impresa.
Lo del temblor tiene truco: si lo hiciera con Math.random(), el texto bailaría cada vez que se repinta la página. Uso un generador pseudoaleatorio con semilla (mulberry32), así el mismo texto tiembla siempre igual.
3. La tinta: presión del Pencil y curvas suaves
El Apple Pencil manda la presión en los eventos de puntero (pointerType === 'pen' y ev.pressure entre 0 y 1). Con eso el grosor del trazo cambia según aprietas:
let pr = ev.pointerType === 'pen' ? Math.pow(Math.max(ev.pressure, 0.05), 0.75) : 0.5;
if (trazo.prevPr != null) pr = trazo.prevPr * 0.55 + pr * 0.45; // suavizado
Hay tres detalles que marcan la diferencia:
getCoalescedEvents(): Safari agrupa los movimientos rápidos en un solo evento. Si no pides los eventos “coalescidos”, la letra rápida sale a trozos rectos.- Curvas cuadráticas entre puntos medios en vez de líneas rectas de punto a punto. Es lo que hace que una “o” sea redonda y no un polígono.
- El dedo no escribe. Con el dedo pasas página; solo el Pencil pinta. Y hay que bloquear
touchstartsobre la hoja, porque si no, iOS saca la lupa y la selección de texto cuando apoyas el lápiz.
Los trazos se guardan como listas de números (x, y, presión) en coordenadas relativas a la página, de 0 a 1. Así la página se ve igual en horizontal, en vertical o en otro dispositivo.
La goma del lápiz me costó una vuelta. Cada hoja tiene dos lienzos superpuestos: uno debajo para el lápiz y otro encima para la tinta. La goma pinta con globalCompositeOperation = 'destination-out' solo en el lienzo del lápiz, así que si escribes con tinta encima de algo a lápiz y luego lo borras, la tinta no se toca. Igual que en el papel.
4. Dónde se guarda: el iPad primero, la Pi después
No quería que el diario dependiera de tener conexión. Así que cada página se guarda primero en el propio iPad, en IndexedDB, y a los 2,5 segundos de dejar de escribir se sube a la Pi. Si no hay red, lo reintenta cada 30 segundos.
El servidor de la Pi es Python sin dependencias (http.server), que sirve los ficheros de la web y tiene una API mínima:
| Ruta | Qué hace |
|---|---|
GET /api/paginas |
Índice con la hora del último cambio de cada página |
GET /api/pagina/<n> |
Una página en JSON |
PUT /api/pagina/<n> |
Guarda la página |
Lo importante está en el PUT. Cada página lleva un campo mod con la hora del último cambio, y el servidor rechaza con un 409 cualquier versión más antigua que la que ya tiene. Además, antes de sobrescribir guarda la anterior como pagina-N.json.prev y escribe en un fichero temporal que luego renombra, para que un corte de luz no deje una página a medias:
if indice.get(str(n), 0) > mod:
return self.json(409, {'error': 'hay una versión más nueva'})
if os.path.exists(fichero):
os.replace(fichero, fichero + '.prev')
escribir(fichero, json.dumps(pag)) # .tmp + fsync + os.replace
Al abrir la app hace el camino contrario: mira el índice de la Pi y se baja las páginas que allí sean más nuevas. Con eso, si un día escribo en otro dispositivo, al volver al iPad está todo.
5. Montarlo en la Pi
En la Pi es un servicio de systemd más. Corre con mi usuario normal, pero con ProtectSystem=strict y ProtectHome=read-only solo puede escribir en su carpeta de datos:
# /etc/systemd/system/diario.service
[Unit]
Description=Diario (web app del cuaderno, puerto 8095)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/diario
ExecStart=/usr/bin/python3 server.py
Environment=PORT=8095
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/pi/diario/datos
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now diario
Aquí viene la trampa: con http://192.168.1.50:8095 el diario funciona, pero el service worker no se registra, porque los navegadores solo lo permiten en HTTPS (o en localhost). Sin service worker no hay modo sin conexión y la “app” es solo una pestaña. La solución que ya uso para Vaultwarden es Tailscale, que pone un certificado válido delante:
sudo tailscale serve --bg --https=8444 http://127.0.0.1:8095
Ahora el diario vive en https://mi-pi.tu-red.ts.net:8444, con candado verde y accesible desde cualquier sitio donde el iPad tenga Tailscale, sin abrir ningún puerto en el router.
El service worker usa la estrategia “primero la red”: si hay conexión, se baja la versión nueva de la app; si no, tira de la caché. Cuando cambio algo subo el nombre de la caché (diario-v3) para que el iPad tire la vieja.
6. La copia al NAS
La carpeta del diario ya entra en el script de copias de la Pi al NAS: solo añadí /home/pi/diario a la lista de orígenes. Al final cada página está en tres sitios: el iPad, la Pi y el NAS, más la versión .prev de cada una. Para un diario me sobra.
Lo que no hay que hacer
- Probarlo solo en el ordenador. En el escritorio todo iba perfecto; en el iPad, Safari no respetaba
backface-visibilityal pasar página y se veían las dos caras a la vez. Se arregla dando a cada cara su propia transformación (translateZ(.5px)), pero solo lo descubres en el dispositivo. - Servirlo por HTTP y dar por hecho que funciona sin conexión. Sin HTTPS no hay service worker, y no avisa: simplemente no se registra.
- Guardar los trazos en píxeles. En cuanto giras el iPad, todo se descoloca. Coordenadas relativas desde el primer día.
- Fiarse solo de IndexedDB. Si borras los datos de Safari, quitas el icono o el iPad se queda sin espacio, lo guardado en el navegador se puede ir. Por eso existe la copia en la Pi.
Resumen
- Sin Mac ni cuenta de desarrollador, una PWA en la pantalla de inicio es la forma de tener tu propia app en el iPad.
- Para que la letra salga bien: presión del Pencil, eventos coalescidos y curvas entre puntos medios.
- Primero se guarda en el iPad y luego en la Pi, con control de versiones simple (
mod, 409 y.prev). - HTTPS con
tailscale serve, o el modo sin conexión no existe. - La Pi lo copia al NAS como todo lo demás.
Es el proyecto más “inútil” que tengo en la Pi y probablemente el que más voy a usar. Me apetece escribir en él precisamente porque no es una app de notas.
¿Te has quedado con la idea?
Tres preguntas rápidas. Cada acierto suma 10 XP.