From 1e1187d28aad19c541a232108df4fbfa49487309 Mon Sep 17 00:00:00 2001 From: Labstream Date: Wed, 16 Sep 2026 21:53:50 -0500 Subject: [PATCH] 2.1.0: el asistente se fusiona con la lista de acciones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Había dos caminos para lo mismo. Ahora encima de la lista sale «Hacer las N de una vez», que ejecuta seguidas las acciones que no necesitan elegir nada, con el mismo motor que tenía el asistente. Convertir y respaldar se quedan fuera porque hay que elegir; lo dudoso nunca entra. - POST /api/acciones/hacer; acciones() trae y el progreso. - Se quita el punto de entrada «Asistente paso a paso» de la interfaz (el motor y la entrada por línea de comandos siguen). - Las acciones de ordenar no cuentan como pendientes sin un plan calculado: tras ordenarlo todo la lista seguía diciendo que faltaban dos cosas. Probado de punta a punta: 19 archivos antes y después, ninguno perdido, árbol completo ordenado en un clic. Co-Authored-By: Claude Opus 5 --- README.md | 14 ++++++++++++++ manifiesto.json | 20 ++++++++++---------- organizador.py | 28 ++++++++++++++++++++++++---- static/index.html | 28 +++++++++++++++++++++------- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c25376b..aaa4dc2 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,20 @@ el inventario, así que tarda entre 0,04 y 0,23 s en las carpetas reales; el pla ya estaba calculado, y si no el botón dice «Calcular». De paso se conecta a la interfaz la detección de material repetido, que funcionaba por dentro pero no tenía dónde pulsarse, y `_gb` pasa a dar terabytes. +2.1.0, el asistente se fusiona con la lista. Había dos formas de hacer lo mismo: la lista de acciones y un +«Asistente paso a paso» que recorría lo mismo preguntando. Ahora hay una sola: encima de la lista sale +**«Hacer las N de una vez»**, que ejecuta seguidas las acciones que no necesitan que elijas nada (apartar +basura y proxies, ordenar por fecha, ordenar lo demás, apartar stock viejo, juntar repetidos) usando el mismo +motor que tenía el asistente. Se quedan fuera convertir y respaldar, que tienen su propio botón porque hay +que elegir. Lo dudoso nunca entra: se queda como está para que alguien lo mire. + +Probado de punta a punta sobre un árbol de ensayo: 19 archivos antes y después, ninguno perdido, y en un solo +clic el material de cámara quedó repartido por mes y fuente, las piezas en su mes, la música en `_Recursos` y +la basura, los proxies y el stock viejo en `_Para borrar`. Endpoint `POST /api/acciones/hacer`; la lista de +acciones trae `automatico` (las que se pueden hacer solas) y el progreso del motor. De paso, las acciones de +ordenar dejan de contar como pendientes mientras no haya un plan calculado: después de ordenarlo todo la +lista seguía diciendo que faltaban dos cosas que en realidad ya no tenían nada. + Probado el 9 sep 2026 sobre una copia de ensayo: plan → aplicar → deshacer → volver a aplicar deja el árbol exacto; conversión ffmpeg verificada y reemplazo con original a `_Para borrar`. El puente de Resolve conectó con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos: diff --git a/manifiesto.json b/manifiesto.json index 4a6ae01..5f02fd5 100644 --- a/manifiesto.json +++ b/manifiesto.json @@ -1,19 +1,19 @@ { "nombre": "OrganiLAB", - "version": "2.0.0", - "builtAt": "2026-09-16T16:44:23.000Z", - "archivo": "OrganizadorLabstream-portatil-2.0.0.zip", - "size": 198696162, - "sha256": "dbde9ca9d11f8b2620cea2ba216cf4705c3ddd66b1310cbaf0935a7a8a0345c1", + "version": "2.1.0", + "builtAt": "2026-09-17T02:51:40.000Z", + "archivo": "OrganizadorLabstream-portatil-2.1.0.zip", + "size": 198698005, + "sha256": "1ba2cd2dde181b42c6de5cc0860fa52d6d201c874bb1b28285200f984c240bf4", "app": { - "archivo": "OrganizadorLabstream-app-2.0.0.zip", - "size": 14876413, - "sha256": "f27d51517adaad20dd43a07697049a5b44469b846db65d47f0b0be97e8e4294e", - "url": "/api/herramientas/OrganizadorLabstream-app-2.0.0.zip" + "archivo": "OrganizadorLabstream-app-2.1.0.zip", + "size": 14878256, + "sha256": "381cbd045a79c4ed920d4101aabb8b234034f93b99b816732bc8b5c2dd7d73b1", + "url": "/api/herramientas/OrganizadorLabstream-app-2.1.0.zip" }, "notes": "", "firma": { "alg": "rsa2048-pkcs1v15-sha256", - "valor": "09b248add12c0bc1ed332398801452ebf3dba6a74bf6e6f94909c5dfbb74a00b85f8aa393ee3edffa700d95b381ba918dbaa0b991d9646963d0b0508a76f4904f952ed2bdf2a4bc8aa66e7ea4edc1b6ef11c04bc1d959a93aca309937db7b662c698d9356984dec3f4a1033e4f844cdc86730a1cd5228a32d40c9ce997453379da96e3055a24fd511e3062cbea029cccf54f05fc95e16cc257abd2fc53cfdcda7bf81695daabbb2e905bcfb378fa1c394b341b70f8e3048c0afb1bba4a99ce53b19433c50ea419e6420a6e17d279b721b56a565d408c133b8cb71de22fdd6b67ead86db2fa80f2c9749d584e7b8b17f2c42d631881695a6efd5d6daf76ecff22" + "valor": "944a227ccc3edf2bc3038ca2d6ab507efac49c5b3384df1a6659073e9a88e84e853b73617e47f03ad53bbac04a24bb37082d38bdc918b6c22bd8c93158d75ec34190abe1fa082b8eb0bf3efb80a42dd80bbac6e1b28d839675654f674e82ab8023eb10b76d1df198cb14bcd847edee12b74f89851160937768981be8e327ae151b94bf1c38339d1e0210a653d593f54d26a3c6140f0401dcfe0b039fbf20b188a14f263f19242824f1c351f3837c5bd07d486a0c5e18376f3c30e2e1e61013e18b66883b659a326f5c38a875527372013ec111c579ddd50ee7d3c4aa4fe757971b3b022cc9ea004472db397d5ab189a29c5450ba8b7b43a8c5f767b0b7d3087e" } } \ No newline at end of file diff --git a/organizador.py b/organizador.py index 2299d8b..58ba0f3 100644 --- a/organizador.py +++ b/organizador.py @@ -34,7 +34,7 @@ import actualizador asistente.O = sys.modules[__name__] actualizador.O = sys.modules[__name__] -VERSION = "2.0.0" +VERSION = "2.1.0" CONGELADO = getattr(sys, "frozen", False) SIN_VENTANA = getattr(subprocess, "CREATE_NO_WINDOW", 0) # ffmpeg/ffprobe/net sin abrir consolas negras # empaquetado como .exe con PyInstaller @@ -2782,8 +2782,10 @@ def acciones(raiz): else: añadir(clave=clave, titulo=titulo, estado="hecho", que="Nada que mover.", cifra="", detalle="") else: - añadir(clave=clave, titulo=titulo, boton="Calcular", que=que, cifra="", - detalle="Hay que calcular el plan para saber cuánto es.", ir="plan", paso=clave) + # 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) + 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) # --- 5. stock de Envato viejo --- tope = limite_meses(3) @@ -2842,7 +2844,10 @@ def acciones(raiz): ir="respaldar") con.close() - return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": dict(esc)} + # «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")] + return {"raiz": raiz, "cliente": r["cliente"], "acciones": out, "escaneo": dict(esc), + "automatico": auto, "asistente": asistente.ESTADO if asistente.ESTADO.get("raiz", "").lower() == raiz.lower() else None} def estado_carpeta(ruta): @@ -3147,6 +3152,21 @@ class Manejador(BaseHTTPRequestHandler): CONFIG["tour_visto"] = True guardar_json(CONFIG_PATH, CONFIG) return self._json({"ok": True}) + if ruta == "/api/acciones/hacer": + # «Hacer todo lo seguro»: el mismo motor que tenía el asistente, pero lanzado desde la + # lista de acciones y sin preguntar nada. Deja fuera lo que no debe ir en automático: + # convertir (tarda horas y hay que elegir formato) y respaldar (necesita otro disco). + raiz = os.path.normpath(d["raiz"]) + if asistente.ESTADO["activo"]: + return self._json({"error": "ya hay algo en marcha en esta carpeta"}, 409) + diag = asistente.diagnostico(raiz) + filas = Plan(raiz_cfg(raiz)).correr()["filas"] + dec = {p["clave"]: p["defecto"] for p in asistente.pasos(diag, filas)} + dec["convertir"] = "no" + dec["respaldo"] = "no" + dec["carpetas"] = "seguras" # nunca las dudosas sin que alguien las mire + asistente.ejecutar(raiz, dec, filas, en_hilo=True) + return self._json({"lanzado": True, "hara": {k: v for k, v in dec.items() if v not in ("no", "dejar")}}) if ruta == "/api/asistente": raiz = os.path.normpath(d["raiz"]) if asistente.ESTADO["activo"]: diff --git a/static/index.html b/static/index.html index 96e6604..0df53db 100644 --- a/static/index.html +++ b/static/index.html @@ -327,7 +327,7 @@
-

…

+

…

@@ -588,7 +588,8 @@ function detectarCambios(){ fin('esc:' + raiz, 'Escaneo terminado.'); if (antes['ana:' + raiz] && !ahora['ana:' + raiz]) { if (BUSCANDO || vistaActual() === 'convertir') { BUSCANDO = false; irA('convertir'); cargarCandidatos().catch(e => aviso(e.message, 'bad')); } else aviso('Lectura de clips terminada.', 'ok', {texto: 'Ver clips', ir: () => { irA('convertir'); cargarCandidatos(); }}); } fin('cola', 'Conversión terminada.', {texto: 'Ver', ir: () => irA('convertir')}); - fin('asis', 'El asistente terminó.' + (ESTADO.asistente.resultado && ESTADO.asistente.resultado.errores && ESTADO.asistente.resultado.errores.length ? ` ${ESTADO.asistente.resultado.errores.length} avisos.` : ''), {texto: 'Ver historial', ir: () => irA('historial')}); + if (antes.asis && !ahora.asis && vistaActual() === 'carpeta') pintarAcciones().catch(() => {}); + fin('asis', 'Listo: se hizo todo lo seguro de esa carpeta.' + (ESTADO.asistente.resultado && ESTADO.asistente.resultado.errores && ESTADO.asistente.resultado.errores.length ? ` ${ESTADO.asistente.resultado.errores.length} avisos.` : ''), {texto: 'Ver historial', ir: () => irA('historial')}); fin('resp', 'Copia de respaldo terminada.', {texto: 'Ver', ir: () => irA('respaldar')}); if ((antes.cola !== ahora.cola || ahora.cola) && vistaActual() === 'convertir' && !document.querySelector('#convLista input:focus')) cargarCola(); } @@ -736,9 +737,24 @@ async function pintarAcciones(){ if (raizActual() !== raiz) return; // cambió de carpeta mientras llegaba ACCIONES = d.acciones || []; const quedan = ACCIONES.filter(a => a.estado === 'toca').length; - $('#acciones').innerHTML = `
${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día'}
` + + const auto = d.automatico || [], asis = d.asistente; + const corriendo = asis && asis.activo; + const cab = corriendo + ? `
Trabajando en esta carpeta${esc(NOMBRE_PASO[asis.paso] || asis.paso || '')} · ${asis.hechas || 0} de ${asis.total || 0}
` + : `
${quedan ? `Lo que se puede hacer ahora · ${quedan}` : 'Esta carpeta está al día'}${auto.length > 1 ? '' : ''}
`; + $('#acciones').innerHTML = cab + ACCIONES.map((a, i) => `
${a.estado === 'hecho' ? '✓' : a.estado === 'espera' ? '·' : i + 1}
${esc(a.titulo)}
${esc(a.que)}
${a.detalle ? `
${esc(a.detalle)}
` : ''}
${a.cifra ? `${esc(a.cifra)}` : ''}${a.boton ? `` : ''}
`).join(''); $$('#acciones [data-a]').forEach(b => b.onclick = () => { const a = ACCIONES[+b.dataset.a]; (IR_ACCION[a.ir] || (() => {}))(a.paso); }); + const bt = $('#btnHacerTodo'); + if (bt) bt.onclick = () => modal('Hacer todo lo seguro de una vez', + `

La app va a hacer seguidas las ${auto.length} cosas de la lista que no necesitan que elijas nada, en orden y sin preguntar.

+

Solo mueve lo que considera seguro: lo dudoso se queda como está para que lo mires tú. Todo se queda dentro de ${esc(raiz)}, nada se borra, y cada cambio se deshace desde el Historial.

+

No entran aquí convertir los clips ni copiar el respaldo: esos tienen su propio botón porque hay que elegir cosas.

`, + () => accion(bt, async () => { + await api('/api/acciones/hacer', {raiz}); + aviso('En marcha. El avance está en la barra de abajo y aquí arriba.', 'ok'); + await cargarEstado(); await pintarAcciones(); + })); } function queSeHizo(lote){ if (!lote) return 'Cambio'; @@ -785,13 +801,12 @@ function pasosDe(C, r){ function pintarSiguiente(P){ const C = CARPETA; const s = {t:'', p:'', acts:[]}; const b = (txt, fn, p) => s.acts.push({txt, fn, p}); - const asis = () => asistente(raizActual(), false); if (P[0].e === 'curso') { s.t = 'Escaneando la carpeta'; s.p = 'Está leyendo nombres, tamaños y fechas. No toca nada. Cuando termine, aquí aparece el diagnóstico.'; } 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[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(); }); b('Asistente paso a paso', asis); } + 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 (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); b('Asistente paso a paso', asis); } + 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 === '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 === 'pend') { const cv = C.convertir; s.t = 'Siguiente: aligerar el material de cámara'; s.p = `Hay ${gb(cv.camara_bytes)} de clips de cámara. Los de proyectos ya entregados pueden pasar a MP4 10 bits, unas 10 veces más ligeros. ${cv.raw_bytes ? 'Los BRAW necesitan DaVinci Resolve Studio abierto. ' : ''}El original no se toca hasta que reemplaces.`; b('Buscar clips a convertir', () => { irA('convertir'); $('#btnBuscarClips').click(); }, true); b('Saltar a respaldar', () => irA('respaldar')); } @@ -1261,7 +1276,6 @@ const asistente = seguro(async (raiz, escanearAntes) => { $('#modalSi').textContent = 'Empezar'; }); const NOMBRE_PASO = {plan: 'pensando el plan', ordenar: 'ordenando carpetas y archivos', stock: 'apartando stock viejo', duplicados: 'juntando repetidos', analizar: 'leyendo los clips', convertir: 'convirtiendo a 10 bits', respaldo: 'copiando el respaldo', fin: 'listo'}; -$('#btnAsistente').onclick = () => asistente(raizActual(), false); // ---------------- modal ---------------- function modal(titulo, html, onSi){ $('#modalTitulo').textContent = titulo; $('#modalCuerpo').innerHTML = html; $('#modal').classList.add('on'); $('#modalSi').style.display = onSi ? '' : 'none'; $('#modalSi').textContent = 'Confirmar'; $('#modalNo').textContent = onSi ? 'Cancelar' : 'Cerrar'; $('#modalSi').onclick = () => { $('#modal').classList.remove('on'); onSi && onSi(); }; $('#modalNo').onclick = () => $('#modal').classList.remove('on'); setTimeout(() => { const f = $('#modalCuerpo input, #modalCuerpo select') || $('#modalSi'); if (f && f.offsetParent) f.focus(); }, 30); }