diff --git a/README.md b/README.md index 682cac9..70485e1 100644 --- a/README.md +++ b/README.md @@ -574,6 +574,22 @@ salvo que alguien encienda el interruptor en Ajustes; «Buscar actualizaciones» nadie lo eligió): al arrancar la 2.6.1, ese valor se apaga; el que se encienda desde Ajustes queda marcado como elegido y se respeta. +2.6.2, arranque en blanco. Munka abrió la app y se encontró dentro de la carpeta de Galeano, y preguntó +si era un caché: no lo era, es la única carpeta registrada en su `config.json` (la añadió el 16 de +septiembre y ese mismo día aplicó dos lotes sobre ella). Pero la pregunta destapó dos cosas de verdad: + +- **Una instalación nueva no abría en blanco**: `CONFIG_DEFECTO` traía tres carpetas de fábrica (David + Reyes, Danney, Camilo) apuntando a Z:, W: y N:, así que en un equipo sin esos discos el programa + arrancaba con tres clientes «sin escanear» que nadie había puesto. Ahora `raices` viene vacía; lo + que sigue de serie es el diccionario de clientes y discos, que es + conocimiento del estudio, no estado. Prueba nueva: una copia recién hecha responde `raices: []`. +- **Al abrir, el desplegable caía solo en la primera carpeta registrada** (con una sola, en esa). Ahora + la primera opción está vacía y la portada enseña «Tus carpetas» —cada una con su último escaneo— para + entrar con un clic. Nada se abre solo. +- Y un fallo latente: un `config.json` ilegible (una coma de más al editarlo a mano) se sustituía por + los valores de fábrica y se guardaba encima, y las carpetas registradas y los clientes desaparecían sin + decir nada. Ahora se aparta en `config.json.roto-` y se avisa en consola. Con prueba. + 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 bc5eb53..215d4f8 100644 --- a/manifiesto.json +++ b/manifiesto.json @@ -1,19 +1,19 @@ { "nombre": "OrganiLAB", - "version": "2.6.1", - "builtAt": "2026-09-25T02:11:07.000Z", - "archivo": "OrganizadorLabstream-portatil-2.6.1.zip", - "size": 198766191, - "sha256": "8823149160b68f7c1bc4f0e70e66de05183a1df5373ddbbdba8b3ed9f473ad6d", + "version": "2.6.2", + "builtAt": "2026-09-25T13:05:26.000Z", + "archivo": "OrganizadorLabstream-portatil-2.6.2.zip", + "size": 198766962, + "sha256": "a9fc3a1ad72a0473cda1c163400ff6ef542105c04075da823cb2c75344754006", "app": { - "archivo": "OrganizadorLabstream-app-2.6.1.zip", - "size": 14946432, - "sha256": "b86022cd0d5f6e5fa20c662f70107e5b8f23868c40db5106a7dabe2ac3aa9346", - "url": "/api/herramientas/OrganizadorLabstream-app-2.6.1.zip" + "archivo": "OrganizadorLabstream-app-2.6.2.zip", + "size": 14947203, + "sha256": "97573e047f879249fb7770db9ba4a480b0341cea889e56c0a368858e2d3ca38b", + "url": "/api/herramientas/OrganizadorLabstream-app-2.6.2.zip" }, "notes": "", "firma": { "alg": "rsa2048-pkcs1v15-sha256", - "valor": "982d481cefe1f7383ae6575c891644553beb64c11dd21cf6d37b951fedb10abc34f7d867f44eec4983f203ae0fdb45ee38b484b8afef8b41f621a3c5820cc643cc19eeb9b8c297ae43baf7ac44a913a962f1bc8b5efed189662e91685ad3a4689c4c0d23f6975e8ef2886c9d8126f4d5f8a64f822d891729661e793e962ab0a20fba68fe4f503462ae6d6d7bf2b5cb16abf2eac57b8026d61a84b09d12500c6b19e85fd22eddb13956e4d45a322cd96b0274791ba7e05488f297f955fc4af048a66a0efe376e8476366f506a46a8435373b799c46d4540ec2c9547f275b19287c04f10a8d44b05df6555678b46e99fb76d1b54e8da82433067dd787afda35fc8" + "valor": "886666421a2acccc37e09fcf52c8275ce1f5a7f524bc359e1d0b93a993fe10b59394b8996964a3733c2f710d871e3426eb584e13071800d92ce24fd22eb4a8f007ffb36055eb0bf72ed6cd40791c360eef9b922bde682f97e881647b31c371dd0c1112da4d4bc7e6bcb456220726e7c2736294c538a6c586ac5b09241ece81909b01843090f3044f93d8116df26aeccfca12c4aad3f2cb131397504a6a1558bc21f5113b064438b9fbdad52e1a4a7d8844af523b5986da66d644d377c7b3bc8f6cf6d55cc925db9652d366944ef544a0ef083dbd92fcd8eefcc0f988c04e097bb28adf6de063dd1a51f7e4fc543b3be6a505f2d254394b8cef0d4672397058d2" } } \ No newline at end of file diff --git a/organizador.py b/organizador.py index c3897d1..125b0a5 100644 --- a/organizador.py +++ b/organizador.py @@ -37,7 +37,7 @@ import actualizador asistente.O = sys.modules[__name__] actualizador.O = sys.modules[__name__] -VERSION = "2.6.1" +VERSION = "2.6.2" 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 @@ -58,11 +58,10 @@ LOCK = threading.RLock() # ========================================================================== CONFIG_DEFECTO = { - "raices": [ - {"ruta": r"Z:\Dermatologas\David Reyes", "cliente": "David Reyes", "tipo": "contenido"}, - {"ruta": r"W:\Danney", "cliente": "Danney Gómez", "tipo": "contenido"}, - {"ruta": "N:\\", "cliente": "Camilo Ortega", "tipo": "pauta"}, - ], + # ninguna carpeta puesta de fábrica: la primera vez el programa abre en blanco y las carpetas + # entran cuando alguien las suelta. Lo que sí viene de serie es el diccionario de clientes y + # discos del estudio, que es conocimiento, no estado. + "raices": [], "clientes": { "David Reyes": {"tipo": "contenido", "alias": ["DR. DAVID REYES", "David Reyes", "Davids", "Dr David Reyes"]}, "Danney Gómez": {"tipo": "contenido", "alias": ["Danney", "DRA. DANNEY GOMEZ", "Dra_Danney", "Dra Danney", "Skincenter"]}, @@ -203,7 +202,18 @@ def cargar_json(ruta, defecto): try: with open(ruta, "r", encoding="utf-8") as f: return json.load(f) - except (OSError, ValueError): + except OSError: + return json.loads(json.dumps(defecto)) + except ValueError as ex: + # el archivo existe pero no se puede leer (una edición a mano con una coma de más, un corte a + # medias). Antes se sustituía por los valores de fábrica y se guardaba encima: las carpetas + # registradas y los clientes desaparecían sin decir nada. Ahora se aparta con copia. + copia = f"{ruta}.roto-{time.strftime('%Y%m%d-%H%M%S')}" + try: + shutil.copy2(ruta, copia) + except OSError: + copia = "(no se pudo copiar)" + print(f"AVISO: {os.path.basename(ruta)} no se pudo leer ({ex}); se apartó en {copia} y se parte de cero.") return json.loads(json.dumps(defecto)) diff --git a/probar.py b/probar.py index d837624..cd5e022 100644 --- a/probar.py +++ b/probar.py @@ -70,6 +70,26 @@ class Reglas(unittest.TestCase): self.assertFalse(R.es_proxy_dir("Entregas")) +class Config(unittest.TestCase): + def test_config_roto_se_aparta(self): + """Un config.json ilegible no se pisa en silencio: queda una copia .roto-* y se parte de cero.""" + tmp = tempfile.mkdtemp(prefix="organilab-cfg-") + try: + ruta = os.path.join(tmp, "config.json") + with open(ruta, "w", encoding="utf-8") as fh: + fh.write('{"raices": [ esto no es json') + src = open(os.path.join(AQUI, "organizador.py"), encoding="utf-8").read() + ini = src.index("def cargar_json(") + fin = src.index("def guardar_json(") + ns = {"json": json, "os": os, "shutil": shutil, "time": time} + exec(src[ini:fin], ns) + out = ns["cargar_json"](ruta, {"raices": [], "a": 1}) + self.assertEqual(out, {"raices": [], "a": 1}) + self.assertTrue([f for f in os.listdir(tmp) if f.startswith("config.json.roto-")], "no dejó copia del roto") + finally: + shutil.rmtree(tmp, ignore_errors=True) + + class Firma(unittest.TestCase): @classmethod def setUpClass(cls): @@ -183,6 +203,8 @@ class Servidor(unittest.TestCase): def test_recorrido(self): q = "?raiz=" + urllib.parse.quote(self.raiz) + # 0. una instalación nueva abre en blanco: ninguna carpeta puesta de fábrica + self.assertEqual(self.get("/api/estado")["raices"], []) # peticiones que no son JSON: fuera rq = urllib.request.Request(self.base + "/api/raices", b"{}", {"Content-Type": "text/plain"}) with self.assertRaises(urllib.error.HTTPError) as cm: diff --git a/static/index.html b/static/index.html index 178cd46..f8f209f 100644 --- a/static/index.html +++ b/static/index.html @@ -99,6 +99,10 @@ .vacio-caja .o{margin-top:16px;font-size:13px;color:var(--mut)} .vacio-caja .o u{color:var(--acc);text-decoration:none;border-bottom:1px solid var(--acc)} .vacio-pasos{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-top:24px;font-size:12.5px} + .portada{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px} + .portada .pc{display:flex;flex-direction:column;gap:3px;padding:12px 14px;border:1px solid var(--line);border-radius:10px;cursor:pointer;background:var(--panel);font-size:12.5px} + .portada .pc:hover{border-color:var(--acc);background:var(--acc2)} + .portada .pc b{font-size:14px} .vacio-pasos b{display:block;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--mut);margin-bottom:4px} @media(max-width:900px){.vacio-pasos{grid-template-columns:1fr 1fr}} .peso{display:flex;height:14px;margin:4px 0 8px;border-radius:4px;overflow:hidden;gap:1px;background:var(--panel2)} @@ -352,6 +356,7 @@

OrganiLAB la escanea, te enseña qué pesa y te lleva paso a paso. No mueve nada sin que lo confirmes, y nunca borra.

  o pega la ruta
+
1 · EscanearLee nombres, tamaños y fechas. Solo lectura.
2 · RevisarQué pesa: BRAW, cámara, stock, proxies, basura, repetidos.
@@ -635,8 +640,10 @@ let ULTIMO = null; async function cargarEstado(){ ESTADO = await api('/api/estado'); const sel = $('#raiz'); const antes = sel.value; - sel.innerHTML = ESTADO.raices.map(r => ``).join(''); - if (antes && ESTADO.raices.some(r => r.clave === antes)) sel.value = antes; + // la primera opción está vacía a propósito: al abrir el programa no se entra en ninguna carpeta, la + // portada las enseña y se elige una. Antes el desplegable caía solo en la primera registrada. + sel.innerHTML = '' + ESTADO.raices.map(r => ``).join(''); + sel.value = (antes && ESTADO.raices.some(r => r.clave === antes)) ? antes : ''; pintarIndice(); const fmt = $('#optFormato'); if (!fmt.options.length && ESTADO.formatos) { fmt.innerHTML = Object.entries(ESTADO.formatos).map(([k, v]) => ``).join(''); fmt.value = ESTADO.config.optimizar.formato || 'h265_10bit'; pintarFormato(); cargarMotores(); } const perf = $('#optPerfil'); if (!perf.options.length) { perf.innerHTML = Object.entries(ESTADO.perfiles).map(([k,v]) => ``).join(''); perf.value = ESTADO.config.optimizar.perfil; $('#optMeses').value = ESTADO.config.optimizar.meses_minimos; $('#optMin').value = ESTADO.config.optimizar.min_mb; $('#optChroma').value = ESTADO.config.optimizar.chroma; $('#optColor').value = ESTADO.config.optimizar.color; pintarCriterio(); } @@ -770,11 +777,22 @@ $('#btnDesconectar').onclick = () => desconectar(raizActual()); // ---------------- 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 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'}; +function pintarPortada(){ + // 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. + const caja = $('#portadaCarpetas'); const rs = ESTADO.raices || []; + if (!rs.length) { caja.innerHTML = ''; return; } + caja.innerHTML = '

Tus carpetas

' + rs.map(r => { + const e = r.escaneo; + return `
${esc(r.cliente)}${esc(r.ruta)}${esc(ESTADO.tipos[r.tipo] || r.tipo)}${e ? `${num(e.n)} archivos · ${gb(e.bytes)} · escaneada ${fecha(e.ts)}` : 'sin escanear todavía'}
`; + }).join('') + '
'; + $$('#portadaCarpetas .pc').forEach(el => { const ir = () => { $('#raiz').value = el.dataset.r; $('#raiz').onchange(); }; el.onclick = ir; el.onkeydown = ev => { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); ir(); } }; }); +} async function cargarCarpeta(){ const raiz = raizActual(); $('#inicioVacio').style.display = raiz ? 'none' : 'block'; $('#inicioLleno').style.display = raiz ? '' : 'none'; - if (!raiz) return; + if (!raiz) { pintarPortada(); return; } const r = cfgRaiz(); CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raiz)); $('#cabTitulo').textContent = r.cliente;