Inicio dice lo que ya se hizo: el plan se olvida al mover y se vuelve a pensar solo

Auditoría de «mover y reorganizar». El motor movía bien, pero el plan en memoria solo se olvidaba al aplicar
desde «Qué va a pasar»: tras «Hacer las N de una vez», apartar stock, juntar repetidos, reemplazar o deshacer,
Inicio seguía ofreciendo lo ya hecho («5 cambios · Ver y aplicar», «Plan listo: 7 de 9») o, tras deshacer,
daba por hecho lo que volvía a estar en desorden. Ahora cualquier movimiento olvida el plan y lo vuelve a
pensar en segundo plano; un plan sin nada marcado es «hecho», cada ficha dice cuándo se hizo y cuántos
movimientos, y el diagnóstico no cuenta como basura lo que ya está en _Para borrar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-10-14 05:15:22 -05:00
padre 0c8caa0116
commit 91618474a0
Se han modificado 5 ficheros con 189 adiciones y 25 borrados

Ver fichero

@@ -880,6 +880,34 @@ Pruebas: `prueba_orden.py` 24 casos (5 nuevos: pieza, bandeja, tarjeta con una c
que rechazar, versiones con ffprobe simulado, y la pasada automática con sus frenos); `prueba_funcional.py` que rechazar, versiones con ffprobe simulado, y la pasada automática con sus frenos); `prueba_funcional.py`
29 de 29 (3 nuevas, por la interfaz de verdad del servidor); `probar.py` 19 y `prueba_ventana.py` 18. 29 de 29 (3 nuevas, por la interfaz de verdad del servidor); `probar.py` 19 y `prueba_ventana.py` 18.
3.6.0, Inicio dice lo que ya se hizo. Munka pidió auditar «mover y reorganizar»: «si hago el cambio debería
decir que el cambio se hizo y no mandarme para hacerlo y mostrarme lo que pasaría o lo que ahorraría cuando ya
lo había movido». Reproducido el 14-oct-2026 sobre una carpeta de ensayo: el motor mueve bien (plan → aplicar →
deshacer → volver a aplicar deja el árbol exacto y el inventario al día), pero Inicio no se enteraba:
- **El plan en memoria no se olvidaba al mover.** Solo lo olvidaba «Aplicar» desde «Qué va a pasar». Tras «Hacer
las N de una vez», apartar stock, juntar repetidos, reemplazar originales o «Deshacer», Inicio seguía con el
plan de antes: «Ordenar por año y mes · 5 cambios · Ver y aplicar» y «Plan listo: 7 de 9 movimientos marcados»
con todo ya movido; y tras deshacer, al revés, «lo seguro ya está hecho» con la carpeta otra vez en desorden.
Ahora cualquier movimiento (`actualizar_inventario`, y borrar carpetas vacías) olvida el plan y lo vuelve a
pensar en segundo plano (`olvidar_plan`); mientras tanto Inicio dice «comprobando qué queda…» y se refresca
solo. «Calcular» y la pasada automática esperan ese recálculo en vez de recorrer la carpeta dos veces, y la
pasada automática deja su plan en memoria para que por la mañana Inicio diga qué quedó. Si se intenta aplicar
un plan ya olvidado, el servidor contesta 409 y la interfaz lo recalcula en vez de mover filas que ya no
existen.
- **Un plan sin nada marcado contaba como pendiente.** Inicio decía «Plan listo: 0 de 2 movimientos marcados» y
mandaba a «Revisar y aplicar». Ahora es «hecho»: «Último cambio hoy 05:12 · 9 mov. · 2 dudosos sin marcar», y
«Siguiente» pasa a convertir o a respaldar.
- **Cada ficha dice cuándo se hizo.** «Ordenar lo demás · Hecho hoy a las 05:12: 9 movimientos»; «Basura y
proxies · 2 archivos ya en _Para borrar (0 MB): se borran cuando tú quieras»; y arriba, «Esta carpeta está al
día · último cambio 14 oct».
- **El diagnóstico no cuenta como basura lo ya apartado.** Después de apartar decía «2 archivos de basura de
sistema» (más que antes, porque los proxies y el stock movidos también se marcan como basura). Lo que está en
_Para borrar es un grupo aparte, «ya apartado», con su frase: «ahí se quedan hasta que tú los borres».
Pruebas: `prueba_funcional.py` comprueba, tras aplicar, deshacer y «hacer todo», lo que dice Inicio (hecho y
cuándo, nada vuelto a ofrecer, el diagnóstico sin contar lo apartado); 32 de 32.
3.5.0, «Qué va a pasar» se explica con una carpeta. Munka, con el plan de Diana Méndez delante: «se lee 3.5.0, «Qué va a pasar» se explica con una carpeta. Munka, con el plan de Diana Méndez delante: «se lee
mucho pero se entiende poco», «lo de ordenar por año y por mes, ¿me toca seleccionar uno a uno o lo hace mucho pero se entiende poco», «lo de ordenar por año y por mes, ¿me toca seleccionar uno a uno o lo hace
todo?», y quería una barra para saber por dónde va y si hace falta algo. Lo que había: tres cajas con un todo?», y quería una barra para saber por dónde va y si hace falta algo. Lo que había: tres cajas con un

Ver fichero

@@ -26,6 +26,7 @@ GRUPOS = [
("stock_reciente", "Stock de Envato reciente", "tomas de Envato de piezas en curso"), ("stock_reciente", "Stock de Envato reciente", "tomas de Envato de piezas en curso"),
("proxies", "Proxies", "copias ligeras que Resolve/Premiere regeneran solas"), ("proxies", "Proxies", "copias ligeras que Resolve/Premiere regeneran solas"),
("basura", "Basura de sistema y autoguardados", ".DS_Store, Thumbs.db, cachés, autosaves de After Effects"), ("basura", "Basura de sistema y autoguardados", ".DS_Store, Thumbs.db, cachés, autosaves de After Effects"),
("apartado", "Ya apartado en _Para borrar", "basura, proxies, stock viejo y originales reemplazados que la app ya apartó; se borran cuando tú quieras"),
("exportados", "Exportados / masters", "videos finales renderizados"), ("exportados", "Exportados / masters", "videos finales renderizados"),
("video_otro", "Otros videos", "videos que no son de cámara ni exportados (descargas, referencias, IA)"), ("video_otro", "Otros videos", "videos que no son de cámara ni exportados (descargas, referencias, IA)"),
("proyectos", "Proyectos de edición y diseño", ".drp, .prproj, .aep, .psd, .ai…"), ("proyectos", "Proyectos de edición y diseño", ".drp, .prproj, .aep, .psd, .ai…"),
@@ -37,7 +38,12 @@ TITULO = {k: t for k, t, _ in GRUPOS}
QUE_ES = {k: q for k, _, q in GRUPOS} QUE_ES = {k: q for k, _, q in GRUPOS}
def _grupo(f, limite_stock): def _grupo(f, limite_stock, papeleras=()):
# lo que ya está en _Para borrar no cuenta como basura, proxies o stock «por apartar»: ya se apartó.
# Antes, después de apartar, Inicio decía «2 archivos de basura» (más que antes de apartar, porque
# los proxies y el stock movidos también se marcan como basura) como si estuviera por hacer.
if papeleras and any(f["ruta"].lower().startswith(p) for p in papeleras):
return "apartado"
if f["es_basura"] or f["rol"] == "basura": if f["es_basura"] or f["rol"] == "basura":
return "basura" return "basura"
if f["es_proxy"] or f["rol"] == "proxy": if f["es_proxy"] or f["rol"] == "proxy":
@@ -70,8 +76,9 @@ def diagnostico(raiz):
"""Resumen corto y sencillo de lo que hay en la carpeta, por tipo de archivo.""" """Resumen corto y sencillo de lo que hay en la carpeta, por tipo de archivo."""
raiz = os.path.normpath(raiz) raiz = os.path.normpath(raiz)
con = O.db() con = O.db()
filas = con.execute("SELECT rol, bytes, mtime, es_basura, es_proxy, es_stock, contexto, fecha FROM archivos WHERE raiz = ?", filas = con.execute("SELECT rol, bytes, mtime, es_basura, es_proxy, es_stock, contexto, fecha, ruta FROM archivos WHERE raiz = ?",
(raiz,)).fetchall() (raiz,)).fetchall()
papeleras = tuple(os.path.join(raiz, p).lower() + os.sep for p in O.R.PAPELERAS)
esc = con.execute("SELECT * FROM escaneos WHERE raiz = ?", (raiz,)).fetchone() esc = con.execute("SELECT * FROM escaneos WHERE raiz = ?", (raiz,)).fetchone()
con.close() con.close()
meses = int(O.CONFIG["optimizar"].get("meses_minimos", 3)) meses = int(O.CONFIG["optimizar"].get("meses_minimos", 3))
@@ -79,7 +86,7 @@ def diagnostico(raiz):
acc = {k: {"clave": k, "titulo": TITULO[k], "que_es": QUE_ES[k], "n": 0, "bytes": 0, "desde": None, "hasta": None} for k, _, _ in GRUPOS} acc = {k: {"clave": k, "titulo": TITULO[k], "que_es": QUE_ES[k], "n": 0, "bytes": 0, "desde": None, "hasta": None} for k, _, _ in GRUPOS}
total_b = 0 total_b = 0
for f in filas: for f in filas:
g = acc[_grupo(f, limite)] g = acc[_grupo(f, limite, papeleras)]
g["n"] += 1 g["n"] += 1
g["bytes"] += f["bytes"] or 0 g["bytes"] += f["bytes"] or 0
total_b += f["bytes"] or 0 total_b += f["bytes"] or 0
@@ -103,6 +110,8 @@ def diagnostico(raiz):
frases.append(f"Los proxies suman {O._gb(b('proxies'))}: se regeneran solos, no hace falta guardarlos.") frases.append(f"Los proxies suman {O._gb(b('proxies'))}: se regeneran solos, no hace falta guardarlos.")
if acc["basura"]["n"]: if acc["basura"]["n"]:
frases.append(f"{acc['basura']['n']:,} archivos de basura de sistema o autoguardado ({O._gb(b('basura'))}).") frases.append(f"{acc['basura']['n']:,} archivos de basura de sistema o autoguardado ({O._gb(b('basura'))}).")
if acc["apartado"]["n"]:
frases.append(f"{acc['apartado']['n']:,} archivos ya apartados en «{O.R.PAPELERA}» ({O._gb(b('apartado'))}): ahí se quedan hasta que tú los borres.")
dup = O.duplicados(raiz) dup = O.duplicados(raiz)
if dup["grupos"]: if dup["grupos"]:
frases.append(f"Hay {dup['n']} archivos repetidos ({O._gb(dup['bytes'])}) en {len(dup['grupos'])} grupos: se pueden juntar en subcarpetas Duplicados para revisarlos a mano.") frases.append(f"Hay {dup['n']} archivos repetidos ({O._gb(dup['bytes'])}) en {len(dup['grupos'])} grupos: se pueden juntar en subcarpetas Duplicados para revisarlos a mano.")

Ver fichero

@@ -675,7 +675,7 @@ def escanear(ruta):
prog["estado"] = "listo" prog["estado"] = "listo"
if fallos: if fallos:
prog["error"] = f"{len(fallos)} sitios no se pudieron leer (el primero: {fallos[0]})" prog["error"] = f"{len(fallos)} sitios no se pudieron leer (el primero: {fallos[0]})"
PLANES.pop(raiz, None) # el plan anterior ya no vale: se calcula de nuevo sobre el inventario fresco olvidar_plan(raiz, recalcular=False) # el plan anterior ya no vale: se calcula de nuevo sobre el inventario fresco
except Exception as ex: # noqa except Exception as ex: # noqa
prog["estado"] = "error" prog["estado"] = "error"
prog["error"] = repr(ex) prog["error"] = repr(ex)
@@ -1975,6 +1975,9 @@ def actualizar_inventario(movidos):
fila["es_proxy"], fila["es_basura"])) fila["es_proxy"], fila["es_basura"]))
_recontar_carpetas(con, movidos) _recontar_carpetas(con, movidos)
con.close() con.close()
# el plan que hubiera en memoria ya no describe la carpeta: se olvida y se vuelve a pensar solo
for k in {r for de, a in movidos for r in (_raiz_de(de), _raiz_de(a)) if r}:
olvidar_plan(k)
def _recontar_carpetas(con, movidos): def _recontar_carpetas(con, movidos):
@@ -2155,6 +2158,8 @@ def borrar_vacias(raiz, rutas):
if e.get("lote") == lote and e.get("op") == "rmdir": if e.get("lote") == lote and e.get("op") == "rmdir":
con.execute("DELETE FROM carpetas WHERE ruta = ?", (e["de"],)) con.execute("DELETE FROM carpetas WHERE ruta = ?", (e["de"],))
con.close() con.close()
if borradas:
olvidar_plan(raiz)
bitacora({"lote": lote, "op": "fin", "raiz": raiz, "filas": borradas, "movimientos": borradas, "errores": errores}) bitacora({"lote": lote, "op": "fin", "raiz": raiz, "filas": borradas, "movimientos": borradas, "errores": errores})
return {"lote": lote, "borradas": borradas, "errores": errores} return {"lote": lote, "borradas": borradas, "errores": errores}
@@ -4428,9 +4433,53 @@ def copiar_respaldo(raiz, plan):
# ========================================================================== # ==========================================================================
PLANES = {} # raiz -> plan PLANES = {} # raiz -> plan
PLAN_GEN = defaultdict(int) # raiz -> cuántas veces se ha olvidado su plan: un recálculo viejo no pisa uno nuevo
REPLAN = {} # raiz -> el hilo que está volviendo a pensar el plan después de mover algo
PLANES_RESPALDO = {} PLANES_RESPALDO = {}
def olvidar_plan(raiz, recalcular=True):
"""Después de mover algo, el plan calculado ya no describe la carpeta: se olvida y, salvo que se diga lo
contrario, se vuelve a pensar en segundo plano. Así Inicio dice lo que de verdad queda (o que no queda
nada) en vez de ofrecer otra vez lo que se acaba de hacer, o de pedir que se calcule. Medido el
14-oct-2026 en el laboratorio: tras «Hacer las 5 de una vez», Inicio seguía diciendo «Ordenar por año y
mes · 5 cambios · Ver y aplicar» y «Plan listo: 7 de 9 movimientos marcados» con todo ya movido, porque
el plan solo se olvidaba al aplicar desde la pestaña «Qué va a pasar»; y tras «Deshacer», al revés:
«Lo seguro ya está hecho» con la carpeta otra vez en desorden."""
k = _clave(raiz)
PLANES.pop(k, None)
PLAN_GEN[k] += 1
if not recalcular or not raiz_cfg(k):
return
gen = PLAN_GEN[k]
def _pensar():
try:
plan = Plan(raiz_cfg(k)).correr()
if PLAN_GEN[k] == gen: # nadie movió nada más mientras tanto
PLANES[k] = plan
except Exception as ex: # noqa
print(f"no se pudo volver a pensar el plan de {k}: {ex}", file=sys.stderr)
finally:
if REPLAN.get(k) is threading.current_thread():
REPLAN.pop(k, None)
t = threading.Thread(target=_pensar, daemon=True)
REPLAN[k] = t
t.start()
def plan_en_marcha(raiz):
"""Si el plan de esta carpeta se está volviendo a pensar, espera a que acabe y lo devuelve; si no, None.
Lo usan «Calcular» y la pasada automática para no recorrer la carpeta dos veces seguidas."""
k = _clave(raiz)
t = REPLAN.get(k)
if t is None or t is threading.current_thread():
return None
t.join()
return PLANES.get(k)
def _clave(ruta): def _clave(ruta):
return os.path.normpath(ruta) return os.path.normpath(ruta)
@@ -5188,7 +5237,7 @@ def nueva_pieza(raiz, titulo, anio=None, mes=None, crear=True):
con.execute("INSERT OR IGNORE INTO carpetas VALUES (?,?,?,0,0,NULL,NULL,0,0,0,0)", (d, raiz, os.path.basename(d))) con.execute("INSERT OR IGNORE INTO carpetas VALUES (?,?,?,0,0,NULL,NULL,0,0,0,0)", (d, raiz, os.path.basename(d)))
con.close() con.close()
bitacora({"lote": lote, "op": "fin", "raiz": raiz, "filas": 1, "movimientos": len(creadas), "errores": []}) bitacora({"lote": lote, "op": "fin", "raiz": raiz, "filas": 1, "movimientos": len(creadas), "errores": []})
PLANES.pop(raiz, None) olvidar_plan(raiz)
return {**out, "ok": True, "lote": lote, "creadas": len(creadas)} return {**out, "ok": True, "lote": lote, "creadas": len(creadas)}
@@ -5522,11 +5571,11 @@ def automatico_una(raiz, limite):
out["errores"] = (res.get("errores") or [])[:20] out["errores"] = (res.get("errores") or [])[:20]
if asistente.ESTADO.get("error"): if asistente.ESTADO.get("error"):
out["errores"].append(asistente.ESTADO["error"]) out["errores"].append(asistente.ESTADO["error"])
despues = Plan(r).correr() despues = plan_en_marcha(raiz) or Plan(r).correr()
out["dudosos"] = sum(1 for f in despues["filas"] if not f["marcado"]) out["dudosos"] = sum(1 for f in despues["filas"] if not f["marcado"])
out["avisos"] = despues["avisos"][:20] out["avisos"] = despues["avisos"][:20]
out["estado"] = "con errores" if out["errores"] else "con dudas" if out["dudosos"] else "al dia" out["estado"] = "con errores" if out["errores"] else "con dudas" if out["dudosos"] else "al dia"
PLANES.pop(raiz, None) PLANES[despues["raiz"]] = despues # se queda: así Inicio dice por la mañana qué quedó, sin pedir que se calcule
return out return out
@@ -5668,6 +5717,34 @@ def automatico_por_comando():
return 1 return 1
_LOTES_OTROS = ("stock-", "dup-", "vacias-", "entrada-", "optimizar", "tanda-", "pieza-", "descartados-", "colores-", "respaldo")
def _ultimo_orden(raiz):
"""El último lote de orden (mover y reorganizar) aplicado sobre esta carpeta y no deshecho: cuándo y cuánto."""
ent = leer_bitacora(3000)
deshechos = {e.get("de_lote") for e in ent if e.get("op") == "deshacer"}
for e in reversed(ent):
l = e.get("lote") or ""
if e.get("op") == "fin" and (e.get("raiz") or "").lower() == raiz.lower() and l and not l.startswith(_LOTES_OTROS) and l not in deshechos:
return {"lote": l, "ts": e.get("ts"), "movimientos": e.get("movimientos", 0), "errores": len(e.get("errores") or [])}
return None
def _cuando(ts):
"""«hoy a las 05:02», «ayer a las 23:10», «el 3 de octubre a las 11:40»."""
if not ts:
return ""
t = time.localtime(ts)
hoy = time.localtime()
hora = time.strftime("%H:%M", t)
if t[:3] == hoy[:3]:
return f"hoy a las {hora}"
if t[:3] == time.localtime(time.time() - 86400)[:3]:
return f"ayer a las {hora}"
return f"el {t.tm_mday} de {R.MESES[t.tm_mon - 1]}" + (f" de {t.tm_year}" if t.tm_year != hoy.tm_year else "") + f" a las {hora}"
def acciones(raiz): def acciones(raiz):
"""Todo lo que se puede hacer AHORA MISMO en esta carpeta, en orden, cada cosa con su cifra. """Todo lo que se puede hacer AHORA MISMO en esta carpeta, en orden, cada cosa con su cifra.
@@ -5730,8 +5807,12 @@ def acciones(raiz):
detalle=f"Todo pasa a «{R.PAPELERA}». No se borra nada.", detalle=f"Todo pasa a «{R.PAPELERA}». No se borra nada.",
ir="plan", paso="limpiar") ir="plan", paso="limpiar")
else: else:
# lo ya apartado se dice, para que se vea que el cambio se hizo (antes la ficha quedaba muda)
apartado = con.execute("SELECT COUNT(*) n, COALESCE(SUM(bytes),0) b FROM archivos WHERE raiz = ? AND ("
+ " OR ".join(["ruta LIKE ? ESCAPE '!'"] * len(R.PAPELERAS)) + ")", [raiz] + args_pap).fetchone()
añadir(clave="limpiar", titulo="Basura y proxies", estado="hecho", añadir(clave="limpiar", titulo="Basura y proxies", estado="hecho",
que="No queda nada suelto que apartar.", cifra="", detalle="") que="No queda nada suelto que apartar.", cifra="",
detalle=f"{_plural(apartado['n'], 'archivo', 'archivos')} ya en «{R.PAPELERA}» ({_gb(apartado['b'])}): se borran cuando tú quieras." if apartado["n"] else "")
# --- 2. carpetas que la app no reconoce --- # --- 2. carpetas que la app no reconoce ---
voc = vocabulario(raiz) voc = vocabulario(raiz)
@@ -5754,6 +5835,8 @@ def acciones(raiz):
# --- 3 y 4. ordenar (usa el plan solo si ya estaba calculado) --- # --- 3 y 4. ordenar (usa el plan solo si ya estaba calculado) ---
plan = PLANES.get(raiz) plan = PLANES.get(raiz)
ultimo = _ultimo_orden(raiz)
hecho_det = f"Hecho {_cuando(ultimo['ts'])}: {_plural(ultimo['movimientos'], 'movimiento', 'movimientos')}." if ultimo else ""
for clave, titulo, que in (("fechas", "Ordenar por año y mes", for clave, titulo, que in (("fechas", "Ordenar por año y mes",
"Meses con nombre completo, grabaciones repartidas por mes y cámara, piezas numeradas."), "Meses con nombre completo, grabaciones repartidas por mes y cámara, piezas numeradas."),
("resto", "Ordenar lo demás", ("resto", "Ordenar lo demás",
@@ -5766,8 +5849,8 @@ def acciones(raiz):
# Si contara como pendiente, la carpeta no llegaría nunca a estar «al día». # Si contara como pendiente, la carpeta no llegaría nunca a estar «al día».
añadir(clave=clave, titulo=titulo, boton="Ver", que=que, estado="espera", añadir(clave=clave, titulo=titulo, boton="Ver", que=que, estado="espera",
cifra=_plural(len(fs), "dudoso", "dudosos"), cifra=_plural(len(fs), "dudoso", "dudosos"),
detalle="Lo seguro ya está hecho. Queda lo que la app no se atreve a mover sola: " detalle=(hecho_det + " " if hecho_det else "Lo seguro ya está hecho. ")
"míralo y marca lo que quieras.", ir="plan", paso=clave) + "Queda lo que la app no se atreve a mover sola: míralo y marca lo que quieras.", ir="plan", paso=clave)
elif fs: elif fs:
añadir(clave=clave, titulo=titulo, boton="Ver y aplicar", que=que, añadir(clave=clave, titulo=titulo, boton="Ver y aplicar", que=que,
cifra=f"{_plural(len(fs), 'cambio', 'cambios')} · {_gb(sum(f['bytes'] for f in fs))}", cifra=f"{_plural(len(fs), 'cambio', 'cambios')} · {_gb(sum(f['bytes'] for f in fs))}",
@@ -5775,12 +5858,16 @@ def acciones(raiz):
if len(marc) < len(fs) else "Todo se mueve dentro de la misma carpeta."), if len(marc) < len(fs) else "Todo se mueve dentro de la misma carpeta."),
ir="plan", paso=clave) ir="plan", paso=clave)
else: else:
añadir(clave=clave, titulo=titulo, estado="hecho", que="Nada que mover.", cifra="", detalle="") añadir(clave=clave, titulo=titulo, estado="hecho", que="Nada que mover.", cifra="", detalle=hecho_det)
elif REPLAN.get(raiz):
# se acaba de mover algo y el plan se está volviendo a pensar: en un momento se sabe qué queda
añadir(clave=clave, titulo=titulo, que=que, cifra="", estado="espera", calculando=True,
detalle=(hecho_det + " " if hecho_det else "") + "Comprobando qué queda…", ir="plan", paso=clave)
else: else:
# sin el plan calculado no se sabe si queda algo: se ofrece calcularlo, pero no cuenta como # sin el plan calculado no se sabe si queda algo: se ofrece calcularlo, pero no cuenta como
# pendiente (si no, después de ordenarlo todo la lista seguía diciendo que faltaban dos cosas) # pendiente (si no, después de ordenarlo todo la lista seguía diciendo que faltaban dos cosas)
añadir(clave=clave, titulo=titulo, boton="Calcular", que=que, cifra="", estado="espera", añadir(clave=clave, titulo=titulo, boton="Calcular", que=que, cifra="", estado="espera",
detalle="Hay que calcular el plan para saber si queda algo.", ir="plan", paso=clave) detalle=(hecho_det + " " if hecho_det else "") + "Hay que calcular el plan para saber si queda algo.", ir="plan", paso=clave)
# --- 5. carpetas que quedaron vacías --- # --- 5. carpetas que quedaron vacías ---
vac = carpetas_vacias(raiz) vac = carpetas_vacias(raiz)
@@ -5896,7 +5983,8 @@ def acciones(raiz):
auto = [a["clave"] for a in out if a["estado"] == "toca" and a["clave"] in ("limpiar", "fechas", "resto", "stock", "repetidos")] 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: if (esc["reglas"] or 0) < R.VERSION_REGLAS:
auto = [] # con un inventario viejo no se hace nada «de una vez»: primero se relee auto = [] # con un inventario viejo no se hace nada «de una vez»: primero se relee
return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": dict(esc), return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": dict(esc), "ultimo": ultimo,
"calculando": REPLAN.get(raiz) is not None,
"automatico": auto, "asistente": asistente.ESTADO if asistente.ESTADO.get("raiz", "").lower() == raiz.lower() else None} "automatico": auto, "asistente": asistente.ESTADO if asistente.ESTADO.get("raiz", "").lower() == raiz.lower() else None}
@@ -6060,7 +6148,7 @@ class Manejador(BaseHTTPRequestHandler):
r = raiz_cfg(q["raiz"]) r = raiz_cfg(q["raiz"])
if not r: if not r:
return self._json({"error": "raíz no configurada"}, 400) return self._json({"error": "raíz no configurada"}, 400)
plan = Plan(r).correr() plan = plan_en_marcha(r["ruta"]) or Plan(r).correr()
PLANES[plan["raiz"]] = plan PLANES[plan["raiz"]] = plan
return self._json(plan) return self._json(plan)
if ruta == "/api/bitacora": if ruta == "/api/bitacora":
@@ -6277,6 +6365,10 @@ class Manejador(BaseHTTPRequestHandler):
raiz = os.path.normpath(d["raiz"]) raiz = os.path.normpath(d["raiz"])
plan = PLANES.get(raiz) plan = PLANES.get(raiz)
if not plan: if not plan:
# el plan se olvidó porque la carpeta cambió (se movió algo, se volvió a escanear): los
# números de fila que trae la interfaz ya no dicen nada; se pide calcularlo otra vez
if PLAN_GEN.get(raiz):
return self._json({"error": "la carpeta cambió desde que se calculó el plan: hay que volver a calcular «Qué va a pasar»", "recalcular": True}, 409)
return self._json({"error": "primero genera el plan"}, 400) return self._json({"error": "primero genera el plan"}, 400)
ids = set(d.get("ids", [])) ids = set(d.get("ids", []))
filas = [f for f in plan["filas"] if f["id"] in ids] filas = [f for f in plan["filas"] if f["id"] in ids]
@@ -6289,7 +6381,8 @@ class Manejador(BaseHTTPRequestHandler):
return self._json({"error": f"el destino «{ed}» queda fuera de la carpeta del cliente"}, 400) return self._json({"error": f"el destino «{ed}» queda fuera de la carpeta del cliente"}, 400)
f["destino"] = nuevo f["destino"] = nuevo
res = aplicar(raiz, filas) res = aplicar(raiz, filas)
PLANES.pop(raiz, None) if not res.get("movimientos"):
olvidar_plan(raiz) # si algo se movió, ya lo olvidó el inventario
return self._json(res) return self._json(res)
if ruta == "/api/deshacer": if ruta == "/api/deshacer":
return self._json(deshacer(d["lote"])) return self._json(deshacer(d["lote"]))

Ver fichero

@@ -228,6 +228,13 @@ def p_plan():
LOTES = {} LOTES = {}
def acciones_quietas():
"""La lista de Inicio cuando el programa ha terminado de volver a pensar el plan tras mover algo."""
assert esperar(lambda: not get('/api/carpeta/acciones' + q(raiz=RAIZ))['calculando'], 60), 'el plan no se volvió a pensar'
a = get('/api/carpeta/acciones' + q(raiz=RAIZ))
return a, {x['clave']: x for x in a['acciones']}
def p_aplicar(): def p_aplicar():
antes = arbol() antes = arbol()
c, r = post('/api/aplicar', {'raiz': RAIZ, 'ids': [f['id'] for f in PLAN['filas'] if f['marcado']]}) c, r = post('/api/aplicar', {'raiz': RAIZ, 'ids': [f['id'] for f in PLAN['filas'] if f['marcado']]})
@@ -241,7 +248,17 @@ def p_aplicar():
assert sorted(os.path.basename(x) for x in antes) == sorted(os.path.basename(x) for x in despues), 'se perdió algún archivo' assert sorted(os.path.basename(x) for x in antes) == sorted(os.path.basename(x) for x in despues), 'se perdió algún archivo'
assert '10- Octubre' in os.listdir(os.path.join(RAIZ, '2026')), 'el mes sigue en minúsculas: %s' % os.listdir(os.path.join(RAIZ, '2026')) assert '10- Octubre' in os.listdir(os.path.join(RAIZ, '2026')), 'el mes sigue en minúsculas: %s' % os.listdir(os.path.join(RAIZ, '2026'))
assert not any('thumbs.db' in x and '_para borrar' not in x for x in despues), 'la basura sigue fuera' assert not any('thumbs.db' in x and '_para borrar' not in x for x in despues), 'la basura sigue fuera'
return '%d movimientos, sin errores, «10- octubre» pasó a «10- Octubre»; el avance acabó en %d de %d' % (r['movimientos'], av['unidades'], av['unidades_total']) # Inicio, sin que nadie vuelva a calcular: lo hecho sale como hecho (y cuándo), no como pendiente
a, por = acciones_quietas()
assert por['resto']['estado'] == 'hecho' and por['resto']['detalle'].startswith('Hecho hoy'), por['resto']
assert por['fechas']['estado'] != 'toca', por['fechas']
assert por['limpiar']['estado'] == 'hecho' and 'ya en' in por['limpiar']['detalle'], por['limpiar']
assert a['ultimo'] and a['ultimo']['lote'] == r['lote'], a['ultimo']
e = get('/api/carpeta/estado' + q(raiz=RAIZ))
assert e['plan'] and e['plan']['marcadas'] == 0, e['plan']
fr = e['diagnostico']['frases']
assert any('ya apartados' in f for f in fr) and not any('de basura de sistema' in f for f in fr), fr
return '%d movimientos, sin errores, «10- octubre» pasó a «10- Octubre»; el avance acabó en %d de %d; Inicio lo da por hecho' % (r['movimientos'], av['unidades'], av['unidades_total'])
def p_deshacer(): def p_deshacer():
@@ -249,7 +266,11 @@ def p_deshacer():
assert c == 200 and not r['errores'], r assert c == 200 and not r['errores'], r
assert '10- octubre' in os.listdir(os.path.join(RAIZ, '2026')), 'deshacer no devolvió las minúsculas: %s' % os.listdir(os.path.join(RAIZ, '2026')) assert '10- octubre' in os.listdir(os.path.join(RAIZ, '2026')), 'deshacer no devolvió las minúsculas: %s' % os.listdir(os.path.join(RAIZ, '2026'))
assert os.path.isfile(os.path.join(RAIZ, '2026', '03- Marzo', 'Grabacion', 'Thumbs.db')), 'la basura no volvió' assert os.path.isfile(os.path.join(RAIZ, '2026', '03- Marzo', 'Grabacion', 'Thumbs.db')), 'la basura no volvió'
return '%d movimientos deshechos' % r['deshechos'] # y en Inicio vuelve a estar todo por hacer (antes se quedaba con «lo seguro ya está hecho»)
a, por = acciones_quietas()
assert all(por[k]['estado'] == 'toca' for k in ('limpiar', 'fechas', 'resto')), {k: por[k]['estado'] for k in ('limpiar', 'fechas', 'resto')}
assert not a['ultimo'], a['ultimo']
return '%d movimientos deshechos, e Inicio vuelve a ofrecerlo todo' % r['deshechos']
def p_reaplicar(): def p_reaplicar():
@@ -448,7 +469,10 @@ def p_asistente():
assert esperar(lambda: not get('/api/estado')['asistente'].get('activo'), 120) assert esperar(lambda: not get('/api/estado')['asistente'].get('activo'), 120)
a = get('/api/estado')['asistente'] a = get('/api/estado')['asistente']
assert not a.get('error'), a.get('error') assert not a.get('error'), a.get('error')
return 'pasos + «hacer las N de una vez» terminan sin error' # después, Inicio no vuelve a ofrecer lo que acaba de hacer
_, por = acciones_quietas()
assert por['fechas']['estado'] != 'toca' and por['resto']['estado'] != 'toca', {k: por[k] for k in ('fechas', 'resto')}
return 'pasos + «hacer las N de una vez» terminan sin error, e Inicio no lo vuelve a ofrecer'
def p_pieza(): def p_pieza():

Ver fichero

@@ -1137,8 +1137,8 @@ function desconectar(clave){
$('#btnDesconectar').onclick = () => desconectar(raizActual()); $('#btnDesconectar').onclick = () => desconectar(raizActual());
// ---------------- carpeta ---------------- // ---------------- carpeta ----------------
const COLOR = {camara_raw:'#6b2f2a', stock_viejo:'#b8923a', stock_reciente:'#d9c58c', camara_video:'#3d5a73', exportados:'#7f8a94', proyectos:'#a9b0b8', basura:'#d3d6da', proxies:'#c4c9ce', audio:'#4f6b5a', celular:'#7a5c8a', fotos:'#8a6f4f', video_otro:'#94a3ad', imagenes:'#b5bec6', documentos:'#cfd5da', otros:'#e1e5e8'}; const COLOR = {camara_raw:'#6b2f2a', stock_viejo:'#b8923a', stock_reciente:'#d9c58c', camara_video:'#3d5a73', exportados:'#7f8a94', proyectos:'#a9b0b8', basura:'#d3d6da', proxies:'#c4c9ce', audio:'#4f6b5a', celular:'#7a5c8a', fotos:'#8a6f4f', video_otro:'#94a3ad', imagenes:'#b5bec6', documentos:'#cfd5da', otros:'#e1e5e8', apartado:'#cfd4d8'};
const NOMBRE_CORTO = {camara_raw:'BRAW', stock_viejo:'stock viejo', stock_reciente:'stock reciente', camara_video:'cámara MP4', exportados:'exportados', proyectos:'proyectos', basura:'basura', proxies:'proxies', audio:'audio', celular:'celular', fotos:'fotos', video_otro:'otros videos', imagenes:'imágenes', documentos:'documentos', otros:'otros'}; const NOMBRE_CORTO = {camara_raw:'BRAW', stock_viejo:'stock viejo', stock_reciente:'stock reciente', camara_video:'cámara MP4', exportados:'exportados', proyectos:'proyectos', basura:'basura', proxies:'proxies', audio:'audio', celular:'celular', fotos:'fotos', video_otro:'otros videos', imagenes:'imágenes', documentos:'documentos', otros:'otros', apartado:'ya apartado'};
function pintarPortada(){ function pintarPortada(){
// las carpetas que el programa ya conoce, para entrar en una con un clic; con la última vez que se // las carpetas que el programa ya conoce, para entrar en una con un clic; con la última vez que se
// escaneó cada una. Es el «historial» de la portada: nada se abre solo. // escaneó cada una. Es el «historial» de la portada: nada se abre solo.
@@ -1207,10 +1207,12 @@ async function pintarAcciones(){
const corriendo = asis && asis.activo; const corriendo = asis && asis.activo;
const cab = corriendo const cab = corriendo
? `<div class="cabsec">Trabajando en esta carpeta<span class="sep"></span><span class="mut" style="text-transform:none;letter-spacing:0">${esc(NOMBRE_PASO[asis.paso] || asis.paso || '')} · ${asis.hechas || 0} de ${asis.total || 0}</span></div>` ? `<div class="cabsec">Trabajando en esta carpeta<span class="sep"></span><span class="mut" style="text-transform:none;letter-spacing:0">${esc(NOMBRE_PASO[asis.paso] || asis.paso || '')} · ${asis.hechas || 0} de ${asis.total || 0}</span></div>`
: `<div class="cabsec">${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día'}${auto.length > 1 ? '<span class="sep"></span><button class="p" id="btnHacerTodo" style="text-transform:none;letter-spacing:0">Hacer las ' + auto.length + ' de una vez</button>' : ''}</div>`; : `<div class="cabsec">${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día' + (d.ultimo ? ` · último cambio ${esc(fechaCorta(d.ultimo.ts))}` : '')}${auto.length > 1 ? '<span class="sep"></span><button class="p" id="btnHacerTodo" style="text-transform:none;letter-spacing:0">Hacer las ' + auto.length + ' de una vez</button>' : ''}</div>`;
$('#acciones').innerHTML = cab + $('#acciones').innerHTML = cab +
ACCIONES.map((a, i) => `<div class="ac ${a.estado}"><span class="ico">${a.estado === 'hecho' ? '✓' : a.estado === 'espera' ? '·' : i + 1}</span><div class="tx"><b>${esc(a.titulo)}</b><div class="q">${esc(a.que)}</div>${a.detalle ? `<div class="d">${esc(a.detalle)}</div>` : ''}</div><div class="der">${a.cifra ? `<span class="cifra">${esc(a.cifra)}</span>` : ''}${a.boton ? `<button class="${a.estado === 'espera' ? 'lnk' : 'p'}" data-a="${i}">${esc(a.boton)}</button>` : ''}</div></div>`).join(''); ACCIONES.map((a, i) => `<div class="ac ${a.estado}"><span class="ico">${a.calculando ? '<span class="spin"></span>' : a.estado === 'hecho' ? '✓' : a.estado === 'espera' ? '·' : i + 1}</span><div class="tx"><b>${esc(a.titulo)}</b><div class="q">${esc(a.que)}</div>${a.detalle ? `<div class="d">${esc(a.detalle)}</div>` : ''}</div><div class="der">${a.cifra ? `<span class="cifra">${esc(a.cifra)}</span>` : ''}${a.boton ? `<button class="${a.estado === 'espera' ? 'lnk' : 'p'}" data-a="${i}">${esc(a.boton)}</button>` : ''}</div></div>`).join('');
$$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso); }); $$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso); });
// se acaba de mover algo y el programa está comprobando qué queda: se vuelve a preguntar en un momento
if (d.calculando) setTimeout(() => { if (raizActual() === raiz && vistaActual() === 'carpeta') cargarCarpeta().catch(() => {}); }, 1500);
const bt = $('#btnHacerTodo'); const bt = $('#btnHacerTodo');
if (bt) bt.onclick = () => modal('Hacer todo lo seguro de una vez', if (bt) bt.onclick = () => modal('Hacer todo lo seguro de una vez',
`<p>La app va a hacer seguidas las <b>${auto.length}</b> cosas de la lista que no necesitan que elijas nada, en orden y sin preguntar.</p> `<p>La app va a hacer seguidas las <b>${auto.length}</b> cosas de la lista que no necesitan que elijas nada, en orden y sin preguntar.</p>
@@ -1250,7 +1252,9 @@ function pasosDe(C, r){
const lotes = (C.lotes || []).filter(l => !l.lote.startsWith('stock-') && !l.lote.startsWith('optimizar') && !l.lote.startsWith('descartados-') && !l.lote.startsWith('tanda-')); const lotes = (C.lotes || []).filter(l => !l.lote.startsWith('stock-') && !l.lote.startsWith('optimizar') && !l.lote.startsWith('descartados-') && !l.lote.startsWith('tanda-'));
const asis = C.asistente && C.asistente.activo; const asis = C.asistente && C.asistente.activo;
P.push(!esc ? {e:'bloq', t:'Ordenar', d:'Después de escanear'} : asis ? {e:'curso', t:'Ordenar', d: (NOMBRE_PASO[C.asistente.paso] || '') + '…', corto: 'en marcha'} P.push(!esc ? {e:'bloq', t:'Ordenar', d:'Después de escanear'} : asis ? {e:'curso', t:'Ordenar', d: (NOMBRE_PASO[C.asistente.paso] || '') + '…', corto: 'en marcha'}
: C.plan ? {e:'listo', t:'Ordenar', d:`Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`, corto: `${C.plan.marcadas} mov.`} : C.plan && C.plan.marcadas ? {e:'listo', t:'Ordenar', d:`Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`, corto: `${C.plan.marcadas} mov.`}
// un plan sin nada marcado es una carpeta en orden (quizá con dudosos): hecho, no «listo para aplicar»
: C.plan ? {e:'hecho', t:'Ordenar', d: (lotes.length ? `Último cambio ${fecha(lotes[0].ts)} · ${lotes[0].movimientos} mov.` : 'Nada que mover') + (C.plan.filas ? ` · ${C.plan.filas} dudoso${C.plan.filas > 1 ? 's' : ''} sin marcar` : ''), corto: lotes.length ? fechaCorta(lotes[0].ts) : 'ok'}
: lotes.length ? {e:'hecho', t:'Ordenar', d:`Último cambio ${fecha(lotes[0].ts)} · ${lotes[0].movimientos} mov.`, corto: fechaCorta(lotes[0].ts)} : {e:'pend', t:'Ordenar', d:'Calcula qué va a pasar y aplica', corto: ''}); : lotes.length ? {e:'hecho', t:'Ordenar', d:`Último cambio ${fecha(lotes[0].ts)} · ${lotes[0].movimientos} mov.`, corto: fechaCorta(lotes[0].ts)} : {e:'pend', t:'Ordenar', d:'Calcula qué va a pasar y aplica', corto: ''});
const cv = C.convertir || {}; const tr = cv.trabajos || {}; const n = k => (tr[k] || {}).n || 0; const cv = C.convertir || {}; const tr = cv.trabajos || {}; const n = k => (tr[k] || {}).n || 0;
const activos = n('pendiente') + n('corriendo'), listos = n('listo'), verif = n('verificado'), reemp = n('reemplazado'); const activos = n('pendiente') + n('corriendo'), listos = n('listo'), verif = n('verificado'), reemp = n('reemplazado');
@@ -1281,7 +1285,7 @@ function pintarSiguiente(P){
else if (P[0].e === 'pend') { s.t = 'Empieza por escanear'; s.p = 'La app lee los nombres, tamaños y fechas de todos los archivos. No mueve nada. En una carpeta grande del NAS tarda unos minutos.'; b('Escanear ahora', () => $('#btnEscanear').click(), true); } else if (P[0].e === 'pend') { s.t = 'Empieza por escanear'; s.p = 'La app lee los nombres, tamaños y fechas de todos los archivos. No mueve nada. En una carpeta grande del NAS tarda unos minutos.'; b('Escanear ahora', () => $('#btnEscanear').click(), true); }
else if (P[2].e === 'curso') { s.t = 'El asistente está trabajando'; s.p = 'Puedes seguir el avance en la barra de abajo.'; } else if (P[2].e === 'curso') { s.t = 'El asistente está trabajando'; s.p = 'Puedes seguir el avance en la barra de abajo.'; }
else if (P[1].e === 'listo' && !C.plan && P[2].e !== 'hecho') { const n = C.sin_nombre_n; s.t = `Antes de ordenar: nombra ${n} carpeta${n > 1 ? 's' : ''}`; s.p = `La app no sabe qué son ${C.sin_nombre.slice(0, 4).map(x => '«' + x.ejemplo + '»').join(', ')}${n > 4 ? ' y otras' : ''}. Si le dices qué es cada una, las ordena bien y se lo aprende para todos los discos. Si prefieres, sáltalo: quedarán como «dudoso» y no se moverán.`; b('Nombrar las carpetas', () => { irA('revisar'); subRev('nombrar'); }, true); b('Saltar y calcular', () => { irA('revisar'); subRev('plan'); $('#btnPlan').click(); }); } else if (P[1].e === 'listo' && !C.plan && P[2].e !== 'hecho') { const n = C.sin_nombre_n; s.t = `Antes de ordenar: nombra ${n} carpeta${n > 1 ? 's' : ''}`; s.p = `La app no sabe qué son ${C.sin_nombre.slice(0, 4).map(x => '«' + x.ejemplo + '»').join(', ')}${n > 4 ? ' y otras' : ''}. Si le dices qué es cada una, las ordena bien y se lo aprende para todos los discos. Si prefieres, sáltalo: quedarán como «dudoso» y no se moverán.`; b('Nombrar las carpetas', () => { irA('revisar'); subRev('nombrar'); }, true); b('Saltar y calcular', () => { irA('revisar'); subRev('plan'); $('#btnPlan').click(); }); }
else if (C.plan) { s.t = `Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`; s.p = `Todo se mueve dentro de la misma carpeta y nada se borra. ${C.plan.baja ? C.plan.baja + ' son dudosos y están sin marcar. ' : ''}Revisa la lista y aplica; se puede deshacer desde Historial.`; b('Revisar y aplicar', () => { irA('revisar'); subRev('plan'); }, true); } else if (C.plan && C.plan.marcadas) { s.t = `Plan listo: ${C.plan.marcadas} de ${C.plan.filas} movimientos marcados`; s.p = `Todo se mueve dentro de la misma carpeta y nada se borra. ${C.plan.baja ? C.plan.baja + ' son dudosos y están sin marcar. ' : ''}Revisa la lista y aplica; se puede deshacer desde Historial.`; b('Revisar y aplicar', () => { irA('revisar'); subRev('plan'); }, true); }
else if (P[2].e === 'pend') { s.t = 'Siguiente: ordenar la carpeta'; s.p = 'Calcula qué va a pasar (basura y proxies a _Para borrar, grabaciones por mes y cámara, piezas numeradas) y aplícalo con un clic. O deja que el asistente te pregunte paso a paso.'; b('Calcular qué va a pasar', () => { irA('revisar'); subRev('plan'); $('#btnPlan').click(); }, true); } else if (P[2].e === 'pend') { s.t = 'Siguiente: ordenar la carpeta'; s.p = 'Calcula qué va a pasar (basura y proxies a _Para borrar, grabaciones por mes y cámara, piezas numeradas) y aplícalo con un clic. O deja que el asistente te pregunte paso a paso.'; b('Calcular qué va a pasar', () => { irA('revisar'); subRev('plan'); $('#btnPlan').click(); }, true); }
else if (P[3].e === 'listo') { const d = P[3].d; s.t = 'Siguiente: ' + (d.includes('verificar') ? 'verificar los clips convertidos' : d.includes('reemplazar') ? 'reemplazar los originales' : 'retomar la conversión'); s.p = d.includes('verificar') ? 'Verificar comprueba códec, 10 bits y duración. Después puedes reemplazar los originales (quedan en _Para borrar/originales).' : d.includes('reemplazar') ? 'Los originales se apartan a _Para borrar/originales y el MP4 10 bits queda en su lugar. Nada se borra.' : 'Hay clips en cola con la conversión parada.'; b('Ir a Convertir', () => irA('convertir'), true); } else if (P[3].e === 'listo') { const d = P[3].d; s.t = 'Siguiente: ' + (d.includes('verificar') ? 'verificar los clips convertidos' : d.includes('reemplazar') ? 'reemplazar los originales' : 'retomar la conversión'); s.p = d.includes('verificar') ? 'Verificar comprueba códec, 10 bits y duración. Después puedes reemplazar los originales (quedan en _Para borrar/originales).' : d.includes('reemplazar') ? 'Los originales se apartan a _Para borrar/originales y el MP4 10 bits queda en su lugar. Nada se borra.' : 'Hay clips en cola con la conversión parada.'; b('Ir a Convertir', () => irA('convertir'), true); }
else if (P[3].e === 'curso') { s.t = 'Convirtiendo clips'; s.p = 'Sigue en segundo plano. El avance está en la barra de abajo; cuando termine, toca reemplazar.'; b('Ver la cola', () => irA('convertir')); } else if (P[3].e === 'curso') { s.t = 'Convirtiendo clips'; s.p = 'Sigue en segundo plano. El avance está en la barra de abajo; cuando termine, toca reemplazar.'; b('Ver la cola', () => irA('convertir')); }
@@ -1593,7 +1597,13 @@ function aplicarFilas(m, btn, titulo){
// mientras el servidor mueve, se le pregunta por dónde va (archivo a archivo); lo de una tanda anterior no vale // mientras el servidor mueve, se le pregunta por dónde va (archivo a archivo); lo de una tanda anterior no vale
const reloj = setInterval(async () => { try { const a = await api('/api/aplicando'); if (a.inicio && a.inicio * 1000 >= t0 - 2000 && !a.fin) pintarAvance({activo: true, ...a, total: m.length}); } catch (e) {} }, 400); const reloj = setInterval(async () => { try { const a = await api('/api/aplicando'); if (a.inicio && a.inicio * 1000 >= t0 - 2000 && !a.fin) pintarAvance({activo: true, ...a, total: m.length}); } catch (e) {} }, 400);
let res; let res;
try { res = await api('/api/aplicar', {raiz, ids: m.map(r => r.id), destinos}); } catch (e) { clearInterval(reloj); $('#planAvance').innerHTML = ''; throw e; } try { res = await api('/api/aplicar', {raiz, ids: m.map(r => r.id), destinos}); }
catch (e) {
clearInterval(reloj); $('#planAvance').innerHTML = '';
// la carpeta cambió desde que se calculó (se apartó stock, se deshizo algo…): se calcula otra vez y ya
if (e.datos && e.datos.recalcular) { aviso('La carpeta cambió desde que se calculó el plan. Se vuelve a calcular; revísalo y aplica.', 'bad'); PLAN = null; await calcularPlan(); return; }
throw e;
}
clearInterval(reloj); clearInterval(reloj);
PLAN = null; MAPA = null; PLAN = null; MAPA = null;
pintarAvance({activo: false, res, seg: (Date.now() - t0) / 1000, pedidas: m.length, bytes}); pintarAvance({activo: false, res, seg: (Date.now() - t0) / 1000, pedidas: m.length, bytes});