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:
16
README.md
16
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-<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
|
||||
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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
22
probar.py
22
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:
|
||||
|
||||
@@ -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 @@
|
||||
<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> o <u>pega la ruta</u></div>
|
||||
</div>
|
||||
<div id="portadaCarpetas"></div>
|
||||
<div class="vacio-pasos">
|
||||
<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>
|
||||
@@ -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 => `<option value="${esc(r.clave)}">${esc(r.cliente)}</option>`).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 = '<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();
|
||||
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(); }
|
||||
@@ -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 = '<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(){
|
||||
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;
|
||||
|
||||
Referencia en una nueva incidencia
Block a user