2.6.2: arranque en blanco

- Una instalación nueva no trae carpetas de fábrica: CONFIG_DEFECTO
  arrancaba con David Reyes, Danney y Camilo apuntando a Z:, W: y N:.
  raices viene vacía; el diccionario de clientes y discos sigue de serie
  porque es conocimiento, no estado.
- Al abrir no se entra sola en ninguna carpeta: la primera opción del
  desplegable está vacía y la portada enseña «Tus carpetas» con su último
  escaneo, para entrar con un clic.
- Un config.json ilegible se aparta en config.json.roto-<fecha> en vez de
  sustituirse por los valores de fábrica en silencio.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Este commit está contenido en:
Labstream
2026-09-25 08:10:36 -05:00
padre bcaa80df12
commit 2daffc175a
Se han modificado 5 ficheros con 86 adiciones y 20 borrados

Ver fichero

@@ -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 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. 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-<fecha>` 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 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ó 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: con Resolve Studio 21.1 pero no pudo abrir proyecto porque Resolve estaba en el Gestor de proyectos:

Ver fichero

@@ -1,19 +1,19 @@
{ {
"nombre": "OrganiLAB", "nombre": "OrganiLAB",
"version": "2.6.1", "version": "2.6.2",
"builtAt": "2026-09-25T02:11:07.000Z", "builtAt": "2026-09-25T13:05:26.000Z",
"archivo": "OrganizadorLabstream-portatil-2.6.1.zip", "archivo": "OrganizadorLabstream-portatil-2.6.2.zip",
"size": 198766191, "size": 198766962,
"sha256": "8823149160b68f7c1bc4f0e70e66de05183a1df5373ddbbdba8b3ed9f473ad6d", "sha256": "a9fc3a1ad72a0473cda1c163400ff6ef542105c04075da823cb2c75344754006",
"app": { "app": {
"archivo": "OrganizadorLabstream-app-2.6.1.zip", "archivo": "OrganizadorLabstream-app-2.6.2.zip",
"size": 14946432, "size": 14947203,
"sha256": "b86022cd0d5f6e5fa20c662f70107e5b8f23868c40db5106a7dabe2ac3aa9346", "sha256": "97573e047f879249fb7770db9ba4a480b0341cea889e56c0a368858e2d3ca38b",
"url": "/api/herramientas/OrganizadorLabstream-app-2.6.1.zip" "url": "/api/herramientas/OrganizadorLabstream-app-2.6.2.zip"
}, },
"notes": "", "notes": "",
"firma": { "firma": {
"alg": "rsa2048-pkcs1v15-sha256", "alg": "rsa2048-pkcs1v15-sha256",
"valor": "982d481cefe1f7383ae6575c891644553beb64c11dd21cf6d37b951fedb10abc34f7d867f44eec4983f203ae0fdb45ee38b484b8afef8b41f621a3c5820cc643cc19eeb9b8c297ae43baf7ac44a913a962f1bc8b5efed189662e91685ad3a4689c4c0d23f6975e8ef2886c9d8126f4d5f8a64f822d891729661e793e962ab0a20fba68fe4f503462ae6d6d7bf2b5cb16abf2eac57b8026d61a84b09d12500c6b19e85fd22eddb13956e4d45a322cd96b0274791ba7e05488f297f955fc4af048a66a0efe376e8476366f506a46a8435373b799c46d4540ec2c9547f275b19287c04f10a8d44b05df6555678b46e99fb76d1b54e8da82433067dd787afda35fc8" "valor": "886666421a2acccc37e09fcf52c8275ce1f5a7f524bc359e1d0b93a993fe10b59394b8996964a3733c2f710d871e3426eb584e13071800d92ce24fd22eb4a8f007ffb36055eb0bf72ed6cd40791c360eef9b922bde682f97e881647b31c371dd0c1112da4d4bc7e6bcb456220726e7c2736294c538a6c586ac5b09241ece81909b01843090f3044f93d8116df26aeccfca12c4aad3f2cb131397504a6a1558bc21f5113b064438b9fbdad52e1a4a7d8844af523b5986da66d644d377c7b3bc8f6cf6d55cc925db9652d366944ef544a0ef083dbd92fcd8eefcc0f988c04e097bb28adf6de063dd1a51f7e4fc543b3be6a505f2d254394b8cef0d4672397058d2"
} }
} }

Ver fichero

@@ -37,7 +37,7 @@ import actualizador
asistente.O = sys.modules[__name__] asistente.O = sys.modules[__name__]
actualizador.O = sys.modules[__name__] actualizador.O = sys.modules[__name__]
VERSION = "2.6.1" VERSION = "2.6.2"
CONGELADO = getattr(sys, "frozen", False) 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 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 = { CONFIG_DEFECTO = {
"raices": [ # ninguna carpeta puesta de fábrica: la primera vez el programa abre en blanco y las carpetas
{"ruta": r"Z:\Dermatologas\David Reyes", "cliente": "David Reyes", "tipo": "contenido"}, # entran cuando alguien las suelta. Lo que sí viene de serie es el diccionario de clientes y
{"ruta": r"W:\Danney", "cliente": "Danney Gómez", "tipo": "contenido"}, # discos del estudio, que es conocimiento, no estado.
{"ruta": "N:\\", "cliente": "Camilo Ortega", "tipo": "pauta"}, "raices": [],
],
"clientes": { "clientes": {
"David Reyes": {"tipo": "contenido", "alias": ["DR. DAVID REYES", "David Reyes", "Davids", "Dr David Reyes"]}, "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"]}, "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: try:
with open(ruta, "r", encoding="utf-8") as f: with open(ruta, "r", encoding="utf-8") as f:
return json.load(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)) return json.loads(json.dumps(defecto))

Ver fichero

@@ -70,6 +70,26 @@ class Reglas(unittest.TestCase):
self.assertFalse(R.es_proxy_dir("Entregas")) 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): class Firma(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -183,6 +203,8 @@ class Servidor(unittest.TestCase):
def test_recorrido(self): def test_recorrido(self):
q = "?raiz=" + urllib.parse.quote(self.raiz) 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 # peticiones que no son JSON: fuera
rq = urllib.request.Request(self.base + "/api/raices", b"{}", {"Content-Type": "text/plain"}) rq = urllib.request.Request(self.base + "/api/raices", b"{}", {"Content-Type": "text/plain"})
with self.assertRaises(urllib.error.HTTPError) as cm: with self.assertRaises(urllib.error.HTTPError) as cm:

Ver fichero

@@ -99,6 +99,10 @@
.vacio-caja .o{margin-top:16px;font-size:13px;color:var(--mut)} .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-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} .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} .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}} @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)} .peso{display:flex;height:14px;margin:4px 0 8px;border-radius:4px;overflow:hidden;gap:1px;background:var(--panel2)}
@@ -352,6 +356,7 @@
<p>OrganiLAB la escanea, te enseña qué pesa y te lleva paso a paso. No mueve nada sin que lo confirmes, y nunca borra.</p> <p>OrganiLAB la escanea, te enseña qué pesa y te lleva paso a paso. No mueve nada sin que lo confirmes, y nunca borra.</p>
<div class="o"><button class="p" id="btnElegir2" type="button">Elegir carpeta…</button> &nbsp; o <u>pega la ruta</u></div> <div class="o"><button class="p" id="btnElegir2" type="button">Elegir carpeta…</button> &nbsp; o <u>pega la ruta</u></div>
</div> </div>
<div id="portadaCarpetas"></div>
<div class="vacio-pasos"> <div class="vacio-pasos">
<div><b>1 · Escanear</b>Lee nombres, tamaños y fechas. Solo lectura.</div> <div><b>1 · Escanear</b>Lee nombres, tamaños y fechas. Solo lectura.</div>
<div><b>2 · Revisar</b>Qué pesa: BRAW, cámara, stock, proxies, basura, repetidos.</div> <div><b>2 · Revisar</b>Qué pesa: BRAW, cámara, stock, proxies, basura, repetidos.</div>
@@ -635,8 +640,10 @@ let ULTIMO = null;
async function cargarEstado(){ async function cargarEstado(){
ESTADO = await api('/api/estado'); ESTADO = await api('/api/estado');
const sel = $('#raiz'); const antes = sel.value; const sel = $('#raiz'); const antes = sel.value;
sel.innerHTML = ESTADO.raices.map(r => `<option value="${esc(r.clave)}">${esc(r.cliente)}</option>`).join(''); // la primera opción está vacía a propósito: al abrir el programa no se entra en ninguna carpeta, la
if (antes && ESTADO.raices.some(r => r.clave === antes)) sel.value = antes; // portada las enseña y se elige una. Antes el desplegable caía solo en la primera registrada.
sel.innerHTML = '<option value=""></option>' + ESTADO.raices.map(r => `<option value="${esc(r.clave)}">${esc(r.cliente)}</option>`).join('');
sel.value = (antes && ESTADO.raices.some(r => r.clave === antes)) ? antes : '';
pintarIndice(); pintarIndice();
const fmt = $('#optFormato'); if (!fmt.options.length && ESTADO.formatos) { fmt.innerHTML = Object.entries(ESTADO.formatos).map(([k, v]) => `<option value="${k}">${esc(v.nombre)}</option>`).join(''); fmt.value = ESTADO.config.optimizar.formato || 'h265_10bit'; pintarFormato(); cargarMotores(); } const fmt = $('#optFormato'); if (!fmt.options.length && ESTADO.formatos) { fmt.innerHTML = Object.entries(ESTADO.formatos).map(([k, v]) => `<option value="${k}">${esc(v.nombre)}</option>`).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]) => `<option value="${k}">${esc(v.nombre)}</option>`).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(); } const perf = $('#optPerfil'); if (!perf.options.length) { perf.innerHTML = Object.entries(ESTADO.perfiles).map(([k,v]) => `<option value="${k}">${esc(v.nombre)}</option>`).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 ---------------- // ---------------- 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'};
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'};
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 = '<h3 style="margin:22px 0 8px">Tus carpetas</h3><div class="portada">' + rs.map(r => {
const e = r.escaneo;
return `<div class="pc" data-r="${esc(r.clave)}" role="button" tabindex="0"><b>${esc(r.cliente)}</b><span class="path">${esc(r.ruta)}</span><span class="mut">${esc(ESTADO.tipos[r.tipo] || r.tipo)}</span><span class="mut">${e ? `${num(e.n)} archivos · ${gb(e.bytes)} · escaneada ${fecha(e.ts)}` : 'sin escanear todavía'}</span></div>`;
}).join('') + '</div>';
$$('#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(){ async function cargarCarpeta(){
const raiz = raizActual(); const raiz = raizActual();
$('#inicioVacio').style.display = raiz ? 'none' : 'block'; $('#inicioVacio').style.display = raiz ? 'none' : 'block';
$('#inicioLleno').style.display = raiz ? '' : 'none'; $('#inicioLleno').style.display = raiz ? '' : 'none';
if (!raiz) return; if (!raiz) { pintarPortada(); return; }
const r = cfgRaiz(); const r = cfgRaiz();
CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raiz)); CARPETA = await api('/api/carpeta/estado?raiz=' + encodeURIComponent(raiz));
$('#cabTitulo').textContent = r.cliente; $('#cabTitulo').textContent = r.cliente;