aboutsummaryrefslogtreecommitdiffstats
path: root/claude_sesiones/webpage.py
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-08-16 19:57:17 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-08-16 19:57:17 -0300
commitbd3ca3a04fcd8d882d23deeb3f3a0d6f04b3f93f (patch)
treea9e0dd65940de54c8f956befe9d86b80350bde2e /claude_sesiones/webpage.py
parentda1c60458b0d28e84739475491a3f2d61ed8eff8 (diff)
downloadclaude-logbook-bd3ca3a04fcd8d882d23deeb3f3a0d6f04b3f93f.tar.gz
claude-logbook-bd3ca3a04fcd8d882d23deeb3f3a0d6f04b3f93f.zip
Preparar el proyecto para publicarlo
El script suelto pasa a ser un paquete instalable, con tests y documentación. El comportamiento del CLI no cambia: mismos flags, misma salida. Estructura - claude_sesiones/{sessions,terminal,webpage,cli}.py separa parseo, presentación, generación del HTML y argumentos. El template pasa a ser data del paquete. - pyproject.toml con el entry point claude-sesiones, sin dependencias. - build.sh se disuelve en `claude-sesiones --html [ARCHIVO]`: el HTML se arma en proceso, sin subprocess ni data.json intermedio. `--open` lo abre en el navegador. Arreglos - El template no tenía doctype ni <meta charset>: abierto como file:// quedaba en quirks mode y con la codificación del sistema, así que los acentos salían rotos. Tampoco tenía viewport, con lo que en el celular se veía a escala de escritorio. - `delete_sessions()` se llamaba con un argumento de menos (lo encontró el test de borrado). - El orden de la tabla comparaba timestamps como strings; ahora compara los datetimes ya parseados. - `mark()` buscaba el texto crudo dentro del HTML ya escapado, así que resaltar algo con & o < nunca encontraba nada. `esc()` no escapaba la comilla simple. - El caché no tenía versión: al cambiar el esquema del registro se leían registros de la forma anterior. Ahora se invalida solo. El temporal lleva el pid, para que dos corridas simultáneas no se pisen. - `pick()` cortaba el proceso con sys.exit desde adentro; ahora levanta SessionError y el código de salida lo decide la CLI. Mejoras - Respeta CLAUDE_CONFIG_DIR. - La página tiene botón de tema claro/oscuro que recuerda la elección, deep links (#uuid abre esa conversación), atajos de teclado, trampa de foco en el lector y aviso si el JS está apagado. Las tres copias de la paleta quedaron en dos, una por tema. - 92 tests con unittest, sin dependencias, contra árboles de .jsonl falsos: nunca tocan ~/.claude. CI en GitHub Actions, Python 3.9 a 3.13. - README en inglés y español, con el esquema del JSON y una advertencia sobre lo que hay adentro de sesiones.html. - Apache-2.0. Claude-Session: https://claude.ai/code/session_01RmtZ9qBemrc9TncwVTG6ED
Diffstat (limited to 'claude_sesiones/webpage.py')
-rw-r--r--claude_sesiones/webpage.py64
1 files changed, 64 insertions, 0 deletions
diff --git a/claude_sesiones/webpage.py b/claude_sesiones/webpage.py
new file mode 100644
index 0000000..8291a6b
--- /dev/null
+++ b/claude_sesiones/webpage.py
@@ -0,0 +1,64 @@
+"""Genera la página HTML autocontenida a partir de las sesiones.
+
+El resultado no necesita red ni servidor: los datos y las transcripciones van
+embebidos adentro y se abre con doble clic.
+"""
+
+import json
+from importlib import resources
+
+MARKER = "__DATA__"
+
+TEMPLATE_NAME = "template.html"
+
+
+class TemplateError(Exception):
+ """El template no tiene la forma que espera el generador."""
+
+
+def template_text(path=None):
+ if path:
+ with open(path, encoding="utf-8") as f:
+ return f.read()
+ return (resources.files(__package__) / TEMPLATE_NAME).read_text(encoding="utf-8")
+
+
+def encode_payload(records):
+ """Serializa los registros para meterlos en un <script type=application/json>.
+
+ El parser de HTML corta ese bloque en el primer "</script", y las
+ transcripciones tienen HTML adentro. Escapamos "</" como "<\\/", que es un
+ escape válido de JSON: JSON.parse lo devuelve intacto.
+ """
+ raw = json.dumps(records, ensure_ascii=False, separators=(",", ":"))
+ return raw.replace("</", "<\\/")
+
+
+def render(records, template=None):
+ """Devuelve el HTML completo con los datos ya embebidos."""
+ html = template if template is not None else template_text()
+ # No se puede verificar el resultado buscando el marcador: estas mismas
+ # sesiones incluyen conversaciones sobre este script, así que el payload lo
+ # contiene como texto. Validamos el template antes de sustituir.
+ if html.count(MARKER) != 1:
+ raise TemplateError(
+ f"el template debe tener exactamente un {MARKER} "
+ f"(encontrados: {html.count(MARKER)})")
+ return html.replace(MARKER, encode_payload(records))
+
+
+def write(records, out_path, template=None):
+ """Escribe la página y devuelve un resumen de lo que quedó adentro."""
+ html = render(records, template=template)
+ with open(out_path, "w", encoding="utf-8") as f:
+ f.write(html)
+ return summary(records)
+
+
+def summary(records):
+ return {
+ "sesiones": len(records),
+ "proyectos": len({r["p"] for r in records}),
+ "mensajes": sum(r["u"] for r in records),
+ "bloques": sum(len(r["c"]) for r in records),
+ }