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:

Ver fichero

@@ -209,7 +209,9 @@ def p_tipo_y_pesa():
assert c == 200, r
e = get('/api/estado')
assert e['config']['clientes']['Cliente Smoke']['tipo'] == 'contenido' and raiz_estado()['tipo'] == 'contenido'
assert 'tipo' not in [x['clave'] for x in get('/api/carpeta/acciones' + q(raiz=RAIZ))['acciones']]
# y la ficha se queda, hecha, con lo que se eligió y cuándo: no desaparece
t = {x['clave']: x for x in get('/api/carpeta/acciones' + q(raiz=RAIZ))['acciones']}['tipo']
assert t['estado'] == 'hecho' and 'Piezas mes a mes' in (t.get('hecho') or ''), t
return '«Qué pesa» entra hasta %d niveles bajo 2026; el tipo de la carpeta y el del cliente se igualan' % d2['niveles']

Ver fichero

@@ -260,6 +260,11 @@
.acc{display:flex;flex-direction:column;gap:8px;margin-bottom:18px}
.ac{display:grid;grid-template-columns:26px minmax(0,1fr) auto;gap:14px;align-items:center;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 14px}
.ac.hecho{background:transparent;border-style:dashed;opacity:.62}
.ac .hc{margin-top:4px;font-size:12.5px;font-weight:600;color:var(--ok)}
.ac.hecho .hc{opacity:1}
.ac.recien{opacity:1;animation:recien 3s ease-out}
@keyframes recien{0%{background:var(--ok2);border-color:var(--ok);border-style:solid}70%{background:var(--ok2)}100%{background:transparent}}
.cabsec .nhechas{margin-left:10px;color:var(--ok);text-transform:none;letter-spacing:0;font-weight:600}
.ac.espera{opacity:.72}
.ac .ico{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;flex:none;background:var(--acc2);color:var(--acc)}
.ac.hecho .ico{background:var(--ok2);color:var(--ok)}
@@ -1225,7 +1230,7 @@ const IR_ACCION = {
await api('/api/raiz/tipo', {raiz: raizActual(), tipo: t});
PLAN = null; MAPAE = null;
aviso(`Listo: la carpeta y el cliente se ordenan como «${(ESTADO.tipos[t] || t).split(' · ')[0]}».`, 'ok');
await cargarEstado(); cargarVista();
await cargarEstado(); await pintarAcciones(); cargarVista();
}));
$('#modalSi').textContent = 'Usar este';
},
@@ -1251,16 +1256,18 @@ async function pintarAcciones(){
const raiz = raizActual(); if (!raiz) return;
const d = await api('/api/carpeta/acciones?raiz=' + encodeURIComponent(raiz));
if (raizActual() !== raiz) return; // cambió de carpeta mientras llegaba
// lo que acaba de pasar a hecho se ilumina un momento: así se ve que el cambio se hizo
const previas = $('#acciones').dataset.raiz === raiz ? Object.fromEntries(ACCIONES.map(a => [a.clave, a.estado])) : {};
ACCIONES = d.acciones || [];
$('#acciones').dataset.raiz = raiz; // de qué carpeta es lo que se ve: así se sabe cuándo decir «leyendo»
const quedan = ACCIONES.filter(a => a.estado === 'toca').length;
const quedan = ACCIONES.filter(a => a.estado === 'toca').length, hechas = ACCIONES.filter(a => a.estado === 'hecho').length;
const auto = d.automatico || [], asis = d.asistente;
const corriendo = asis && asis.activo;
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">${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>`;
: `<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))}` : '')}${hechas ? `<span class="nhechas">✓ ${hechas} hecho${hechas !== 1 ? 's' : ''}</span>` : ''}${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.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.map((a, i) => `<div class="ac ${a.estado} ${previas[a.clave] && previas[a.clave] !== 'hecho' && a.estado === 'hecho' ? 'recien' : ''}" data-clave="${esc(a.clave)}"><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>` : ''}${a.hecho ? `<div class="hc">✓ ${esc(a.hecho)}</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, a); });
// 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);
@@ -1484,13 +1491,13 @@ function pintarNombrar(){
}
$('#btnAceptarSug').onclick = () => accion($('#btnAceptarSug'), async () => {
const seguras = VOC.filter(t => !t.cubeta && !t.dentro_pieza && t.sugerencia && t.sug_conf === 'alta');
for (const t of seguras) { await api('/api/diccionario', {token: t.token, cubeta: t.sugerencia}); t.cubeta = t.sugerencia; t.aprendido = true; }
for (const t of seguras) { await api('/api/diccionario', {token: t.token, cubeta: t.sugerencia, raiz: raizActual(), ejemplo: t.ejemplo}); t.cubeta = t.sugerencia; t.aprendido = true; }
PLAN = null; estadoPlan(); if (CARPETA) { CARPETA.sin_nombre_n = VOC.filter(x => !x.cubeta && !x.dentro_pieza).length; pintarIndice(); }
pintarNombrar(); aviso(`${seguras.length} carpetas nombradas. Calcula el plan para verlas ordenadas.`, 'ok');
});
const nombrarToken = seguro(async (tok, cub) => {
await api('/api/diccionario', {token: tok, cubeta: cub});
const t = VOC.find(x => x.token === tok); if (t) { t.cubeta = cub; t.aprendido = !!cub; t.motivo = 'lo dijiste tú'; }
const t = VOC.find(x => x.token === tok);
await api('/api/diccionario', {token: tok, cubeta: cub, raiz: raizActual(), ejemplo: t ? t.ejemplo : tok}); if (t) { t.cubeta = cub; t.aprendido = !!cub; t.motivo = 'lo dijiste tú'; }
PLAN = null; estadoPlan(); if (CARPETA) { CARPETA.sin_nombre_n = VOC.filter(x => !x.cubeta && !x.dentro_pieza).length; pintarIndice(); }
pintarNombrar();
if (cub) aviso(`«${t ? t.ejemplo : tok}» ahora es ${NOMBRE_CUBETA[cub] || cub}. Calcula el plan de nuevo para verlo.`, 'ok');