aboutsummaryrefslogtreecommitdiffstats
path: root/claude_sesiones/webpage.py
blob: 8291a6be406d44ea717482024e8c49a41bce0f87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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),
    }