@@ -14,6 +14,7 @@ import { parseBox, parseEstimate } from './sockets/maps.socket';
1414import {
1515 preflight , listInstalledChannels , browseRemoteChannels , resolveIdentifier ,
1616 browseChannelTree , estimateSelection , deleteChannel , getKolibriTask , verifyCredentials ,
17+ ChannelNotInstalledError ,
1718} from './sockets/kolibri.query' ;
1819import { checkReadiness , KolibriAuthError , KolibriApiError , login as kolibriLogin } from './sockets/kolibri.session' ;
1920import {
@@ -359,31 +360,43 @@ apiRouter.get('/system/dashboard/update-check', async (_req: Request, res: Respo
359360 }
360361} ) ;
361362
362- // --- Kolibri: readiness, catálogo y selección (ADFA-4949) --- ----------------------
363- // Consultas directas (no -job). La descarga en sí es un job durable
364- // (POST /kolibri/download), que sale gratis al añadir 'kolibri' a VALID_TYPES.
363+ // --- Kolibri: readiness, catalogue and selection (ADFA-4949) ----------------------
364+ // Direct (non -job) queries. The download itself is a durable job
365+ // (POST /kolibri/download), which comes free from adding 'kolibri' to VALID_TYPES.
365366//
366- // Nota de puertos y prefijos : nginx expone Kolibri en /kolibri/ y además le enruta
367- // /api/, /content/, /device/ y /learn/ en el :8085 compartido. Nosotros hablamos
368- // con Kolibri directamente en 127.0.0.1:8009 desde dentro, así que no hay colisión .
367+ // A note on ports and prefixes : nginx exposes Kolibri at /kolibri/ and also routes
368+ // /api/, /content/, /device/ and /learn/ to it on the shared :8085. We talk to
369+ // Kolibri directly on 127.0.0.1:8009 from inside, so there is no collision .
369370
370- /** Traduce un KolibriAuthError al status HTTP correcto . */
371+ /** Translates a Kolibri failure into the right HTTP status . */
371372function authStatus ( e : unknown ) : number {
372373 if ( e instanceof KolibriAuthError ) {
373374 switch ( e . reason ) {
374- case 'unreachable' : return 503 ; // el servicio no está listo
375+ case 'unreachable' : return 503 ; // the service is not ready
375376 case 'credentials' : return 401 ;
376377 case 'permission' : return 403 ;
377378 default : return 502 ;
378379 }
379380 }
380- // Kolibri respondió, pero con error: propagamos su semántica en lugar de un 500.
381+ // Kolibri answered, but with an error. 502 in every case that is not an auth
382+ // one: whatever Kolibri's status was, from here it is an upstream we could not
383+ // get a usable answer from, and echoing its 4xx would blame the app's caller
384+ // for a request the app itself composed. (This used to read
385+ // `e.status >= 500 ? 502 : 502` — both arms the same, so the distinction it
386+ // implied never existed.)
381387 if ( e instanceof KolibriApiError ) {
382388 if ( e . status === 401 || e . status === 403 ) return e . status ;
383- return e . status >= 500 ? 502 : 502 ; // upstream en mal estado
389+ return 502 ;
384390 }
385- // fetch aborta por timeout (AbortError) o no conecta (TypeError). Eso es
386- // "Kolibri no está listo", no "nuestro servidor se rompió": 503, no 500.
391+ // The request was well formed but a precondition is not met — the channel is
392+ // not on the device. That is the caller's state, not a server fault, so it
393+ // must not be a 500: the readable message would arrive labelled as an
394+ // internal error, which is as misleading as the bare 500 it replaced.
395+ if ( e instanceof ChannelNotInstalledError ) {
396+ return 409 ;
397+ }
398+ // fetch aborts on timeout (AbortError) or fails to connect (TypeError). That is
399+ // "Kolibri is not ready", not "our server broke": 503, not 500.
387400 if ( e instanceof Error && ( e . name === 'AbortError' || e . name === 'TimeoutError'
388401 || e instanceof TypeError ) ) {
389402 return 503 ;
@@ -392,19 +405,19 @@ function authStatus(e: unknown): number {
392405}
393406
394407// El "gate" de arranque: mientras ready sea false, no tiene sentido lanzar jobs.
395- // Nunca lanza: siempre devuelve un diagnóstico con los bloqueadores en claro .
408+ // Never throws: it always returns a diagnosis with the blockers spelled out .
396409apiRouter . get ( '/kolibri/ready' , async ( _req : Request , res : Response ) : Promise < void > => {
397410 try {
398411 const readiness = await checkReadiness ( ) ;
399- // 200 siempre: es un endpoint de estado, no una operación. El cliente mira
412+ // Always 200: this is a status endpoint, not an operation. The client reads
400413 // el campo `ready`, no el status.
401414 res . json ( readiness ) ;
402415 } catch ( e : any ) {
403416 res . status ( 500 ) . json ( { ready : false , blockers : [ e ?. message || 'readiness failed' ] } ) ;
404417 }
405418} ) ;
406419
407- // Diagnóstico ampliado : readiness + estado local (canales , bytes en disco, rutas ).
420+ // Extended diagnosis : readiness plus local state (channels , bytes on disk, paths ).
408421apiRouter . get ( '/kolibri/preflight' , async ( _req : Request , res : Response ) : Promise < void > => {
409422 try {
410423 res . json ( await preflight ( ) ) ;
@@ -413,8 +426,8 @@ apiRouter.get('/kolibri/preflight', async (_req: Request, res: Response): Promis
413426 }
414427} ) ;
415428
416- // Qué hay ya en el dispositivo. Lectura local en readonly: no necesita ni sesión
417- // ni que Kolibri esté arriba .
429+ // What is already on the device. A local readonly read: it needs neither a session
430+ // nor Kolibri to be up .
418431apiRouter . get ( '/kolibri/channels' , ( _req : Request , res : Response ) : void => {
419432 try {
420433 res . json ( listInstalledChannels ( ) ) ;
@@ -423,7 +436,7 @@ apiRouter.get('/kolibri/channels', (_req: Request, res: Response): void => {
423436 }
424437} ) ;
425438
426- // Catálogo remoto para el selector del wizard. ?keyword= y ?language= opcionales .
439+ // Remote catalogue for the wizard's picker . ?keyword= and ?language= are optional .
427440apiRouter . get ( '/kolibri/catalog' , async ( req : Request , res : Response ) : Promise < void > => {
428441 try {
429442 res . json ( await browseRemoteChannels ( {
@@ -446,10 +459,10 @@ apiRouter.get('/kolibri/resolve/:identifier', async (req: Request, res: Response
446459 }
447460} ) ;
448461
449- // Un nivel del árbol del canal, para elegir subárboles .
462+ // One level of the channel tree, for choosing subtrees .
450463// PRECONDICIÓN: los metadatos del canal deben estar ya en la base local, o sea que
451464// hay que haber importado el canal antes. Es el mismo flujo que la UI de Kolibri:
452- // primero los metadatos (MB), luego la selección del contenido (GB).
465+ // metadata first (MB), then the content selection (GB).
453466apiRouter . get ( '/kolibri/tree/:channelId' , async ( req : Request , res : Response ) : Promise < void > => {
454467 try {
455468 const nodeId = req . query . nodeId ? String ( req . query . nodeId ) : undefined ;
@@ -459,7 +472,7 @@ apiRouter.get('/kolibri/tree/:channelId', async (req: Request, res: Response): P
459472 }
460473} ) ;
461474
462- // Tamaño exacto de una selección + espacio libre, para el paso de consentimiento .
475+ // What a selection still has to transfer, plus free space, for the consent step .
463476// Body: { channelId, nodeIds?, excludeNodeIds? }
464477apiRouter . post ( '/kolibri/estimate' , async ( req : Request , res : Response ) : Promise < void > => {
465478 const body = req . body as { channelId ?: unknown ; nodeIds ?: unknown ; excludeNodeIds ?: unknown } ;
@@ -477,7 +490,7 @@ apiRouter.post('/kolibri/estimate', async (req: Request, res: Response): Promise
477490} ) ;
478491
479492// Borrar un canal. Devuelve el id del job de Kolibri, no del motor local: es una
480- // operación corta y no merece un job durable.
493+ // a short operation and does not warrant a durable job .
481494apiRouter . post ( '/kolibri/delete' , async ( req : Request , res : Response ) : Promise < void > => {
482495 const body = req . body as { channelId ?: unknown ; channelName ?: unknown } ;
483496 const channelId = String ( body ?. channelId ?? '' ) . trim ( ) ;
@@ -493,7 +506,7 @@ apiRouter.post('/kolibri/delete', async (req: Request, res: Response): Promise<v
493506} ) ;
494507
495508// Estado de una tarea lanzada directamente en Kolibri (hoy solo el borrado). Sin
496- // esto , /kolibri/delete devolvía un id que ningún endpoint sabía consultar .
509+ // this , /kolibri/delete returned an id no endpoint knew how to query .
497510apiRouter . get ( '/kolibri/task/:id' , async ( req : Request , res : Response ) : Promise < void > => {
498511 try {
499512 res . json ( await getKolibriTask ( String ( req . params . id ) ) ) ;
@@ -504,8 +517,8 @@ apiRouter.get('/kolibri/task/:id', async (req: Request, res: Response): Promise<
504517} ) ;
505518
506519// --- Credenciales de servicios (ADFA-4949) ---------------------------------------
507- // Permite al webview actualizar usuario/contraseña sin recompilar. La contraseña
508- // NUNCA se devuelve: el GET solo dice qué usuario hay y de dónde sale .
520+ // Lets the webview update the username/password without a rebuild. The password is
521+ // NEVER returned: the GET only says which user is set and where it came from .
509522
510523apiRouter . get ( '/credentials/:service' , ( req : Request , res : Response ) : void => {
511524 const service = String ( req . params . service ) ;
@@ -517,8 +530,8 @@ apiRouter.get('/credentials/:service', (req: Request, res: Response): void => {
517530 }
518531} ) ;
519532
520- // Valida ANTES de persistir: así el error aparece en el formulario donde se cometió,
521- // no media hora después en una descarga fallida .
533+ // Validates BEFORE persisting, so the error surfaces in the form where it was made
534+ // rather than half an hour later in a failed download .
522535// 401 → credenciales rechazadas (no se guarda nada)
523536// 403 → autentica pero no puede gestionar contenido (no se guarda nada)
524537// 503 → Kolibri no responde; la capa de arranque debe resolverlo primero
@@ -579,7 +592,7 @@ apiRouter.post('/credentials/:service', async (req: Request, res: Response): Pro
579592 try {
580593 const check = await verifyCredentials ( username , password ) ;
581594 if ( ! check . canManageContent ) {
582- // Distinguir esto de "contraseña mala" ahorra horas de diagnóstico: el
595+ // Telling this apart from "wrong password" saves hours of diagnosis: the
583596 // usuario existe y la clave es correcta, pero no puede importar contenido.
584597 res . status ( 403 ) . json ( {
585598 error : `'${ username } ' autentica pero no tiene permiso para gestionar contenido` ,
@@ -593,14 +606,14 @@ apiRouter.post('/credentials/:service', async (req: Request, res: Response): Pro
593606 const status = authStatus ( e ) ;
594607 res . status ( status ) . json ( {
595608 error : status === 401
596- ? 'Kolibri rechazó estas credenciales '
609+ ? 'Kolibri rejected these credentials '
597610 : ( e ?. message || 'verification failed' ) ,
598611 saved : false ,
599612 } ) ;
600613 }
601614} ) ;
602615
603- // Volver al valor de fábrica (o a la variable de entorno, si está puesta ).
616+ // Back to the factory value (or to the environment variable, if one is set ).
604617apiRouter . delete ( '/credentials/:service' , ( req : Request , res : Response ) : void => {
605618 const service = String ( req . params . service ) ;
606619 if ( ! isServiceName ( service ) ) { res . status ( 404 ) . json ( { error : 'unknown service' } ) ; return ; }
0 commit comments