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:
102
organizador.py
102
organizador.py
@@ -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:
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Referencia en una nueva incidencia
Block a user