Inicio como lista con ✓: lo hecho se queda en su sitio, con cuándo y qué

Al terminar algo (elegir el tipo de trabajo, apartar stock, juntar repetidos, la bandeja, convertir) la ficha
desaparecía sin decir nada y no se sabía por dónde se iba. Ahora cada paso hecho se queda con su ✓ y «Hecho hoy
a las 07:41: …», arriba se cuentan los hechos, y lo que acaba de pasar a hecho se ilumina. Aplicar el plan
apunta qué pasos entraron; elegir el tipo y nombrar una carpeta quedan en la bitácora. «Basura y proxies» deja
de pedirse cuando lo que queda es lo de «No tocar» o lo que va sin marcar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-14 07:43:50 -05:00
padre 5515ef7dac
commit 41155cd834
Se han modificado 3 ficheros con 115 adiciones y 12 borrados

Ver fichero

@@ -42,7 +42,7 @@ import actualizador
asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__]
VERSION = "3.7.0"
VERSION = "3.7.1"
BASE = os.path.dirname(os.path.abspath(sys.executable if getattr(sys, "frozen", False) else __file__))
# Empaquetado: el programa corre con el motor que viaja en su propia carpeta. OrganizadorLabstream.exe es
@@ -2162,7 +2162,9 @@ def aplicar(raiz, filas):
if os.path.normpath(base).lower() != os.path.normpath(raiz).lower():
quitar_vacias(base, raiz)
actualizar_inventario(movidos)
bitacora({"lote": lote, "op": "fin", "raiz": raiz, "filas": hechas, "movimientos": len(movidos), "errores": errores})
# qué pasos del plan entraron (limpiar, fechas, resto, banco): Inicio les pone su ✓ con esto
pasos = dict(Counter(f.get("paso") or "resto" for f in filas))
bitacora({"lote": lote, "op": "fin", "raiz": raiz, "filas": hechas, "movimientos": len(movidos), "errores": errores, "pasos": pasos})
return {"lote": lote, "filas": hechas, "movimientos": len(movidos), "errores": errores}
@@ -6011,6 +6013,62 @@ def _ultimo_orden(raiz):
return None
def _nombre_tipo(t):
return (R.TIPOS_CLIENTE.get(t) or t or "").split(" · ")[0]
# el orden de la lista de Inicio: lo hecho se queda en su sitio, no se va al final ni desaparece
_ORDEN_ACCIONES = ["escanear", "tipo", "entrada", "limpiar", "nombrar", "fechas", "resto", "banco", "vacias", "stock",
"repetidos", "convertir", "respaldar"]
def _historial_carpeta(raiz):
"""Lo que ya se hizo en esta carpeta, por acción de Inicio: la última vez y qué fue. Con esto cada paso
hecho sale con su ✓ en vez de desaparecer de la lista (antes el stock, los repetidos, la bandeja, el
tipo de trabajo o la conversión, al terminarlos, simplemente se iban, y no se sabía por dónde se iba)."""
ent = leer_bitacora(20000)
rl = raiz.lower()
deshechos = {e.get("de_lote") for e in ent if e.get("op") == "deshacer"}
h = {}
def pon(clave, ts, texto, n=1):
x = h.setdefault(clave, {"ts": 0, "texto": "", "veces": 0})
x["veces"] += n
if (ts or 0) >= x["ts"]:
x["ts"], x["texto"] = ts or 0, texto
for e in ent:
op, l, ts = e.get("op"), e.get("lote") or "", e.get("ts")
if (e.get("raiz") or "").lower() != rl and rl not in [x.lower() for x in (e.get("raices") or [])]:
continue
if l and l in deshechos:
continue
if op == "tipo":
pon("tipo", ts, f"elegiste «{_nombre_tipo(e.get('tipo'))}»")
elif op == "nombrar":
pon("nombrar", ts, f"dijiste qué es «{e.get('ejemplo') or e.get('token')}»")
elif op == "respaldo":
pon("respaldar", ts, f"{_plural(e.get('copiados') or 0, 'archivo copiado', 'archivos copiados')} a {e.get('a') or 'el respaldo'}")
elif op == "tanda":
pon("convertir", ts, _plural(e.get("clips") or 0, "clip convertido", "clips convertidos"))
elif op == "fin":
m = e.get("movimientos") or 0
if l.startswith("stock-"):
pon("stock", ts, _plural(m, "archivo apartado", "archivos apartados") + " a «_Para borrar\\stock»")
elif l.startswith("dup-"):
pon("repetidos", ts, _plural(m, "copia juntada", "copias juntadas") + " en «Duplicados»")
elif l.startswith("vacias-"):
pon("vacias", ts, _plural(m, "carpeta vacía borrada", "carpetas vacías borradas"))
elif l.startswith("entrada-"):
pon("entrada", ts, _plural(m, "archivo traído", "archivos traídos"))
elif l.startswith(("optimizar", "descartados-")):
pon("convertir", ts, _plural(m, "original reemplazado", "originales reemplazados"))
elif not l.startswith(("pieza-", "colores-", "tanda-", "respaldo")):
# aplicar el plan: los pasos que entraron (los lotes de antes de la 3.7.1 no lo apuntaban)
for p, n in (e.get("pasos") or {"limpiar": 0, "fechas": 0, "resto": 0}).items():
pon(p, ts, _plural(n, "cambio aplicado", "cambios aplicados") if n else _plural(m, "movimiento", "movimientos"))
return h
def _cuando(ts):
"""«hoy a las 05:02», «ayer a las 23:10», «el 3 de octubre a las 11:40»."""
if not ts:
@@ -6086,7 +6144,18 @@ def acciones(raiz):
fila = con.execute("SELECT COUNT(*) n, COALESCE(SUM(bytes),0) b FROM archivos WHERE raiz = ? "
"AND (es_basura = 1 OR es_proxy = 1)" + en_papelera, [raiz] + args_pap).fetchone()
vieja = con.execute("SELECT n, bytes FROM carpetas WHERE ruta = ?", (os.path.join(raiz, R.PAPELERAS[1]),)).fetchone()
if fila["n"]:
plan_l = PLANES.get(raiz)
marc_l = [f for f in plan_l["filas"] if f.get("paso") == "limpiar" and f.get("marcado")] if plan_l else None
sin_l = [f for f in plan_l["filas"] if f.get("paso") == "limpiar" and not f.get("marcado")] if plan_l else []
if fila["n"] and marc_l is not None and not marc_l:
# lo que queda no lo aparta la app sola (está en «No tocar», o es un proxy sin su original): está hecho
# lo seguro; queda mirar
añadir(clave="limpiar", titulo="Basura y proxies", boton="Ver", estado="espera",
que="Lo seguro ya está apartado. Lo que queda no se aparta sin que lo marques tú.",
cifra=f"{_plural(len(sin_l), 'cambio sin marcar', 'cambios sin marcar')} · {_gb(sum(f['bytes'] for f in sin_l))}",
detalle=("Hay proxies sin su original: pueden ser la única copia." if any("OJO" in f["motivo"] for f in sin_l) else "")
+ _no_tocar_dentro(con, raiz), ir="plan", paso="limpiar")
elif fila["n"]:
añadir(clave="limpiar", titulo="Apartar basura y proxies", boton="Ver y apartar",
que="Archivos de sistema y proxies, que Resolve y Premiere regeneran solos.",
cifra=f"{_plural(fila['n'], 'archivo', 'archivos')} · {_gb(fila['b'])}",
@@ -6114,7 +6183,7 @@ def acciones(raiz):
# sin sumar bytes: una carpeta sin reconocer dentro de otra sin reconocer contaría dos veces
# el mismo material, y salían totales mayores que el propio disco
con_sug = [t for t in sin if t.get("sugerencia")]
añadir(clave="nombrar", titulo="Decir qué son " + _plural(len(sin), "carpeta", "carpetas"), boton="Nombrarlas" if len(sin) > 1 else "Nombrarla",
añadir(clave="nombrar", titulo=("Decir qué son " + _plural(len(sin), "carpeta", "carpetas")) if len(sin) > 1 else "Decir qué es una carpeta", boton="Nombrarlas" if len(sin) > 1 else "Nombrarla",
que="Sin esto la app no las mueve: se quedan donde están.",
cifra=_plural(len(sin), "carpeta", "carpetas") + (f" · {len(seguras)} de un clic" if seguras else ""),
detalle=(f"La app propone qué son {len(con_sug)} de ellas mirando lo que tienen dentro"
@@ -6298,6 +6367,27 @@ def acciones(raiz):
ir="respaldar")
con.close()
hist = _historial_carpeta(raiz)
hay = {a["clave"] for a in out}
for clave, titulo, que in (("tipo", "Cómo se ordena esta carpeta", f"Como «{_nombre_tipo(r.get('tipo', 'contenido'))}», igual que su cliente."),
("entrada", "Bandeja de entrada", "Vacía: lo que entró ya está repartido."),
("stock", "Stock de Envato viejo", "No queda stock viejo por apartar."),
("repetidos", "Material repetido", "No quedan copias de más por juntar."),
("convertir", "Clips de cámara", "No quedan clips que compense convertir.")):
if clave in hist and clave not in hay:
añadir(clave=clave, titulo=titulo, estado="hecho", que=que, cifra="", detalle="")
for a in out:
x = hist.get(a["clave"])
if not x or "Hecho " in (a.get("detalle") or ""):
continue
cuando = _cuando(x["ts"])
if a["clave"] == "nombrar" and x["veces"] > 1:
a["hecho"] = f"Ya nombraste {x['veces']}; la última {cuando}."
elif a["estado"] == "hecho":
a["hecho"] = f"Hecho {cuando}: {x['texto']}."
else:
a["hecho"] = f"Última vez {cuando}: {x['texto']}."
out.sort(key=lambda a: _ORDEN_ACCIONES.index(a["clave"]) if a["clave"] in _ORDEN_ACCIONES else 99)
# «lo seguro» son las acciones que se pueden hacer sin elegir nada: no entran convertir ni respaldar
auto = [a["clave"] for a in out if a["estado"] == "toca" and a["clave"] in ("limpiar", "fechas", "resto", "stock", "repetidos")]
if (esc["reglas"] or 0) < R.VERSION_REGLAS:
@@ -6748,9 +6838,11 @@ class Manejador(BaseHTTPRequestHandler):
tipo = d.get("tipo")
if not rc or tipo not in PLANTILLAS:
return self._json({"error": "carpeta o tipo de trabajo desconocido"}, 400)
antes = rc.get("tipo", "contenido")
rc["tipo"] = tipo
CONFIG.setdefault("clientes", {}).setdefault(rc["cliente"], {})["tipo"] = tipo
guardar_json(CONFIG_PATH, CONFIG)
bitacora({"op": "tipo", "raiz": k, "tipo": tipo, "antes": antes})
olvidar_plan(k)
return self._json({"ok": True, "tipo": tipo})
if ruta == "/api/clientes":
@@ -6785,6 +6877,8 @@ class Manejador(BaseHTTPRequestHandler):
DICC["carpetas"][tok] = cub
DICC["aprendido"].append({"ts": time.time(), "token": tok, "cubeta": cub})
guardar_json(DICC_PATH, DICC)
if cub and d.get("raiz"): # para el ✓ de «Decir qué son» en Inicio
bitacora({"op": "nombrar", "raiz": _clave(d["raiz"]), "token": tok, "cubeta": cub, "ejemplo": d.get("ejemplo") or tok})
return self._json({"ok": True})
if ruta == "/api/regla":
with LOCK: