"""Standup diario (27-oct-2026): el bot de scrum del sitio. Cada mañana el agente diario (Claude, tarea programada) lee el tablero y prepara para cada persona con pendientes abiertos su «parte del día»: instrucciones concretas (qué hacer hoy, en qué orden, con las ligas a los documentos) y las tres preguntas de siempre sobre SUS pendientes: ¿qué terminaste?, ¿qué te falta?, ¿qué te bloquea? Cada quien contesta en /tableros/standup/ (el sitio le avisa con una franja arriba hasta que conteste). Al día siguiente el agente lee las respuestas, arma un lote para el tablero (Leo lo aplica) y un resumen para Leo y Adriana. Si el agente no corrió, la página igual pregunta por los pendientes abiertos de la persona (sin instrucciones). Lo engancha app.py al final con registrar(app, globals()). Datos: DATOS_BASE/standup/AAAA-MM-DD/.json {fecha, usuario, nombre, preparado_por, preparado_en, instrucciones:[{texto, tarea_id?, liga?}], preguntas:[{tarea_id, titulo, pregunta}], nota_bot, respuesta:{enviada_en, hecho, falta, bloqueos, comentario, por_tarea:[{tarea_id, estado, nota}]}, historial} Rutas (todas con sesión): GET /api/standup/hoy[?fecha=] el mío de hoy (o el que el bot dejó en los últimos 3 días sin contestar) POST /api/standup/respuesta {fecha, hecho, falta, bloqueos, comentario, por_tarea:[{tarea_id, estado, nota}]} GET /api/standup?fecha= equipo: quién contestó, bloqueos (STANDUP_LECTORES + administradores) POST /api/standup/preparar {fecha, personas:[{usuario, instrucciones, preguntas, nota_bot}]} (claude + admins) GET /api/standup/estado {pendiente: bool, fecha} — para la franja de aviso (barato) Estados por pendiente en la respuesta: hecho · sigo · bloqueado · no-empece. """ import datetime import os from flask import jsonify, request ESTADOS_RESP = ("hecho", "sigo", "bloqueado", "no-empece") DIAS_ATRAS = 3 def registrar(app, g): carpeta = os.environ.get("STANDUP_DIR", os.path.join(g["DATOS_BASE"], "standup")) lectores = {u.strip().lower() for u in os.environ.get("STANDUP_LECTORES", "leo,adriana,claude,gibranlp").split(",") if u.strip()} preparan = {u.strip().lower() for u in os.environ.get("STANDUP_PREPARAN", "claude").split(",") if u.strip()} texto, ahora, hist = g["_texto"], g["ahora"], g["_hist"] def yo(): return g["current_user"]() def hoy(): return datetime.date.today().isoformat() def fecha_valida(s): s = texto(s, 10) return s if g["FECHA_RE"].match(s or "") else None def ruta(fecha, usuario): return os.path.join(carpeta, fecha, g["_norm_id"](usuario) + ".json") def mis_pendientes(u): miembros = g["_miembros"]() out = [] for t in g["_listar_json"](g["TAREAS_DIR"]): if t.get("estado") == "hecho": continue ids = g["_ids_responsable"](t.get("responsable"), miembros)[0] if g["_norm_id"](u) in ids: out.append({"id": t.get("id"), "titulo": t.get("titulo"), "estado": t.get("estado"), "vence": t.get("vence"), "pista": t.get("pista"), "bloqueado_por": t.get("bloqueado_por")}) orden = {"bloqueado": 0, "en-curso": 1, "por-hacer": 2} out.sort(key=lambda t: (orden.get(t["estado"], 3), t.get("vence") or "9999")) return out def armar_generico(u, fecha): pend = mis_pendientes(u) m = g["_miembros"]().get(g["_norm_id"](u)) or {} return {"fecha": fecha, "usuario": g["_norm_id"](u), "nombre": m.get("nombre") or u, "preparado_por": "", "preparado_en": None, "instrucciones": [], "nota_bot": "", "preguntas": [{"tarea_id": t["id"], "titulo": t["titulo"], "pregunta": ""} for t in pend[:12]], "respuesta": None, "_generico": True} def buscar_mio(u, fecha=None): """El de la fecha pedida; si no, el de hoy; si no hay de hoy, el último sin contestar de los últimos días.""" if fecha: return g["_leer_json"](ruta(fecha, u)), fecha d = g["_leer_json"](ruta(hoy(), u)) if d: return d, hoy() for i in range(1, DIAS_ATRAS + 1): f = (datetime.date.today() - datetime.timedelta(days=i)).isoformat() d = g["_leer_json"](ruta(f, u)) if d and not d.get("respuesta"): return d, f return None, hoy() def con_pendientes(d, u): """Agrega el estado de hoy de cada pendiente preguntado (pudo moverse desde que el bot preguntó).""" actuales = {t["id"]: t for t in mis_pendientes(u)} for p in d.get("preguntas") or []: t = actuales.get(p.get("tarea_id")) p["estado_tablero"] = t["estado"] if t else "hecho o ya no es tuyo" p["vence"] = t.get("vence") if t else None d["pendientes_abiertos"] = list(actuales.values()) return d @app.get("/api/standup/hoy") def standup_hoy(): u = yo() if not u: return g["_sin_usuario"]() d, fecha = buscar_mio(u, fecha_valida(request.args.get("fecha"))) d = d or armar_generico(u, fecha) return jsonify(ok=True, standup=con_pendientes(d, u), estados=list(ESTADOS_RESP), hoy=hoy()) @app.get("/api/standup/estado") def standup_estado(): u = yo() if not u: return g["_sin_usuario"]() d, fecha = buscar_mio(u) return jsonify(ok=True, preparado=bool(d), pendiente=bool(d and not d.get("respuesta")), fecha=fecha) @app.post("/api/standup/respuesta") def standup_respuesta(): u = yo() if not u: return g["_sin_usuario"]() d = g["_json_body"]() if d is None: return jsonify(ok=False, error="JSON inválido"), 400 fecha = fecha_valida(d.get("fecha")) or hoy() reg = g["_leer_json"](ruta(fecha, u)) or armar_generico(u, fecha) reg.pop("_generico", None) por_tarea = [] for x in d.get("por_tarea") or []: if isinstance(x, dict) and x.get("tarea_id") and x.get("estado") in ESTADOS_RESP: por_tarea.append({"tarea_id": texto(x["tarea_id"], 60), "estado": x["estado"], "nota": texto(x.get("nota"), 600)}) resp = {"enviada_en": ahora(), "hecho": texto(d.get("hecho"), 3000), "falta": texto(d.get("falta"), 3000), "bloqueos": texto(d.get("bloqueos"), 3000), "comentario": texto(d.get("comentario"), 3000), "por_tarea": por_tarea} if not (resp["hecho"] or resp["falta"] or resp["bloqueos"] or por_tarea): return jsonify(ok=False, error="Contesta al menos una pregunta o marca cómo va un pendiente."), 400 cambio = "contestó" if not reg.get("respuesta") else "corrigió su respuesta" reg["respuesta"] = resp hist(reg, u, cambio) g["_escribir_json"](ruta(fecha, u), reg) return jsonify(ok=True, standup=reg) @app.post("/api/standup/preparar") def standup_preparar(): u = yo() if not u or not (u.lower() in preparan or g["is_admin"]()): return jsonify(ok=False, error="Solo el bot (claude) o un administrador prepara el standup."), 403 d = g["_json_body"]() if d is None: return jsonify(ok=False, error="JSON inválido"), 400 fecha = fecha_valida(d.get("fecha")) or hoy() miembros = g["_miembros"]() hechos, omitidos = [], [] for p in d.get("personas") or []: uid = g["_norm_id"](p.get("usuario")) if uid not in miembros: omitidos.append({"usuario": uid, "por": "no es miembro del equipo"}) continue previo = g["_leer_json"](ruta(fecha, uid)) if previo and previo.get("respuesta"): omitidos.append({"usuario": uid, "por": "ya contestó: no se reescribe"}) continue instr = [] for x in p.get("instrucciones") or []: if isinstance(x, str): x = {"texto": x} if isinstance(x, dict) and texto(x.get("texto")): liga = texto(x.get("liga"), 300) if liga.lower().startswith(("javascript:", "data:")): liga = "" instr.append({"texto": texto(x.get("texto"), 600), "tarea_id": texto(x.get("tarea_id"), 60), "liga": liga}) preg = [] for x in p.get("preguntas") or []: if isinstance(x, dict) and (x.get("tarea_id") or x.get("pregunta")): preg.append({"tarea_id": texto(x.get("tarea_id"), 60), "titulo": texto(x.get("titulo"), 200), "pregunta": texto(x.get("pregunta"), 400)}) reg = {"fecha": fecha, "usuario": uid, "nombre": miembros[uid].get("nombre") or uid, "preparado_por": u, "preparado_en": ahora(), "instrucciones": instr[:12], "preguntas": preg[:15], "nota_bot": texto(p.get("nota_bot"), 1200), "respuesta": None, "historial": (previo or {}).get("historial", [])} hist(reg, u, "preparó el standup" if not previo else "rehízo el standup") g["_escribir_json"](ruta(fecha, uid), reg) hechos.append(uid) return jsonify(ok=True, fecha=fecha, preparados=hechos, omitidos=omitidos) @app.get("/api/standup") def standup_equipo(): u = yo() if not u: return g["_sin_usuario"]() if not (u.lower() in lectores or g["is_admin"]()): return jsonify(ok=False, error="La vista del equipo es para Leo, Adriana y el bot."), 403 fecha = fecha_valida(request.args.get("fecha")) or hoy() base = os.path.join(carpeta, fecha) regs = g["_listar_json"](base) if os.path.isdir(base) else [] fechas = sorted([f for f in os.listdir(carpeta) if g["FECHA_RE"].match(f)], reverse=True)[:30] if os.path.isdir(carpeta) else [] return jsonify(ok=True, fecha=fecha, fechas=fechas, standups=regs, contestaron=sum(1 for r in regs if r.get("respuesta")), total=len(regs)) g["_standup"] = {"mis_pendientes": mis_pendientes, "carpeta": carpeta}